Skip to content

Commit b5c5424

Browse files
committed
fix(): checkbox should only emit a change event when input element emitted one.
1 parent 93f67d4 commit b5c5424

File tree

2 files changed

+59
-37
lines changed

2 files changed

+59
-37
lines changed

src/components/checkbox/checkbox.spec.ts

+52-21
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import {
55
inject,
66
async,
77
fakeAsync,
8-
flushMicrotasks,
9-
tick
8+
flushMicrotasks
109
} from '@angular/core/testing';
1110
import {
1211
FORM_DIRECTIVES,
@@ -19,7 +18,6 @@ import {TestComponentBuilder, ComponentFixture} from '@angular/compiler/testing'
1918
import {Component, DebugElement} from '@angular/core';
2019
import {By} from '@angular/platform-browser';
2120
import {MdCheckbox, MdCheckboxChange} from './checkbox';
22-
import {PromiseCompleter} from '@angular2-material/core/async/promise-completer';
2321

2422

2523

@@ -215,21 +213,48 @@ describe('MdCheckbox', () => {
215213
expect(testComponent.onCheckboxClick).toHaveBeenCalledTimes(1);
216214
});
217215

218-
it('should emit a change event when the `checked` value changes', () => {
219-
// TODO(jelbourn): this *should* work with async(), but fixture.whenStable currently doesn't
220-
// know to look at pending macro tasks.
221-
// See https://github.com/angular/angular/issues/8389
222-
// As a short-term solution, use a promise (which jasmine knows how to understand).
223-
let promiseCompleter = new PromiseCompleter();
224-
checkboxInstance.change.subscribe(() => {
225-
promiseCompleter.resolve();
216+
it('should trigger the change event properly', 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);
226234
});
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');
227242

228243
testComponent.isChecked = true;
229244
fixture.detectChanges();
230245

231-
return promiseCompleter.promise;
232-
});
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.
251+
fixture.whenStable().then(() => {
252+
// The change event shouldn't fire, because the value change was not caused
253+
// by any interaction.
254+
expect(testComponent.onCheckboxChange).not.toHaveBeenCalled();
255+
});
256+
257+
}));
233258

234259
describe('state transition css classes', () => {
235260
it('should transition unchecked -> checked -> unchecked', () => {
@@ -308,17 +333,21 @@ describe('MdCheckbox', () => {
308333
});
309334
}));
310335

311-
it('should call the change event on first change after initialization', fakeAsync(() => {
312-
fixture.detectChanges();
313-
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);
314340

315-
checkboxInstance.checked = true;
316341
fixture.detectChanges();
342+
expect(changeSpy).not.toHaveBeenCalled();
317343

318-
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();
319348

320-
expect(testComponent.lastEvent.checked).toBe(true);
321-
}));
349+
expect(changeSpy).toHaveBeenCalledTimes(1);
350+
});
322351

323352
it('should not emit a DOM event to the change output', async(() => {
324353
fixture.detectChanges();
@@ -496,7 +525,8 @@ describe('MdCheckbox', () => {
496525
[indeterminate]="isIndeterminate"
497526
[disabled]="isDisabled"
498527
(change)="changeCount = changeCount + 1"
499-
(click)="onCheckboxClick($event)">
528+
(click)="onCheckboxClick($event)"
529+
(change)="onCheckboxChange($event)">
500530
Simple checkbox
501531
</md-checkbox>
502532
</div>`
@@ -511,6 +541,7 @@ class SingleCheckbox {
511541
lastKeydownEvent: Event = null;
512542

513543
onCheckboxClick(event: Event) {}
544+
onCheckboxChange(event: MdCheckboxChange) {}
514545
}
515546

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

src/components/checkbox/checkbox.ts

+7-16
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import {
88
Provider,
99
Renderer,
1010
ViewEncapsulation,
11-
forwardRef,
12-
AfterContentInit
11+
forwardRef
1312
} from '@angular/core';
1413
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
1514

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

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

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

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

276262
if (!this.disabled) {
277263
this.toggle();
264+
265+
// Emit our custom change event if the native input emitted one.
266+
// It is important to only emit it, if the native input triggered one, because
267+
// we don't want to trigger a change event, when the `checked` variable changes for example.
268+
this._emitChangeEvent();
278269
}
279270
}
280271

0 commit comments

Comments
 (0)