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.
This commit is contained in:
Paul Gschwendtner
2017-02-17 01:57:22 +01:00
committed by Andrew Seguin
parent bc9d25b2bb
commit 5cdeb755e0
2 changed files with 67 additions and 35 deletions
+26 -1
View File
@@ -438,6 +438,7 @@ describe('MdSlideToggle', () => {
let slideToggleElement: HTMLElement;
let slideToggleControl: NgControl;
let slideThumbContainer: HTMLElement;
let inputElement: HTMLInputElement;
beforeEach(async(() => {
fixture = TestBed.createComponent(SlideToggleTestApp);
@@ -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(() => {
@@ -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);
@@ -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', () => {
+41 -34
View File
@@ -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.
@@ -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());
}
}
}
@@ -280,48 +278,57 @@ 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. */
@@ -329,7 +336,7 @@ class SlideToggleRenderer {
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;
}