diff --git a/src/components/slide-toggle/slide-toggle.spec.ts b/src/components/slide-toggle/slide-toggle.spec.ts index cc1673e3c968..1b3b45086e08 100644 --- a/src/components/slide-toggle/slide-toggle.spec.ts +++ b/src/components/slide-toggle/slide-toggle.spec.ts @@ -33,11 +33,19 @@ describe('MdSlideToggle', () => { beforeEach(async(() => { builder.createAsync(SlideToggleTestApp).then(f => { fixture = f; + + testComponent = fixture.debugElement.componentInstance; + + // Enable jasmine spies on event functions, which may trigger at initialization + // of the slide-toggle component. + spyOn(fixture.debugElement.componentInstance, 'onSlideChange').and.callThrough(); + spyOn(fixture.debugElement.componentInstance, 'onSlideClick').and.callThrough(); + + // Initialize the slide-toggle component, by triggering the first change detection cycle. fixture.detectChanges(); let slideToggleDebug = fixture.debugElement.query(By.css('md-slide-toggle')); - testComponent = fixture.debugElement.componentInstance; slideToggle = slideToggleDebug.componentInstance; slideToggleElement = slideToggleDebug.nativeElement; slideToggleControl = slideToggleDebug.injector.get(NgControl); @@ -103,8 +111,6 @@ describe('MdSlideToggle', () => { // Since we're using a label element and a visual hidden input, this behavior can led // to an issue, where the click events on the slide-toggle are getting executed twice. - spyOn(testComponent, 'onSlideClick'); - expect(slideToggle.checked).toBe(false); expect(slideToggleElement.classList).not.toContain('md-checked'); @@ -117,6 +123,42 @@ describe('MdSlideToggle', () => { expect(testComponent.onSlideClick).toHaveBeenCalledTimes(1); }); + it('should not trigger the change event multiple times', async(() => { + expect(inputElement.checked).toBe(false); + expect(slideToggleElement.classList).not.toContain('md-checked'); + + testComponent.slideChecked = true; + fixture.detectChanges(); + + expect(inputElement.checked).toBe(true); + expect(slideToggleElement.classList).toContain('md-checked'); + + // Wait for the fixture to become stable, because the EventEmitter for the change event, + // will only fire after the zone async change detection has finished. + fixture.whenStable().then(() => { + expect(testComponent.onSlideChange).toHaveBeenCalledTimes(1); + }); + + })); + + it('should not trigger the change event on initialization', async(() => { + expect(inputElement.checked).toBe(false); + expect(slideToggleElement.classList).not.toContain('md-checked'); + + testComponent.slideChecked = true; + fixture.detectChanges(); + + expect(inputElement.checked).toBe(true); + expect(slideToggleElement.classList).toContain('md-checked'); + + // Wait for the fixture to become stable, because the EventEmitter for the change event, + // will only fire after the zone async change detection has finished. + fixture.whenStable().then(() => { + expect(testComponent.onSlideChange).toHaveBeenCalledTimes(1); + }); + + })); + it('should add a suffix to the inputs id', () => { testComponent.slideId = 'myId'; fixture.detectChanges(); @@ -269,6 +311,56 @@ describe('MdSlideToggle', () => { }); + describe('custom template', () => { + + let testComponent: SlideToggleTestApp; + let slideToggle: MdSlideToggle; + let slideToggleElement: HTMLElement; + let labelElement: HTMLLabelElement; + let inputElement: HTMLInputElement; + + it('should not trigger the change event on initialization', async(() => { + builder + .overrideTemplate(SlideToggleTestApp, ` + + `) + .createAsync(SlideToggleTestApp) + .then(fixture => { + // Initialize the variables for our test. + initializeTest(fixture); + + // Enable jasmine spies on event functions, which may trigger at initialization + // of the slide-toggle component. + spyOn(fixture.debugElement.componentInstance, 'onSlideChange').and.callThrough(); + + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(testComponent.onSlideChange).not.toHaveBeenCalled(); + }); + }); + })); + + /** + * Initializes the suites variables, to allow developers to easily access the several variables + * without loading / querying them always again. + * @param fixture Custom fixture, which contains the slide-toggle component. + */ + function initializeTest(fixture: ComponentFixture) { + testComponent = fixture.debugElement.componentInstance; + + // Initialize the slide-toggle component, by triggering the first change detection cycle. + fixture.detectChanges(); + + let slideToggleDebug = fixture.debugElement.query(By.css('md-slide-toggle')); + + slideToggle = slideToggleDebug.componentInstance; + slideToggleElement = slideToggleDebug.nativeElement; + inputElement = fixture.debugElement.query(By.css('input')).nativeElement; + labelElement = fixture.debugElement.query(By.css('label')).nativeElement; + } + }); + }); /** @@ -288,7 +380,7 @@ function dispatchFocusChangeEvent(eventName: string, element: HTMLElement): void Test Slide Toggle @@ -307,4 +399,7 @@ class SlideToggleTestApp { lastEvent: MdSlideToggleChange; onSlideClick(event: Event) {} + onSlideChange(event: MdSlideToggleChange) { + this.lastEvent = event; + } } diff --git a/src/components/slide-toggle/slide-toggle.ts b/src/components/slide-toggle/slide-toggle.ts index 7c1a8c881f36..b8027c5a6bc1 100644 --- a/src/components/slide-toggle/slide-toggle.ts +++ b/src/components/slide-toggle/slide-toggle.ts @@ -6,7 +6,8 @@ import { ChangeDetectionStrategy, Input, Output, - EventEmitter + EventEmitter, + AfterContentInit } from '@angular/core'; import { ControlValueAccessor, @@ -45,7 +46,7 @@ let nextId = 0; providers: [MD_SLIDE_TOGGLE_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class MdSlideToggle implements ControlValueAccessor { +export class MdSlideToggle implements AfterContentInit, ControlValueAccessor { private onChange = (_: any) => {}; private onTouched = () => {}; @@ -56,6 +57,7 @@ export class MdSlideToggle implements ControlValueAccessor { private _color: string; private _hasFocus: boolean = false; private _isMousedown: boolean = false; + private _isInitialized: boolean = false; @Input() @BooleanFieldValue() disabled: boolean = false; @Input() name: string = null; @@ -74,6 +76,14 @@ export class MdSlideToggle implements ControlValueAccessor { private _renderer: Renderer) { } + /** TODO: internal */ + ngAfterContentInit() { + // Mark this component as initialized in AfterContentInit because the initial checked value can + // possibly be set by NgModel or the checked attribute. This would cause the change event to + // be emitted, before the component is actually initialized. + this._isInitialized = true; + } + /** * The onChangeEvent method will be also called on click. * This is because everything for the slide-toggle is wrapped inside of a label, @@ -163,7 +173,12 @@ export class MdSlideToggle implements ControlValueAccessor { if (this.checked !== !!value) { this._checked = value; this.onChange(this._checked); - this._emitChangeEvent(); + + // Only fire a change event if the `slide-toggle` is completely initialized and + // all attributes / inputs are properly loaded. + if (this._isInitialized) { + this._emitChangeEvent(); + } } }