Skip to content

Commit

Permalink
fix(slide-toggle): input not updated after drag (#3067)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
devversion authored and andrewseguin committed Feb 17, 2017
1 parent bc9d25b commit 5cdeb75
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 35 deletions.
27 changes: 26 additions & 1 deletion src/lib/slide-toggle/slide-toggle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ describe('MdSlideToggle', () => {
let slideToggleElement: HTMLElement;
let slideToggleControl: NgControl;
let slideThumbContainer: HTMLElement;
let inputElement: HTMLInputElement;

beforeEach(async(() => {
fixture = TestBed.createComponent(SlideToggleTestApp);
Expand All @@ -453,6 +454,8 @@ describe('MdSlideToggle', () => {
slideToggleElement = slideToggleDebug.nativeElement;
slideToggleControl = slideToggleDebug.injector.get(NgControl);
slideThumbContainer = thumbContainerDebug.nativeElement;

inputElement = slideToggleElement.querySelector('input');
}));

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

it('should not drag when disbaled', fakeAsync(() => {
it('should not drag when disabled', fakeAsync(() => {
slideToggle.disabled = true;

expect(slideToggle.checked).toBe(false);
Expand Down Expand Up @@ -538,6 +541,28 @@ describe('MdSlideToggle', () => {
expect(testComponent.lastEvent.checked).toBe(true);
}));

it('should update the checked property of the input', fakeAsync(() => {
expect(inputElement.checked).toBe(false);

gestureConfig.emitEventForElement('slidestart', slideThumbContainer);

expect(slideThumbContainer.classList).toContain('mat-dragging');

gestureConfig.emitEventForElement('slide', slideThumbContainer, {
deltaX: 200 // Arbitrary, large delta that will be clamped to the end of the slide-toggle.
});

gestureConfig.emitEventForElement('slideend', slideThumbContainer);
fixture.detectChanges();

expect(inputElement.checked).toBe(true);

// Flush the timeout for the slide ending.
tick();

expect(slideThumbContainer.classList).not.toContain('mat-dragging');
}));

});

describe('with a FormControl', () => {
Expand Down
75 changes: 41 additions & 34 deletions src/lib/slide-toggle/slide-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
event.stopPropagation();

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

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

_onDrag(event: HammerInput) {
if (this._slideRenderer.isDragging()) {
if (this._slideRenderer.dragging) {
this._slideRenderer.updateThumbPosition(event.deltaX);
}
}

_onDragEnd() {
if (!this._slideRenderer.isDragging()) {
return;
}

// Notice that we have to stop outside of the current event handler,
// because otherwise the click event will be fired and will reset the new checked variable.
setTimeout(() => {
this.checked = this._slideRenderer.stopThumbDrag();
if (this._slideRenderer.dragging) {
this.checked = this._slideRenderer.dragPercentage > 50;
this._emitChangeEvent();
}, 0);

// The drag should be stopped outside of the current event handler, because otherwise the
// click event will be fired before and will revert the drag change.
setTimeout(() => this._slideRenderer.stopThumbDrag());
}
}

}
Expand All @@ -280,56 +278,65 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
*/
class SlideToggleRenderer {

/** Reference to the thumb HTMLElement. */
private _thumbEl: HTMLElement;

/** Reference to the thumb bar HTMLElement. */
private _thumbBarEl: HTMLElement;

/** Width of the thumb bar of the slide-toggle. */
private _thumbBarWidth: number;
private _checked: boolean;
private _percentage: number;

/** Previous checked state before drag started. */
private _previousChecked: boolean;

/** Percentage of the thumb while dragging. */
dragPercentage: number;

/** Whether the thumb is currently being dragged. */
dragging: boolean = false;

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

/** Whether the slide-toggle is currently dragging. */
isDragging(): boolean {
return !!this._thumbBarWidth;
}


/** Initializes the drag of the slide-toggle. */
startThumbDrag(checked: boolean) {
if (!this.isDragging()) {
this._thumbBarWidth = this._thumbBarEl.clientWidth - this._thumbEl.clientWidth;
this._checked = checked;
this._thumbEl.classList.add('mat-dragging');
}
if (this.dragging) { return; }

this._thumbBarWidth = this._thumbBarEl.clientWidth - this._thumbEl.clientWidth;
this._thumbEl.classList.add('mat-dragging');

this._previousChecked = checked;
this.dragging = true;
}

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

applyCssTransform(this._thumbEl, '');
this.dragging = false;
this._thumbEl.classList.remove('mat-dragging');

return this._percentage > 50;
}
// Reset the transform because the component will take care of the thumb position after drag.
applyCssTransform(this._thumbEl, '');

return this.dragPercentage > 50;
}

/** Updates the thumb containers position from the specified distance. */
updateThumbPosition(distance: number) {
this._percentage = this._getThumbPercentage(distance);
applyCssTransform(this._thumbEl, `translate3d(${this._percentage}%, 0, 0)`);
this.dragPercentage = this._getThumbPercentage(distance);
applyCssTransform(this._thumbEl, `translate3d(${this.dragPercentage}%, 0, 0)`);
}

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

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

Expand Down

0 comments on commit 5cdeb75

Please sign in to comment.