diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 9ee22496d39c..bef825705c39 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -41,6 +41,7 @@ import {MatFormField} from '@angular/material/form-field'; import {DOCUMENT} from '@angular/common'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; +import {defer} from 'rxjs/observable/defer'; import {fromEvent} from 'rxjs/observable/fromEvent'; import {merge} from 'rxjs/observable/merge'; import {of as observableOf} from 'rxjs/observable/of'; @@ -202,9 +203,17 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { } /** Stream of autocomplete option selections. */ - get optionSelections(): Observable { - return merge(...this.autocomplete.options.map(option => option.onSelectionChange)); - } + optionSelections: Observable = defer(() => { + if (this.autocomplete && this.autocomplete.options) { + return merge(...this.autocomplete.options.map(option => option.onSelectionChange)); + } + + // If there are any subscribers before `ngAfterViewInit`, the `autocomplete` will be undefined. + // Return a stream that we'll replace with the real one once everything is in place. + return this._zone.onStable + .asObservable() + .pipe(take(1), switchMap(() => this.optionSelections)); + }); /** The currently active option, coerced to MatOption type. */ get activeOption(): MatOption | null { diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index a98d236344f7..7a256f3f7705 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -31,7 +31,7 @@ import { flush, } from '@angular/core/testing'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {MatOption} from '@angular/material/core'; +import {MatOption, MatOptionSelectionChange} from '@angular/material/core'; import {MatFormField, MatFormFieldModule} from '@angular/material/form-field'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; @@ -1309,6 +1309,45 @@ describe('MatAutocomplete', () => { expect(componentOptions[0].deselect).toHaveBeenCalled(); componentOptions.slice(1).forEach(option => expect(option.deselect).not.toHaveBeenCalled()); })); + + it('should emit an event when an option is selected', fakeAsync(() => { + const spy = jasmine.createSpy('option selection spy'); + const subscription = fixture.componentInstance.trigger.optionSelections.subscribe(spy); + const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; + option.click(); + fixture.detectChanges(); + + expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange)); + subscription.unsubscribe(); + })); + + it('should handle `optionSelections` being accessed too early', fakeAsync(() => { + overlayContainer.ngOnDestroy(); + fixture.destroy(); + fixture = TestBed.createComponent(SimpleAutocomplete); + + let spy = jasmine.createSpy('option selection spy'); + let subscription: Subscription; + + expect(fixture.componentInstance.trigger.autocomplete).toBeFalsy(); + expect(() => { + subscription = fixture.componentInstance.trigger.optionSelections.subscribe(spy); + }).not.toThrow(); + + fixture.detectChanges(); + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; + + option.click(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange)); + })); + }); describe('panel closing', () => {