Skip to content

Commit 5cdeb75

Browse files
devversionandrewseguin
authored andcommitted
fix(slide-toggle): input not updated after drag (#3067)
The `MdSlideToggle` uses the OnPush ChangeDetection strategy for the component. **Problem Explanation** When the thumb is being dragged and the value changes, the new value is currently applied after a timeout. This timeout does not trigger any change detection, and the new value won't be applied to the underlying input. **Solution** With this change, the new value is applied immediately in the `dragend` event (because events trigger a change detection) and just the `dragging` state will be updated in the next tick. The timeout is required to ensure that there will be no `click` event after the `dragend` event.
1 parent bc9d25b commit 5cdeb75

File tree

2 files changed

+67
-35
lines changed

2 files changed

+67
-35
lines changed

src/lib/slide-toggle/slide-toggle.spec.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ describe('MdSlideToggle', () => {
438438
let slideToggleElement: HTMLElement;
439439
let slideToggleControl: NgControl;
440440
let slideThumbContainer: HTMLElement;
441+
let inputElement: HTMLInputElement;
441442

442443
beforeEach(async(() => {
443444
fixture = TestBed.createComponent(SlideToggleTestApp);
@@ -453,6 +454,8 @@ describe('MdSlideToggle', () => {
453454
slideToggleElement = slideToggleDebug.nativeElement;
454455
slideToggleControl = slideToggleDebug.injector.get(NgControl);
455456
slideThumbContainer = thumbContainerDebug.nativeElement;
457+
458+
inputElement = slideToggleElement.querySelector('input');
456459
}));
457460

458461
it('should drag from start to end', fakeAsync(() => {
@@ -495,7 +498,7 @@ describe('MdSlideToggle', () => {
495498
expect(slideThumbContainer.classList).not.toContain('mat-dragging');
496499
}));
497500

498-
it('should not drag when disbaled', fakeAsync(() => {
501+
it('should not drag when disabled', fakeAsync(() => {
499502
slideToggle.disabled = true;
500503

501504
expect(slideToggle.checked).toBe(false);
@@ -538,6 +541,28 @@ describe('MdSlideToggle', () => {
538541
expect(testComponent.lastEvent.checked).toBe(true);
539542
}));
540543

544+
it('should update the checked property of the input', fakeAsync(() => {
545+
expect(inputElement.checked).toBe(false);
546+
547+
gestureConfig.emitEventForElement('slidestart', slideThumbContainer);
548+
549+
expect(slideThumbContainer.classList).toContain('mat-dragging');
550+
551+
gestureConfig.emitEventForElement('slide', slideThumbContainer, {
552+
deltaX: 200 // Arbitrary, large delta that will be clamped to the end of the slide-toggle.
553+
});
554+
555+
gestureConfig.emitEventForElement('slideend', slideThumbContainer);
556+
fixture.detectChanges();
557+
558+
expect(inputElement.checked).toBe(true);
559+
560+
// Flush the timeout for the slide ending.
561+
tick();
562+
563+
expect(slideThumbContainer.classList).not.toContain('mat-dragging');
564+
}));
565+
541566
});
542567

543568
describe('with a FormControl', () => {

src/lib/slide-toggle/slide-toggle.ts

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
134134
event.stopPropagation();
135135

136136
// Once a drag is currently in progress, we do not want to toggle the slide-toggle on a click.
137-
if (!this.disabled && !this._slideRenderer.isDragging()) {
137+
if (!this.disabled && !this._slideRenderer.dragging) {
138138
this.toggle();
139139

140140
// Emit our custom change event if the native input emitted one.
@@ -255,22 +255,20 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
255255
}
256256

257257
_onDrag(event: HammerInput) {
258-
if (this._slideRenderer.isDragging()) {
258+
if (this._slideRenderer.dragging) {
259259
this._slideRenderer.updateThumbPosition(event.deltaX);
260260
}
261261
}
262262

263263
_onDragEnd() {
264-
if (!this._slideRenderer.isDragging()) {
265-
return;
266-
}
267-
268-
// Notice that we have to stop outside of the current event handler,
269-
// because otherwise the click event will be fired and will reset the new checked variable.
270-
setTimeout(() => {
271-
this.checked = this._slideRenderer.stopThumbDrag();
264+
if (this._slideRenderer.dragging) {
265+
this.checked = this._slideRenderer.dragPercentage > 50;
272266
this._emitChangeEvent();
273-
}, 0);
267+
268+
// The drag should be stopped outside of the current event handler, because otherwise the
269+
// click event will be fired before and will revert the drag change.
270+
setTimeout(() => this._slideRenderer.stopThumbDrag());
271+
}
274272
}
275273

276274
}
@@ -280,56 +278,65 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
280278
*/
281279
class SlideToggleRenderer {
282280

281+
/** Reference to the thumb HTMLElement. */
283282
private _thumbEl: HTMLElement;
283+
284+
/** Reference to the thumb bar HTMLElement. */
284285
private _thumbBarEl: HTMLElement;
286+
287+
/** Width of the thumb bar of the slide-toggle. */
285288
private _thumbBarWidth: number;
286-
private _checked: boolean;
287-
private _percentage: number;
289+
290+
/** Previous checked state before drag started. */
291+
private _previousChecked: boolean;
292+
293+
/** Percentage of the thumb while dragging. */
294+
dragPercentage: number;
295+
296+
/** Whether the thumb is currently being dragged. */
297+
dragging: boolean = false;
288298

289299
constructor(private _elementRef: ElementRef) {
290300
this._thumbEl = _elementRef.nativeElement.querySelector('.mat-slide-toggle-thumb-container');
291301
this._thumbBarEl = _elementRef.nativeElement.querySelector('.mat-slide-toggle-bar');
292302
}
293303

294-
/** Whether the slide-toggle is currently dragging. */
295-
isDragging(): boolean {
296-
return !!this._thumbBarWidth;
297-
}
298-
299-
300304
/** Initializes the drag of the slide-toggle. */
301305
startThumbDrag(checked: boolean) {
302-
if (!this.isDragging()) {
303-
this._thumbBarWidth = this._thumbBarEl.clientWidth - this._thumbEl.clientWidth;
304-
this._checked = checked;
305-
this._thumbEl.classList.add('mat-dragging');
306-
}
306+
if (this.dragging) { return; }
307+
308+
this._thumbBarWidth = this._thumbBarEl.clientWidth - this._thumbEl.clientWidth;
309+
this._thumbEl.classList.add('mat-dragging');
310+
311+
this._previousChecked = checked;
312+
this.dragging = true;
307313
}
308314

309-
/** Stops the current drag and returns the new checked value. */
315+
/** Resets the current drag and returns the new checked value. */
310316
stopThumbDrag(): boolean {
311-
if (this.isDragging()) {
312-
this._thumbBarWidth = null;
313-
this._thumbEl.classList.remove('mat-dragging');
317+
if (!this.dragging) { return; }
314318

315-
applyCssTransform(this._thumbEl, '');
319+
this.dragging = false;
320+
this._thumbEl.classList.remove('mat-dragging');
316321

317-
return this._percentage > 50;
318-
}
322+
// Reset the transform because the component will take care of the thumb position after drag.
323+
applyCssTransform(this._thumbEl, '');
324+
325+
return this.dragPercentage > 50;
319326
}
320327

321328
/** Updates the thumb containers position from the specified distance. */
322329
updateThumbPosition(distance: number) {
323-
this._percentage = this._getThumbPercentage(distance);
324-
applyCssTransform(this._thumbEl, `translate3d(${this._percentage}%, 0, 0)`);
330+
this.dragPercentage = this._getThumbPercentage(distance);
331+
applyCssTransform(this._thumbEl, `translate3d(${this.dragPercentage}%, 0, 0)`);
325332
}
326333

327334
/** Retrieves the percentage of thumb from the moved distance. */
328335
private _getThumbPercentage(distance: number) {
329336
let percentage = (distance / this._thumbBarWidth) * 100;
330337

331338
// When the toggle was initially checked, then we have to start the drag at the end.
332-
if (this._checked) {
339+
if (this._previousChecked) {
333340
percentage += 100;
334341
}
335342

0 commit comments

Comments
 (0)