Skip to content

Commit f21b2f4

Browse files
devversionjelbourn
authored andcommitted
fix(slide-toggle): stop change event firing upon init (#713)
* test(slide-toggle): add test to confirm that change event don't fires multiple times * Adds a test which confirms, that the slide-toggle isn't firing the (change) event multiple times. Closes #709 * update(): prevent change event to be fired at initialization * update(): remove internal annotation
1 parent b407278 commit f21b2f4

File tree

2 files changed

+117
-7
lines changed

2 files changed

+117
-7
lines changed

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

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,19 @@ describe('MdSlideToggle', () => {
3333
beforeEach(async(() => {
3434
builder.createAsync(SlideToggleTestApp).then(f => {
3535
fixture = f;
36+
37+
testComponent = fixture.debugElement.componentInstance;
38+
39+
// Enable jasmine spies on event functions, which may trigger at initialization
40+
// of the slide-toggle component.
41+
spyOn(fixture.debugElement.componentInstance, 'onSlideChange').and.callThrough();
42+
spyOn(fixture.debugElement.componentInstance, 'onSlideClick').and.callThrough();
43+
44+
// Initialize the slide-toggle component, by triggering the first change detection cycle.
3645
fixture.detectChanges();
3746

3847
let slideToggleDebug = fixture.debugElement.query(By.css('md-slide-toggle'));
3948

40-
testComponent = fixture.debugElement.componentInstance;
4149
slideToggle = slideToggleDebug.componentInstance;
4250
slideToggleElement = slideToggleDebug.nativeElement;
4351
slideToggleControl = slideToggleDebug.injector.get(NgControl);
@@ -103,8 +111,6 @@ describe('MdSlideToggle', () => {
103111
// Since we're using a label element and a visual hidden input, this behavior can led
104112
// to an issue, where the click events on the slide-toggle are getting executed twice.
105113

106-
spyOn(testComponent, 'onSlideClick');
107-
108114
expect(slideToggle.checked).toBe(false);
109115
expect(slideToggleElement.classList).not.toContain('md-checked');
110116

@@ -117,6 +123,42 @@ describe('MdSlideToggle', () => {
117123
expect(testComponent.onSlideClick).toHaveBeenCalledTimes(1);
118124
});
119125

126+
it('should not trigger the change event multiple times', async(() => {
127+
expect(inputElement.checked).toBe(false);
128+
expect(slideToggleElement.classList).not.toContain('md-checked');
129+
130+
testComponent.slideChecked = true;
131+
fixture.detectChanges();
132+
133+
expect(inputElement.checked).toBe(true);
134+
expect(slideToggleElement.classList).toContain('md-checked');
135+
136+
// Wait for the fixture to become stable, because the EventEmitter for the change event,
137+
// will only fire after the zone async change detection has finished.
138+
fixture.whenStable().then(() => {
139+
expect(testComponent.onSlideChange).toHaveBeenCalledTimes(1);
140+
});
141+
142+
}));
143+
144+
it('should not trigger the change event on initialization', async(() => {
145+
expect(inputElement.checked).toBe(false);
146+
expect(slideToggleElement.classList).not.toContain('md-checked');
147+
148+
testComponent.slideChecked = true;
149+
fixture.detectChanges();
150+
151+
expect(inputElement.checked).toBe(true);
152+
expect(slideToggleElement.classList).toContain('md-checked');
153+
154+
// Wait for the fixture to become stable, because the EventEmitter for the change event,
155+
// will only fire after the zone async change detection has finished.
156+
fixture.whenStable().then(() => {
157+
expect(testComponent.onSlideChange).toHaveBeenCalledTimes(1);
158+
});
159+
160+
}));
161+
120162
it('should add a suffix to the inputs id', () => {
121163
testComponent.slideId = 'myId';
122164
fixture.detectChanges();
@@ -269,6 +311,56 @@ describe('MdSlideToggle', () => {
269311

270312
});
271313

314+
describe('custom template', () => {
315+
316+
let testComponent: SlideToggleTestApp;
317+
let slideToggle: MdSlideToggle;
318+
let slideToggleElement: HTMLElement;
319+
let labelElement: HTMLLabelElement;
320+
let inputElement: HTMLInputElement;
321+
322+
it('should not trigger the change event on initialization', async(() => {
323+
builder
324+
.overrideTemplate(SlideToggleTestApp, `
325+
<md-slide-toggle checked="true" (change)="onSlideChange($event)"></md-slide-toggle>
326+
`)
327+
.createAsync(SlideToggleTestApp)
328+
.then(fixture => {
329+
// Initialize the variables for our test.
330+
initializeTest(fixture);
331+
332+
// Enable jasmine spies on event functions, which may trigger at initialization
333+
// of the slide-toggle component.
334+
spyOn(fixture.debugElement.componentInstance, 'onSlideChange').and.callThrough();
335+
336+
fixture.detectChanges();
337+
338+
fixture.whenStable().then(() => {
339+
expect(testComponent.onSlideChange).not.toHaveBeenCalled();
340+
});
341+
});
342+
}));
343+
344+
/**
345+
* Initializes the suites variables, to allow developers to easily access the several variables
346+
* without loading / querying them always again.
347+
* @param fixture Custom fixture, which contains the slide-toggle component.
348+
*/
349+
function initializeTest(fixture: ComponentFixture<any>) {
350+
testComponent = fixture.debugElement.componentInstance;
351+
352+
// Initialize the slide-toggle component, by triggering the first change detection cycle.
353+
fixture.detectChanges();
354+
355+
let slideToggleDebug = fixture.debugElement.query(By.css('md-slide-toggle'));
356+
357+
slideToggle = slideToggleDebug.componentInstance;
358+
slideToggleElement = slideToggleDebug.nativeElement;
359+
inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
360+
labelElement = fixture.debugElement.query(By.css('label')).nativeElement;
361+
}
362+
});
363+
272364
});
273365

274366
/**
@@ -288,7 +380,7 @@ function dispatchFocusChangeEvent(eventName: string, element: HTMLElement): void
288380
<md-slide-toggle [(ngModel)]="slideModel" [disabled]="isDisabled" [color]="slideColor"
289381
[id]="slideId" [checked]="slideChecked" [name]="slideName"
290382
[aria-label]="slideLabel" [ariaLabel]="slideLabel"
291-
[ariaLabelledby]="slideLabelledBy" (change)="lastEvent = $event"
383+
[ariaLabelledby]="slideLabelledBy" (change)="onSlideChange($event)"
292384
(click)="onSlideClick($event)">
293385
<span>Test Slide Toggle</span>
294386
</md-slide-toggle>
@@ -307,4 +399,7 @@ class SlideToggleTestApp {
307399
lastEvent: MdSlideToggleChange;
308400

309401
onSlideClick(event: Event) {}
402+
onSlideChange(event: MdSlideToggleChange) {
403+
this.lastEvent = event;
404+
}
310405
}

src/components/slide-toggle/slide-toggle.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
ChangeDetectionStrategy,
77
Input,
88
Output,
9-
EventEmitter
9+
EventEmitter,
10+
AfterContentInit
1011
} from '@angular/core';
1112
import {
1213
ControlValueAccessor,
@@ -45,7 +46,7 @@ let nextId = 0;
4546
providers: [MD_SLIDE_TOGGLE_VALUE_ACCESSOR],
4647
changeDetection: ChangeDetectionStrategy.OnPush
4748
})
48-
export class MdSlideToggle implements ControlValueAccessor {
49+
export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
4950

5051
private onChange = (_: any) => {};
5152
private onTouched = () => {};
@@ -56,6 +57,7 @@ export class MdSlideToggle implements ControlValueAccessor {
5657
private _color: string;
5758
private _hasFocus: boolean = false;
5859
private _isMousedown: boolean = false;
60+
private _isInitialized: boolean = false;
5961

6062
@Input() @BooleanFieldValue() disabled: boolean = false;
6163
@Input() name: string = null;
@@ -74,6 +76,14 @@ export class MdSlideToggle implements ControlValueAccessor {
7476
private _renderer: Renderer) {
7577
}
7678

79+
/** TODO: internal */
80+
ngAfterContentInit() {
81+
// Mark this component as initialized in AfterContentInit because the initial checked value can
82+
// possibly be set by NgModel or the checked attribute. This would cause the change event to
83+
// be emitted, before the component is actually initialized.
84+
this._isInitialized = true;
85+
}
86+
7787
/**
7888
* The onChangeEvent method will be also called on click.
7989
* This is because everything for the slide-toggle is wrapped inside of a label,
@@ -163,7 +173,12 @@ export class MdSlideToggle implements ControlValueAccessor {
163173
if (this.checked !== !!value) {
164174
this._checked = value;
165175
this.onChange(this._checked);
166-
this._emitChangeEvent();
176+
177+
// Only fire a change event if the `slide-toggle` is completely initialized and
178+
// all attributes / inputs are properly loaded.
179+
if (this._isInitialized) {
180+
this._emitChangeEvent();
181+
}
167182
}
168183
}
169184

0 commit comments

Comments
 (0)