Skip to content

Commit a96cc0a

Browse files
devversionmmalerba
authored andcommitted
fix(slide-toggle): invalid model change event (#4140)" (#4218)
While initially looking into #4124, there were a few more issues inside of the slide-toggle. * ngModelChange event is dispatched at initialization. * Checked state isn't synchronized when state changes through drag. New state after dragging got overwritten by click event on label. * Removes unnecessary logic inside of `change` listener. Change event doesn't fire if underlying checkbox is disabled. Fixes #4124.
1 parent ac3e21a commit a96cc0a

File tree

2 files changed

+34
-36
lines changed

2 files changed

+34
-36
lines changed

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

+14-11
Original file line numberDiff line numberDiff line change
@@ -295,18 +295,23 @@ describe('MdSlideToggle', () => {
295295
expect(slideToggleModel.pristine).toBe(true);
296296
expect(slideToggleModel.touched).toBe(false);
297297

298-
// After changing the value programmatically, the control should
299-
// become dirty (not pristine), but remain untouched.
298+
// After changing the value from the view, the control should
299+
// become dirty (not pristine), but remain untouched if focus is still there.
300300
slideToggle.checked = true;
301+
302+
// Dispatch a change event on the input element to fake a user interaction that triggered
303+
// the state change.
304+
dispatchFakeEvent(inputElement, 'change');
305+
301306
fixture.detectChanges();
302307

303308
expect(slideToggleModel.valid).toBe(true);
304309
expect(slideToggleModel.pristine).toBe(false);
305310
expect(slideToggleModel.touched).toBe(false);
306311

307-
// After a user interaction occurs (such as a click), the control should remain dirty and
308-
// now also be touched.
309-
labelElement.click();
312+
// Once the input element looses focus, the control should remain dirty but should
313+
// also turn touched.
314+
dispatchFakeEvent(inputElement, 'blur');
310315
fixture.detectChanges();
311316

312317
expect(slideToggleModel.valid).toBe(true);
@@ -324,13 +329,13 @@ describe('MdSlideToggle', () => {
324329
expect(slideToggleModel.touched).toBe(false);
325330
expect(slideToggleElement.classList).toContain('mat-checked');
326331

327-
// After a user interaction occurs (such as a click), the control should remain dirty and
328-
// now also be touched.
329-
inputElement.click();
332+
// Once the input element looses focus, the control should remain dirty but should
333+
// also turn touched.
334+
dispatchFakeEvent(inputElement, 'blur');
330335
fixture.detectChanges();
331336

332337
expect(slideToggleModel.touched).toBe(true);
333-
expect(slideToggleElement.classList).not.toContain('mat-checked');
338+
expect(slideToggleElement.classList).toContain('mat-checked');
334339
});
335340

336341
// TODO(kara): update when core/testing adds fix
@@ -434,15 +439,13 @@ describe('MdSlideToggle', () => {
434439
}));
435440

436441
it('should prevent the form from submit when being required', () => {
437-
438442
if ('reportValidity' in inputElement === false) {
439443
// If the browser does not report the validity then the tests will break.
440444
// e.g Safari 8 on Mobile.
441445
return;
442446
}
443447

444448
testComponent.isRequired = true;
445-
446449
fixture.detectChanges();
447450

448451
buttonElement.click();

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

+20-25
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ export class MdSlideToggle extends _MdSlideToggleMixinBase
8484

8585
// A unique id for the slide-toggle. By default the id is auto-generated.
8686
private _uniqueId = `md-slide-toggle-${++nextId}`;
87-
private _checked: boolean = false;
8887
private _slideRenderer: SlideToggleRenderer;
8988
private _required: boolean = false;
9089
private _disableRipple: boolean = false;
@@ -104,6 +103,9 @@ export class MdSlideToggle extends _MdSlideToggleMixinBase
104103
/** Whether the label should appear after or before the slide-toggle. Defaults to 'after' */
105104
@Input() labelPosition: 'before' | 'after' = 'after';
106105

106+
/** Whether the slide-toggle element is checked or not */
107+
@Input() checked: boolean = false;
108+
107109
/** Used to set the aria-label attribute on the underlying input element. */
108110
@Input('aria-label') ariaLabel: string | null = null;
109111

@@ -153,29 +155,30 @@ export class MdSlideToggle extends _MdSlideToggleMixinBase
153155
}
154156

155157
/**
156-
* The onChangeEvent method will be also called on click.
157-
* This is because everything for the slide-toggle is wrapped inside of a label,
158-
* which triggers a onChange event on click.
158+
* This function will called if the underlying input changed its value through user interaction.
159159
*/
160160
_onChangeEvent(event: Event) {
161161
// We always have to stop propagation on the change event.
162162
// Otherwise the change event, from the input element, will bubble up and
163163
// emit its event object to the component's `change` output.
164164
event.stopPropagation();
165165

166-
// Once a drag is currently in progress, we do not want to toggle the slide-toggle on a click.
167-
if (!this.disabled && !this._slideRenderer.dragging) {
168-
this.toggle();
166+
// Sync the value from the underlying input element with the slide-toggle component.
167+
this.checked = this._inputElement.nativeElement.checked;
169168

170-
// Emit our custom change event if the native input emitted one.
171-
// It is important to only emit it, if the native input triggered one, because
172-
// we don't want to trigger a change event, when the `checked` variable changes for example.
173-
this._emitChangeEvent();
174-
}
169+
// Emit our custom change event if the native input emitted one.
170+
// It is important to only emit it, if the native input triggered one, because we don't want
171+
// to trigger a change event, when the `checked` variable changes programmatically.
172+
this._emitChangeEvent();
175173
}
176174

177175
_onInputClick(event: Event) {
178-
this.onTouched();
176+
// In some situations the user will release the mouse on the label element. The label element
177+
// redirects the click to the underlying input element and will result in a value change.
178+
// Prevent the default behavior if dragging, because the value will be set after drag.
179+
if (this._slideRenderer.dragging) {
180+
event.preventDefault();
181+
}
179182

180183
// We have to stop propagation for click events on the visual hidden input element.
181184
// By default, when a user clicks on a label element, a generated click event will be
@@ -213,16 +216,6 @@ export class MdSlideToggle extends _MdSlideToggleMixinBase
213216
this._focusOriginMonitor.focusVia(this._inputElement.nativeElement, 'keyboard');
214217
}
215218

216-
/** Whether the slide-toggle is checked. */
217-
@Input()
218-
get checked() { return !!this._checked; }
219-
set checked(value) {
220-
if (this.checked !== !!value) {
221-
this._checked = value;
222-
this.onChange(this._checked);
223-
}
224-
}
225-
226219
/** Toggles the checked state of the slide-toggle. */
227220
toggle() {
228221
this.checked = !this.checked;
@@ -244,15 +237,17 @@ export class MdSlideToggle extends _MdSlideToggleMixinBase
244237
}
245238
}
246239

247-
/** Emits the change event to the `change` output EventEmitter */
240+
/**
241+
* Emits a change event on the `change` output. Also notifies the FormControl about the change.
242+
*/
248243
private _emitChangeEvent() {
249244
let event = new MdSlideToggleChange();
250245
event.source = this;
251246
event.checked = this.checked;
252247
this.change.emit(event);
248+
this.onChange(this.checked);
253249
}
254250

255-
256251
_onDragStart() {
257252
if (!this.disabled) {
258253
this._slideRenderer.startThumbDrag(this.checked);

0 commit comments

Comments
 (0)