Skip to content

Commit d52e6e0

Browse files
devversionkara
authored andcommitted
fix(checkbox, slide-toggle): only emit change event if native input emitted one. (#820)
* fix(): slide-toggle, checkbox should only emit events if native input emitted one. * Fix nested checkbox demo and update test name Fixes #575. * Remove whitespace
1 parent 758b851 commit d52e6e0

File tree

6 files changed

+92
-47
lines changed

6 files changed

+92
-47
lines changed

src/components/checkbox/checkbox.spec.ts

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import {
33
inject,
44
async,
55
fakeAsync,
6-
flushMicrotasks,
7-
tick
6+
flushMicrotasks
87
} from '@angular/core/testing';
98
import {
109
FORM_DIRECTIVES,
@@ -214,13 +213,47 @@ describe('MdCheckbox', () => {
214213
expect(testComponent.onCheckboxClick).toHaveBeenCalledTimes(1);
215214
});
216215

217-
it('should emit a change event when the `checked` value changes', async(() => {
216+
it('should trigger a change event when the native input does', async(() => {
217+
spyOn(testComponent, 'onCheckboxChange');
218+
219+
expect(inputElement.checked).toBe(false);
220+
expect(checkboxNativeElement.classList).not.toContain('md-checkbox-checked');
221+
222+
labelElement.click();
223+
fixture.detectChanges();
224+
225+
expect(inputElement.checked).toBe(true);
226+
expect(checkboxNativeElement.classList).toContain('md-checkbox-checked');
227+
228+
// Wait for the fixture to become stable, because the EventEmitter for the change event,
229+
// will only fire after the zone async change detection has finished.
230+
fixture.whenStable().then(() => {
231+
// The change event shouldn't fire, because the value change was not caused
232+
// by any interaction.
233+
expect(testComponent.onCheckboxChange).toHaveBeenCalledTimes(1);
234+
});
235+
}));
236+
237+
it('should not trigger the change event by changing the native value', async(() => {
238+
spyOn(testComponent, 'onCheckboxChange');
239+
240+
expect(inputElement.checked).toBe(false);
241+
expect(checkboxNativeElement.classList).not.toContain('md-checkbox-checked');
242+
218243
testComponent.isChecked = true;
219244
fixture.detectChanges();
220245

246+
expect(inputElement.checked).toBe(true);
247+
expect(checkboxNativeElement.classList).toContain('md-checkbox-checked');
248+
249+
// Wait for the fixture to become stable, because the EventEmitter for the change event,
250+
// will only fire after the zone async change detection has finished.
221251
fixture.whenStable().then(() => {
222-
expect(fixture.componentInstance.changeCount).toBe(1);
252+
// The change event shouldn't fire, because the value change was not caused
253+
// by any interaction.
254+
expect(testComponent.onCheckboxChange).not.toHaveBeenCalled();
223255
});
256+
224257
}));
225258

226259
describe('state transition css classes', () => {
@@ -300,17 +333,21 @@ describe('MdCheckbox', () => {
300333
});
301334
}));
302335

303-
it('should call the change event on first change after initialization', fakeAsync(() => {
304-
fixture.detectChanges();
305-
expect(testComponent.lastEvent).toBeUndefined();
336+
it('should emit the event to the change observable', () => {
337+
let changeSpy = jasmine.createSpy('onChangeObservable');
338+
339+
checkboxInstance.change.subscribe(changeSpy);
306340

307-
checkboxInstance.checked = true;
308341
fixture.detectChanges();
342+
expect(changeSpy).not.toHaveBeenCalled();
309343

310-
tick();
344+
// When changing the native `checked` property the checkbox will not fire a change event,
345+
// because the element is not focused and it's not the native behavior of the input element.
346+
labelElement.click();
347+
fixture.detectChanges();
311348

312-
expect(testComponent.lastEvent.checked).toBe(true);
313-
}));
349+
expect(changeSpy).toHaveBeenCalledTimes(1);
350+
});
314351

315352
it('should not emit a DOM event to the change output', async(() => {
316353
fixture.detectChanges();
@@ -488,7 +525,8 @@ describe('MdCheckbox', () => {
488525
[indeterminate]="isIndeterminate"
489526
[disabled]="isDisabled"
490527
(change)="changeCount = changeCount + 1"
491-
(click)="onCheckboxClick($event)">
528+
(click)="onCheckboxClick($event)"
529+
(change)="onCheckboxChange($event)">
492530
Simple checkbox
493531
</md-checkbox>
494532
</div>`
@@ -504,6 +542,7 @@ class SingleCheckbox {
504542
changeCount: number = 0;
505543

506544
onCheckboxClick(event: Event) {}
545+
onCheckboxChange(event: MdCheckboxChange) {}
507546
}
508547

509548
/** Simple component for testing an MdCheckbox with ngModel. */

src/components/checkbox/checkbox.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import {
77
Output,
88
Renderer,
99
ViewEncapsulation,
10-
forwardRef,
11-
AfterContentInit
10+
forwardRef
1211
} from '@angular/core';
1312
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
1413

@@ -71,7 +70,7 @@ export class MdCheckboxChange {
7170
encapsulation: ViewEncapsulation.None,
7271
changeDetection: ChangeDetectionStrategy.OnPush
7372
})
74-
export class MdCheckbox implements AfterContentInit, ControlValueAccessor {
73+
export class MdCheckbox implements ControlValueAccessor {
7574
/**
7675
* Attached to the aria-label attribute of the host element. In most cases, arial-labelledby will
7776
* take precedence so this may be omitted.
@@ -115,9 +114,6 @@ export class MdCheckbox implements AfterContentInit, ControlValueAccessor {
115114
/** Called when the checkbox is blurred. Needed to properly implement ControlValueAccessor. */
116115
onTouched: () => any = () => {};
117116

118-
/** Whether the `checked` state has been set to its initial value. */
119-
private _isInitialized: boolean = false;
120-
121117
private _currentAnimationClass: string = '';
122118

123119
private _currentCheckState: TransitionCheckState = TransitionCheckState.Init;
@@ -146,19 +142,9 @@ export class MdCheckbox implements AfterContentInit, ControlValueAccessor {
146142
this._checked = checked;
147143
this._transitionCheckState(
148144
this._checked ? TransitionCheckState.Checked : TransitionCheckState.Unchecked);
149-
150-
// Only fire a change event if this isn't the first time the checked property is ever set.
151-
if (this._isInitialized) {
152-
this._emitChangeEvent();
153-
}
154145
}
155146
}
156147

157-
/** TODO: internal */
158-
ngAfterContentInit() {
159-
this._isInitialized = true;
160-
}
161-
162148
/**
163149
* Whether the checkbox is indeterminate. This is also known as "mixed" mode and can be used to
164150
* represent a checkbox with three states, e.g. a checkbox that represents a nested list of
@@ -267,6 +253,11 @@ export class MdCheckbox implements AfterContentInit, ControlValueAccessor {
267253

268254
if (!this.disabled) {
269255
this.toggle();
256+
257+
// Emit our custom change event if the native input emitted one.
258+
// It is important to only emit it, if the native input triggered one, because
259+
// we don't want to trigger a change event, when the `checked` variable changes for example.
260+
this._emitChangeEvent();
270261
}
271262
}
272263

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,11 @@ describe('MdSlideToggle', () => {
127127
expect(testComponent.onSlideClick).toHaveBeenCalledTimes(1);
128128
});
129129

130-
it('should not trigger the change event multiple times', async(() => {
130+
it('should trigger the change event properly', async(() => {
131131
expect(inputElement.checked).toBe(false);
132132
expect(slideToggleElement.classList).not.toContain('md-checked');
133133

134-
testComponent.slideChecked = true;
134+
labelElement.click();
135135
fixture.detectChanges();
136136

137137
expect(inputElement.checked).toBe(true);
@@ -140,11 +140,33 @@ describe('MdSlideToggle', () => {
140140
// Wait for the fixture to become stable, because the EventEmitter for the change event,
141141
// will only fire after the zone async change detection has finished.
142142
fixture.whenStable().then(() => {
143+
// The change event shouldn't fire, because the value change was not caused
144+
// by any interaction.
143145
expect(testComponent.onSlideChange).toHaveBeenCalledTimes(1);
144146
});
145147

146148
}));
147149

150+
it('should not trigger the change event by changing the native value', async(() => {
151+
expect(inputElement.checked).toBe(false);
152+
expect(slideToggleElement.classList).not.toContain('md-checked');
153+
154+
testComponent.slideChecked = true;
155+
fixture.detectChanges();
156+
157+
expect(inputElement.checked).toBe(true);
158+
expect(slideToggleElement.classList).toContain('md-checked');
159+
160+
// Wait for the fixture to become stable, because the EventEmitter for the change event,
161+
// will only fire after the zone async change detection has finished.
162+
fixture.whenStable().then(() => {
163+
// The change event shouldn't fire, because the value change was not caused
164+
// by any interaction.
165+
expect(testComponent.onSlideChange).not.toHaveBeenCalled();
166+
});
167+
168+
}));
169+
148170
it('should not trigger the change event on initialization', async(() => {
149171
expect(inputElement.checked).toBe(false);
150172
expect(slideToggleElement.classList).not.toContain('md-checked');
@@ -158,7 +180,8 @@ describe('MdSlideToggle', () => {
158180
// Wait for the fixture to become stable, because the EventEmitter for the change event,
159181
// will only fire after the zone async change detection has finished.
160182
fixture.whenStable().then(() => {
161-
expect(testComponent.onSlideChange).toHaveBeenCalledTimes(1);
183+
// The change event shouldn't fire, because the native input element is not focused.
184+
expect(testComponent.onSlideChange).not.toHaveBeenCalled();
162185
});
163186

164187
}));

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

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
5858
private _color: string;
5959
private _hasFocus: boolean = false;
6060
private _isMousedown: boolean = false;
61-
private _isInitialized: boolean = false;
6261
private _slideRenderer: SlideToggleRenderer = null;
6362

6463
@Input() @BooleanFieldValue() disabled: boolean = false;
@@ -79,11 +78,6 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
7978
/** TODO: internal */
8079
ngAfterContentInit() {
8180
this._slideRenderer = new SlideToggleRenderer(this._elementRef);
82-
83-
// Mark this component as initialized in AfterContentInit because the initial checked value can
84-
// possibly be set by NgModel or the checked attribute. This would cause the change event to
85-
// be emitted, before the component is actually initialized.
86-
this._isInitialized = true;
8781
}
8882

8983
/**
@@ -100,6 +94,11 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
10094
// Once a drag is currently in progress, we do not want to toggle the slide-toggle on a click.
10195
if (!this.disabled && !this._slideRenderer.isDragging()) {
10296
this.toggle();
97+
98+
// Emit our custom change event if the native input emitted one.
99+
// It is important to only emit it, if the native input triggered one, because
100+
// we don't want to trigger a change event, when the `checked` variable changes for example.
101+
this._emitChangeEvent();
103102
}
104103
}
105104

@@ -171,12 +170,6 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
171170
if (this.checked !== !!value) {
172171
this._checked = value;
173172
this.onChange(this._checked);
174-
175-
// Only fire a change event if the `slide-toggle` is completely initialized and
176-
// all attributes / inputs are properly loaded.
177-
if (this._isInitialized) {
178-
this._emitChangeEvent();
179-
}
180173
}
181174
}
182175

src/demo-app/checkbox/checkbox-demo.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,5 @@ <h1>md-checkbox: Basic Example</h1>
4040
</div>
4141
</div>
4242

43-
<h1>Application Example: Nested Checklist</h1>
44-
<h2><em>Caution: WIP!</em></h2>
45-
<md-checkbox-demo-nested-checklist></md-checkbox-demo-nested-checklist>
43+
<h1>Nested Checklist</h1>
44+
<md-checkbox-demo-nested-checklist></md-checkbox-demo-nested-checklist>

src/demo-app/checkbox/nested-checklist.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ <h2>Tasks</h2>
44
<md-checkbox [(ngModel)]="task.completed"
55
[checked]="allComplete(task)"
66
[indeterminate]="someComplete(task.subtasks)"
7-
(change)="setAllCompleted(task.subtasks, $event)">
7+
(change)="setAllCompleted(task.subtasks, $event.checked)">
88
<h3>{{task.name}}</h3>
99
</md-checkbox>
1010
<ul>

0 commit comments

Comments
 (0)