From 5105a89f23739599ee5b91a05a01199ce9397363 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 29 Dec 2016 16:10:41 +0200 Subject: [PATCH 1/4] feat(select): emit change event Adds an event to `md-select` that is emitted when the selected option has changed. Fixes #2248. --- src/demo-app/select/select-demo.html | 4 ++-- src/demo-app/select/select-demo.ts | 4 ++++ src/lib/select/select.spec.ts | 25 ++++++++++++++++++++++++- src/lib/select/select.ts | 22 ++++++++++++++++++---- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/demo-app/select/select-demo.html b/src/demo-app/select/select-demo.html index aae5985aae20..c618af0a6268 100644 --- a/src/demo-app/select/select-demo.html +++ b/src/demo-app/select/select-demo.html @@ -3,7 +3,7 @@
- + {{ food.viewValue }}

Value: {{ foodControl.value }}

@@ -19,7 +19,7 @@ + (change)="changeListener($event)" #drinkControl="ngModel"> {{ drink.viewValue }} diff --git a/src/demo-app/select/select-demo.ts b/src/demo-app/select/select-demo.ts index ee2c542537c2..588a3f51d1f6 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, @@ -36,4 +37,7 @@ export class SelectDemo { this.foodControl.enabled ? this.foodControl.disable() : this.foodControl.enable(); } + changeListener(event: MdSelectChange) { + console.log(event); + }; } diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index f5e3f6b9f40b..9bf4889483b7 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -237,6 +237,27 @@ describe('MdSelect', () => { expect(fixture.componentInstance.select.selected).not.toBeDefined(); }); + + 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); + }); }); describe('forms integration', () => { @@ -1146,7 +1167,8 @@ describe('MdSelect', () => { selector: 'basic-select', template: `
- + {{ food.viewValue }} @@ -1169,6 +1191,7 @@ class BasicSelect { isRequired: boolean; heightAbove = 0; heightBelow = 0; + changeListener = jasmine.createSpy('MdSelect change listener'); @ViewChild(MdSelect) select: MdSelect; @ViewChildren(MdOption) options: QueryList; diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index e2de6c548829..dcbc1ad92374 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', @@ -214,10 +219,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, @@ -429,8 +437,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); }); @@ -444,6 +452,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(' '); From 8d18e6e6bf47be877b1554816d26950283d591c9 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Fri, 30 Dec 2016 14:33:09 +0200 Subject: [PATCH 2/4] Separate the change event example from the ones that use the form directives. --- src/demo-app/select/select-demo.html | 15 ++++-- src/demo-app/select/select-demo.ts | 11 ++-- src/lib/select/select.spec.ts | 76 ++++++++++++++++++++-------- 3 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/demo-app/select/select-demo.html b/src/demo-app/select/select-demo.html index c618af0a6268..f1e903f4824b 100644 --- a/src/demo-app/select/select-demo.html +++ b/src/demo-app/select/select-demo.html @@ -3,7 +3,7 @@
- + {{ food.viewValue }}

Value: {{ foodControl.value }}

@@ -16,10 +16,9 @@
- + #drinkControl="ngModel"> {{ drink.viewValue }} @@ -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 588a3f51d1f6..4196edbc633d 100644 --- a/src/demo-app/select/select-demo.ts +++ b/src/demo-app/select/select-demo.ts @@ -13,6 +13,7 @@ export class SelectDemo { isDisabled = false; showSelect = false; currentDrink: string; + latestChangeEvent: MdSelectChange; foodControl = new FormControl('pizza-1'); foods = [ @@ -33,11 +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(); } - - changeListener(event: MdSelectChange) { - console.log(event); - }; } diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index 9bf4889483b7..5bc9a077c684 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; @@ -237,27 +237,6 @@ describe('MdSelect', () => { expect(fixture.componentInstance.select.selected).not.toBeDefined(); }); - - 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); - }); }); describe('forms integration', () => { @@ -1161,6 +1140,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({ @@ -1257,7 +1268,28 @@ class NgIfSelect { @ViewChild(MdSelect) select: MdSelect; } +@Component({ + selector: 'select-with-change-element', + 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 From cd2fe22b8ebe545f9e37cdcd027e177e06e956dc Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 4 Jan 2017 20:26:05 +0100 Subject: [PATCH 3/4] Remove leftover event listener. --- src/lib/select/select.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index 5bc9a077c684..c8d9532e91fd 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -1178,8 +1178,7 @@ describe('MdSelect', () => { selector: 'basic-select', template: `
- + {{ food.viewValue }} @@ -1269,7 +1268,7 @@ class NgIfSelect { } @Component({ - selector: 'select-with-change-element', + selector: 'select-with-change-event', template: ` {{ food }} From bf3fcf0e98305fb2392b862508c6458e650d6e32 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 4 Jan 2017 20:38:06 +0100 Subject: [PATCH 4/4] More leftovers. --- src/lib/select/select.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index c8d9532e91fd..5a075462ac1b 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -1201,7 +1201,6 @@ class BasicSelect { isRequired: boolean; heightAbove = 0; heightBelow = 0; - changeListener = jasmine.createSpy('MdSelect change listener'); @ViewChild(MdSelect) select: MdSelect; @ViewChildren(MdOption) options: QueryList;