Skip to content

Commit e5bd15c

Browse files
crisbetotinayuangao
authored andcommitted
feat(select): emit change event (#2458)
* feat(select): emit change event Adds an event to `md-select` that is emitted when the selected option has changed. Fixes #2248. * Separate the change event example from the ones that use the form directives. * Remove leftover event listener. * More leftovers.
1 parent 52aa715 commit e5bd15c

File tree

4 files changed

+90
-7
lines changed

4 files changed

+90
-7
lines changed

src/demo-app/select/select-demo.html

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
</md-card>
1717
</div>
1818

19-
2019
<md-card>
2120
<md-select placeholder="Drink" [(ngModel)]="currentDrink" [required]="isRequired" [disabled]="isDisabled"
2221
#drinkControl="ngModel">
@@ -34,5 +33,15 @@
3433
<button md-button (click)="drinkControl.reset()">RESET</button>
3534
</md-card>
3635

36+
<div *ngIf="showSelect">
37+
<md-card>
38+
<md-select placeholder="Starter Pokemon" (change)="latestChangeEvent = $event">
39+
<md-option *ngFor="let starter of pokemon" [value]="starter.value"> {{ starter.viewValue }} </md-option>
40+
</md-select>
41+
42+
<p> Change event value: {{ latestChangeEvent?.value }} </p>
43+
</md-card>
44+
</div>
45+
3746
</div>
3847
<div style="height: 500px">This div is for testing scrolled selects.</div>

src/demo-app/select/select-demo.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {Component} from '@angular/core';
22
import {FormControl} from '@angular/forms';
3+
import {MdSelectChange} from '@angular/material';
34

45
@Component({
56
moduleId: module.id,
@@ -12,6 +13,7 @@ export class SelectDemo {
1213
isDisabled = false;
1314
showSelect = false;
1415
currentDrink: string;
16+
latestChangeEvent: MdSelectChange;
1517
foodControl = new FormControl('pizza-1');
1618

1719
foods = [
@@ -32,8 +34,13 @@ export class SelectDemo {
3234
{value: 'milk-8', viewValue: 'Milk'},
3335
];
3436

37+
pokemon = [
38+
{value: 'bulbasaur-0', viewValue: 'Bulbasaur'},
39+
{value: 'charizard-1', viewValue: 'Charizard'},
40+
{value: 'squirtle-2', viewValue: 'Squirtle'}
41+
];
42+
3543
toggleDisabled() {
3644
this.foodControl.enabled ? this.foodControl.disable() : this.foodControl.enable();
3745
}
38-
3946
}

src/lib/select/select.spec.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('MdSelect', () => {
1616
beforeEach(async(() => {
1717
TestBed.configureTestingModule({
1818
imports: [MdSelectModule.forRoot(), ReactiveFormsModule, FormsModule],
19-
declarations: [BasicSelect, NgModelSelect, ManySelects, NgIfSelect],
19+
declarations: [BasicSelect, NgModelSelect, ManySelects, NgIfSelect, SelectWithChangeEvent],
2020
providers: [
2121
{provide: OverlayContainer, useFactory: () => {
2222
overlayContainerElement = document.createElement('div') as HTMLElement;
@@ -1155,6 +1155,38 @@ describe('MdSelect', () => {
11551155

11561156
});
11571157

1158+
describe('change event', () => {
1159+
let fixture: ComponentFixture<SelectWithChangeEvent>;
1160+
let trigger: HTMLElement;
1161+
1162+
beforeEach(() => {
1163+
fixture = TestBed.createComponent(SelectWithChangeEvent);
1164+
fixture.detectChanges();
1165+
1166+
trigger = fixture.debugElement.query(By.css('.md-select-trigger')).nativeElement;
1167+
});
1168+
1169+
it('should emit an event when the selected option has changed', () => {
1170+
trigger.click();
1171+
fixture.detectChanges();
1172+
1173+
(overlayContainerElement.querySelector('md-option') as HTMLElement).click();
1174+
1175+
expect(fixture.componentInstance.changeListener).toHaveBeenCalled();
1176+
});
1177+
1178+
it('should not emit multiple change events for the same option', () => {
1179+
trigger.click();
1180+
fixture.detectChanges();
1181+
1182+
let option = overlayContainerElement.querySelector('md-option') as HTMLElement;
1183+
1184+
option.click();
1185+
option.click();
1186+
1187+
expect(fixture.componentInstance.changeListener).toHaveBeenCalledTimes(1);
1188+
});
1189+
});
11581190
});
11591191

11601192
@Component({
@@ -1249,7 +1281,28 @@ class NgIfSelect {
12491281
@ViewChild(MdSelect) select: MdSelect;
12501282
}
12511283

1284+
@Component({
1285+
selector: 'select-with-change-event',
1286+
template: `
1287+
<md-select (change)="changeListener($event)">
1288+
<md-option *ngFor="let food of foods" [value]="food">{{ food }}</md-option>
1289+
</md-select>
1290+
`
1291+
})
1292+
class SelectWithChangeEvent {
1293+
foods: string[] = [
1294+
'steak-0',
1295+
'pizza-1',
1296+
'tacos-2',
1297+
'sandwich-3',
1298+
'chips-4',
1299+
'eggs-5',
1300+
'pasta-6',
1301+
'sushi-7'
1302+
];
12521303

1304+
changeListener = jasmine.createSpy('MdSelect change listener');
1305+
}
12531306

12541307
/**
12551308
* TODO: Move this to core testing utility until Angular has event faking

src/lib/select/select.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ export const SELECT_PANEL_PADDING_Y = 16;
6464
*/
6565
export const SELECT_PANEL_VIEWPORT_PADDING = 8;
6666

67+
/** Change event object that is emitted when the select value has changed. */
68+
export class MdSelectChange {
69+
constructor(public source: MdSelect, public value: any) { }
70+
}
71+
6772
@Component({
6873
moduleId: module.id,
6974
selector: 'md-select, mat-select',
@@ -217,10 +222,13 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
217222
set required(value: any) { this._required = coerceBooleanProperty(value); }
218223

219224
/** Event emitted when the select has been opened. */
220-
@Output() onOpen = new EventEmitter();
225+
@Output() onOpen: EventEmitter<void> = new EventEmitter<void>();
221226

222227
/** Event emitted when the select has been closed. */
223-
@Output() onClose = new EventEmitter();
228+
@Output() onClose: EventEmitter<void> = new EventEmitter<void>();
229+
230+
/** Event emitted when the selected value has been changed by the user. */
231+
@Output() change: EventEmitter<MdSelectChange> = new EventEmitter<MdSelectChange>();
224232

225233
constructor(private _element: ElementRef, private _renderer: Renderer,
226234
private _viewportRuler: ViewportRuler, @Optional() private _dir: Dir,
@@ -434,8 +442,8 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
434442
private _listenToOptions(): void {
435443
this.options.forEach((option: MdOption) => {
436444
const sub = option.onSelect.subscribe((isUserInput: boolean) => {
437-
if (isUserInput) {
438-
this._onChange(option.value);
445+
if (isUserInput && this._selected !== option) {
446+
this._emitChangeEvent(option);
439447
}
440448
this._onSelect(option);
441449
});
@@ -449,6 +457,12 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
449457
this._subscriptions = [];
450458
}
451459

460+
/** Emits an event when the user selects an option. */
461+
private _emitChangeEvent(option: MdOption): void {
462+
this._onChange(option.value);
463+
this.change.emit(new MdSelectChange(this, option.value));
464+
}
465+
452466
/** Records option IDs to pass to the aria-owns property. */
453467
private _setOptionIds() {
454468
this._optionIds = this.options.map(option => option.id).join(' ');

0 commit comments

Comments
 (0)