From 58d161a1140744d4181241c8a264afa0dbd82a9e Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 20 Apr 2017 23:09:16 +0200 Subject: [PATCH 1/2] feat(autocomplete): emit event when an option is selected Emits the `select` event when an option in the autocomplete is selected. **Note:** I went with passing the selected option from the trigger to the panel, instead of listening to the `onSelectionChange` inside the panel, because it involves keeping track of less subscriptions and not having to re-construct them when the list of options changes. Fixes #4094. Fixes #3645. --- src/lib/autocomplete/autocomplete-trigger.ts | 1 + src/lib/autocomplete/autocomplete.spec.ts | 48 +++++++++++++++++++- src/lib/autocomplete/autocomplete.ts | 21 ++++++++- 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index c0d0281f1a48..ade59b533b37 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -427,6 +427,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { this._setTriggerValue(event.source.value); this._onChange(event.source.value); this._element.nativeElement.focus(); + this.autocomplete._emitSelectEvent(event.source); } this.closePanel(); diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 3678b583caa5..6d5d47278798 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -21,6 +21,7 @@ import { MdAutocomplete, MdAutocompleteModule, MdAutocompleteTrigger, + MdAutocompleteSelect, } from './index'; import {MdInputModule} from '../input/index'; import {Subscription} from 'rxjs/Subscription'; @@ -57,7 +58,8 @@ describe('MdAutocomplete', () => { AutocompleteWithNativeInput, AutocompleteWithoutPanel, AutocompleteWithFormsAndNonfloatingPlaceholder, - AutocompleteWithGroups + AutocompleteWithGroups, + AutocompleteWithSelectEvent, ], providers: [ {provide: OverlayContainer, useFactory: () => { @@ -1548,6 +1550,29 @@ describe('MdAutocomplete', () => { expect(panel.classList).toContain(visibleClass, `Expected panel to be visible.`); }); })); + + it('should call emit an event when an option is selected', fakeAsync(() => { + let fixture = TestBed.createComponent(AutocompleteWithSelectEvent); + + fixture.detectChanges(); + fixture.componentInstance.trigger.openPanel(); + tick(); + fixture.detectChanges(); + + let options = overlayContainerElement.querySelectorAll('md-option') as NodeListOf; + let spy = fixture.componentInstance.select; + + options[1].click(); + tick(); + fixture.detectChanges(); + + expect(spy).toHaveBeenCalledTimes(1); + + let event = spy.calls.mostRecent().args[0] as MdAutocompleteSelect; + + expect(event.source).toBe(fixture.componentInstance.autocomplete); + expect(event.option.value).toBe('Washington'); + })); }); @Component({ @@ -1826,3 +1851,24 @@ class AutocompleteWithGroups { } ]; } + +@Component({ + template: ` + + + + + + {{ state }} + + + ` +}) +class AutocompleteWithSelectEvent { + selectedState: string; + states = ['New York', 'Washington', 'Oregon']; + select = jasmine.createSpy('select callback'); + + @ViewChild(MdAutocompleteTrigger) trigger: MdAutocompleteTrigger; + @ViewChild(MdAutocomplete) autocomplete: MdAutocomplete; +} diff --git a/src/lib/autocomplete/autocomplete.ts b/src/lib/autocomplete/autocomplete.ts index a7253fde8b13..60da405aebe4 100644 --- a/src/lib/autocomplete/autocomplete.ts +++ b/src/lib/autocomplete/autocomplete.ts @@ -18,16 +18,26 @@ import { ViewEncapsulation, ChangeDetectorRef, ChangeDetectionStrategy, + EventEmitter, + Output, } from '@angular/core'; import {MdOption, MdOptgroup} from '../core'; import {ActiveDescendantKeyManager} from '@angular/cdk/a11y'; + /** * Autocomplete IDs need to be unique across components, so this counter exists outside of * the component definition. */ let _uniqueAutocompleteIdCounter = 0; +/** Event object that is emitted when an autocomplete option is selected */ +export class MdAutocompleteSelect { + constructor(public source: MdAutocomplete, public option: MdOption) { } +} + +export type AutocompletePositionY = 'above' | 'below'; + @Component({ moduleId: module.id, selector: 'md-autocomplete, mat-autocomplete', @@ -63,6 +73,9 @@ export class MdAutocomplete implements AfterContentInit { /** Function that maps an option's control value to its display value in the trigger. */ @Input() displayWith: ((value: any) => string) | null = null; + /** Event that is emitted whenever an option from the list is selected. */ + @Output() select: EventEmitter = new EventEmitter(); + /** Unique ID to be used by autocomplete trigger's "aria-owns" property. */ id: string = `md-autocomplete-${_uniqueAutocompleteIdCounter++}`; @@ -88,13 +101,19 @@ export class MdAutocomplete implements AfterContentInit { } /** Panel should hide itself when the option list is empty. */ - _setVisibility() { + _setVisibility(): void { Promise.resolve().then(() => { this.showPanel = !!this.options.length; this._changeDetectorRef.markForCheck(); }); } + /** Emits the `select` event. */ + _emitSelectEvent(option: MdOption): void { + const selectEvent = new MdAutocompleteSelect(this, option); + this.select.emit(selectEvent); + } + /** Sets a class on the panel based on whether it is visible. */ _getClassList() { return { From e60439ff7af13454e42b24831f93b12cb00e2b31 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sun, 20 Aug 2017 13:33:53 +0200 Subject: [PATCH 2/2] refactor: address feedback --- src/lib/autocomplete/autocomplete.spec.ts | 39 +++++++++++++++++++---- src/lib/autocomplete/autocomplete.ts | 10 +++--- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 6d5d47278798..01e940027f05 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -21,7 +21,7 @@ import { MdAutocomplete, MdAutocompleteModule, MdAutocompleteTrigger, - MdAutocompleteSelect, + MdAutocompleteSelectedEvent, } from './index'; import {MdInputModule} from '../input/index'; import {Subscription} from 'rxjs/Subscription'; @@ -1551,7 +1551,7 @@ describe('MdAutocomplete', () => { }); })); - it('should call emit an event when an option is selected', fakeAsync(() => { + it('should emit an event when an option is selected', fakeAsync(() => { let fixture = TestBed.createComponent(AutocompleteWithSelectEvent); fixture.detectChanges(); @@ -1560,7 +1560,7 @@ describe('MdAutocomplete', () => { fixture.detectChanges(); let options = overlayContainerElement.querySelectorAll('md-option') as NodeListOf; - let spy = fixture.componentInstance.select; + let spy = fixture.componentInstance.optionSelected; options[1].click(); tick(); @@ -1568,11 +1568,37 @@ describe('MdAutocomplete', () => { expect(spy).toHaveBeenCalledTimes(1); - let event = spy.calls.mostRecent().args[0] as MdAutocompleteSelect; + let event = spy.calls.mostRecent().args[0] as MdAutocompleteSelectedEvent; expect(event.source).toBe(fixture.componentInstance.autocomplete); expect(event.option.value).toBe('Washington'); })); + + it('should emit an event when a newly-added option is selected', fakeAsync(() => { + let fixture = TestBed.createComponent(AutocompleteWithSelectEvent); + + fixture.detectChanges(); + fixture.componentInstance.trigger.openPanel(); + tick(); + fixture.detectChanges(); + + fixture.componentInstance.states.push('Puerto Rico'); + fixture.detectChanges(); + + let options = overlayContainerElement.querySelectorAll('md-option') as NodeListOf; + let spy = fixture.componentInstance.optionSelected; + + options[3].click(); + tick(); + fixture.detectChanges(); + + expect(spy).toHaveBeenCalledTimes(1); + + let event = spy.calls.mostRecent().args[0] as MdAutocompleteSelectedEvent; + + expect(event.source).toBe(fixture.componentInstance.autocomplete); + expect(event.option.value).toBe('Puerto Rico'); + })); }); @Component({ @@ -1857,7 +1883,8 @@ class AutocompleteWithGroups { - + + {{ state }} @@ -1867,7 +1894,7 @@ class AutocompleteWithGroups { class AutocompleteWithSelectEvent { selectedState: string; states = ['New York', 'Washington', 'Oregon']; - select = jasmine.createSpy('select callback'); + optionSelected = jasmine.createSpy('optionSelected callback'); @ViewChild(MdAutocompleteTrigger) trigger: MdAutocompleteTrigger; @ViewChild(MdAutocomplete) autocomplete: MdAutocomplete; diff --git a/src/lib/autocomplete/autocomplete.ts b/src/lib/autocomplete/autocomplete.ts index 60da405aebe4..3126485ea6e5 100644 --- a/src/lib/autocomplete/autocomplete.ts +++ b/src/lib/autocomplete/autocomplete.ts @@ -32,11 +32,10 @@ import {ActiveDescendantKeyManager} from '@angular/cdk/a11y'; let _uniqueAutocompleteIdCounter = 0; /** Event object that is emitted when an autocomplete option is selected */ -export class MdAutocompleteSelect { +export class MdAutocompleteSelectedEvent { constructor(public source: MdAutocomplete, public option: MdOption) { } } -export type AutocompletePositionY = 'above' | 'below'; @Component({ moduleId: module.id, @@ -74,7 +73,8 @@ export class MdAutocomplete implements AfterContentInit { @Input() displayWith: ((value: any) => string) | null = null; /** Event that is emitted whenever an option from the list is selected. */ - @Output() select: EventEmitter = new EventEmitter(); + @Output() optionSelected: EventEmitter = + new EventEmitter(); /** Unique ID to be used by autocomplete trigger's "aria-owns" property. */ id: string = `md-autocomplete-${_uniqueAutocompleteIdCounter++}`; @@ -110,8 +110,8 @@ export class MdAutocomplete implements AfterContentInit { /** Emits the `select` event. */ _emitSelectEvent(option: MdOption): void { - const selectEvent = new MdAutocompleteSelect(this, option); - this.select.emit(selectEvent); + const event = new MdAutocompleteSelectedEvent(this, option); + this.optionSelected.emit(event); } /** Sets a class on the panel based on whether it is visible. */