diff --git a/src/demo-app/select/select-demo.html b/src/demo-app/select/select-demo.html index aae5985aae20..f1e903f4824b 100644 --- a/src/demo-app/select/select-demo.html +++ b/src/demo-app/select/select-demo.html @@ -16,7 +16,6 @@ - @@ -34,5 +33,15 @@ +
+ + + {{ starter.viewValue }} + + +

Change event value: {{ latestChangeEvent?.value }}

+
+
+
This div is for testing scrolled selects.
diff --git a/src/demo-app/select/select-demo.ts b/src/demo-app/select/select-demo.ts index ee2c542537c2..4196edbc633d 100644 --- a/src/demo-app/select/select-demo.ts +++ b/src/demo-app/select/select-demo.ts @@ -1,5 +1,6 @@ import {Component} from '@angular/core'; import {FormControl} from '@angular/forms'; +import {MdSelectChange} from '@angular/material'; @Component({ moduleId: module.id, @@ -12,6 +13,7 @@ export class SelectDemo { isDisabled = false; showSelect = false; currentDrink: string; + latestChangeEvent: MdSelectChange; foodControl = new FormControl('pizza-1'); foods = [ @@ -32,8 +34,13 @@ export class SelectDemo { {value: 'milk-8', viewValue: 'Milk'}, ]; + pokemon = [ + {value: 'bulbasaur-0', viewValue: 'Bulbasaur'}, + {value: 'charizard-1', viewValue: 'Charizard'}, + {value: 'squirtle-2', viewValue: 'Squirtle'} + ]; + toggleDisabled() { this.foodControl.enabled ? this.foodControl.disable() : this.foodControl.enable(); } - } diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index 12739be32528..8603a2719b4e 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -16,7 +16,7 @@ describe('MdSelect', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [MdSelectModule.forRoot(), ReactiveFormsModule, FormsModule], - declarations: [BasicSelect, NgModelSelect, ManySelects, NgIfSelect], + declarations: [BasicSelect, NgModelSelect, ManySelects, NgIfSelect, SelectWithChangeEvent], providers: [ {provide: OverlayContainer, useFactory: () => { overlayContainerElement = document.createElement('div') as HTMLElement; @@ -1155,6 +1155,38 @@ describe('MdSelect', () => { }); + describe('change event', () => { + let fixture: ComponentFixture; + let trigger: HTMLElement; + + beforeEach(() => { + fixture = TestBed.createComponent(SelectWithChangeEvent); + fixture.detectChanges(); + + trigger = fixture.debugElement.query(By.css('.md-select-trigger')).nativeElement; + }); + + it('should emit an event when the selected option has changed', () => { + trigger.click(); + fixture.detectChanges(); + + (overlayContainerElement.querySelector('md-option') as HTMLElement).click(); + + expect(fixture.componentInstance.changeListener).toHaveBeenCalled(); + }); + + it('should not emit multiple change events for the same option', () => { + trigger.click(); + fixture.detectChanges(); + + let option = overlayContainerElement.querySelector('md-option') as HTMLElement; + + option.click(); + option.click(); + + expect(fixture.componentInstance.changeListener).toHaveBeenCalledTimes(1); + }); + }); }); @Component({ @@ -1249,7 +1281,28 @@ class NgIfSelect { @ViewChild(MdSelect) select: MdSelect; } +@Component({ + selector: 'select-with-change-event', + template: ` + + {{ food }} + + ` +}) +class SelectWithChangeEvent { + foods: string[] = [ + 'steak-0', + 'pizza-1', + 'tacos-2', + 'sandwich-3', + 'chips-4', + 'eggs-5', + 'pasta-6', + 'sushi-7' + ]; + changeListener = jasmine.createSpy('MdSelect change listener'); +} /** * TODO: Move this to core testing utility until Angular has event faking diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index ef218bafb779..cb7d2e7a3555 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -64,6 +64,11 @@ export const SELECT_PANEL_PADDING_Y = 16; */ export const SELECT_PANEL_VIEWPORT_PADDING = 8; +/** Change event object that is emitted when the select value has changed. */ +export class MdSelectChange { + constructor(public source: MdSelect, public value: any) { } +} + @Component({ moduleId: module.id, selector: 'md-select, mat-select', @@ -217,10 +222,13 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr set required(value: any) { this._required = coerceBooleanProperty(value); } /** Event emitted when the select has been opened. */ - @Output() onOpen = new EventEmitter(); + @Output() onOpen: EventEmitter = new EventEmitter(); /** Event emitted when the select has been closed. */ - @Output() onClose = new EventEmitter(); + @Output() onClose: EventEmitter = new EventEmitter(); + + /** Event emitted when the selected value has been changed by the user. */ + @Output() change: EventEmitter = new EventEmitter(); constructor(private _element: ElementRef, private _renderer: Renderer, private _viewportRuler: ViewportRuler, @Optional() private _dir: Dir, @@ -434,8 +442,8 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr private _listenToOptions(): void { this.options.forEach((option: MdOption) => { const sub = option.onSelect.subscribe((isUserInput: boolean) => { - if (isUserInput) { - this._onChange(option.value); + if (isUserInput && this._selected !== option) { + this._emitChangeEvent(option); } this._onSelect(option); }); @@ -449,6 +457,12 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr this._subscriptions = []; } + /** Emits an event when the user selects an option. */ + private _emitChangeEvent(option: MdOption): void { + this._onChange(option.value); + this.change.emit(new MdSelectChange(this, option.value)); + } + /** Records option IDs to pass to the aria-owns property. */ private _setOptionIds() { this._optionIds = this.options.map(option => option.id).join(' ');