Skip to content

Commit

Permalink
feat(autocomplete): emit event when an option is selected
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
crisbeto committed Apr 20, 2017
1 parent 8d0cd04 commit 21e4ae2
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
this._clearPreviousSelectedOption(event.source);
this._setTriggerValue(event.source.value);
this._onChange(event.source.value);
this.autocomplete._emitSelectEvent(event.source);
}

this.closePanel();
Expand Down
51 changes: 49 additions & 2 deletions src/lib/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {ENTER, DOWN_ARROW, SPACE, UP_ARROW, HOME, END} from '../core/keyboard/ke
import {MdOption} from '../core/option/option';
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler';
import {MdAutocomplete} from './autocomplete';
import {MdAutocomplete, MdAutocompleteSelect} from './autocomplete';
import {MdInputContainer} from '../input/input-container';
import {Observable} from 'rxjs/Observable';
import {dispatchFakeEvent} from '../core/testing/dispatch-events';
Expand All @@ -47,7 +47,8 @@ describe('MdAutocomplete', () => {
AutocompleteWithoutForms,
NgIfAutocomplete,
AutocompleteWithNgModel,
AutocompleteWithOnPushDelay
AutocompleteWithOnPushDelay,
AutocompleteWithSelectEvent
],
providers: [
{provide: OverlayContainer, useFactory: () => {
Expand Down Expand Up @@ -1146,6 +1147,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<HTMLElement>;
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({
Expand Down Expand Up @@ -1319,6 +1343,29 @@ class AutocompleteWithOnPushDelay implements OnInit {
}


@Component({
template: `
<md-input-container>
<input mdInput placeholder="State" [mdAutocomplete]="auto" [(ngModel)]="selectedState">
</md-input-container>
<md-autocomplete #auto="mdAutocomplete" (select)="select($event)">
<md-option *ngFor="let state of states" [value]="state">
<span>{{ state }}</span>
</md-option>
</md-autocomplete>
`
})
class AutocompleteWithSelectEvent {
selectedState: string;
states = ['New York', 'Washington', 'Oregon'];
select = jasmine.createSpy('select callback');

@ViewChild(MdAutocompleteTrigger) trigger: MdAutocompleteTrigger;
@ViewChild(MdAutocomplete) autocomplete: MdAutocomplete;
}


/** This is a mock keyboard event to test keyboard events in the autocomplete. */
class MockKeyboardEvent {
constructor(public keyCode: number) {}
Expand Down
21 changes: 19 additions & 2 deletions src/lib/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,24 @@ import {
ViewChild,
ViewEncapsulation,
ChangeDetectorRef,
Output,
EventEmitter,
} from '@angular/core';
import {MdOption} from '../core';
import {MdOption} from '../core/option/option';
import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-manager';


/**
* 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({
Expand Down Expand Up @@ -50,6 +58,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;

/** Event that is emitted whenever an option from the list is selected. */
@Output() select: EventEmitter<MdAutocompleteSelect> = new EventEmitter<MdAutocompleteSelect>();

/** Unique ID to be used by autocomplete trigger's "aria-owns" property. */
id: string = `md-autocomplete-${_uniqueAutocompleteIdCounter++}`;

Expand All @@ -70,13 +81,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 its position (used to set y-offset). */
_getClassList() {
return {
Expand Down

0 comments on commit 21e4ae2

Please sign in to comment.