Skip to content

Commit 42a9593

Browse files
committed
fix(autocomplete): handle optionSelections being accessed early
Currently the `MatAutocompleteTrigger` will throw if `optionSelections` is accessed before `ngAfterViewInit`. These changes add a fallback stream that will be replaced once everything is initialized. Fixes #4616.
1 parent d85c44b commit 42a9593

File tree

2 files changed

+52
-4
lines changed

2 files changed

+52
-4
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {MatFormField} from '@angular/material/form-field';
4141
import {DOCUMENT} from '@angular/common';
4242
import {Observable} from 'rxjs/Observable';
4343
import {Subject} from 'rxjs/Subject';
44+
import {defer} from 'rxjs/observable/defer';
4445
import {fromEvent} from 'rxjs/observable/fromEvent';
4546
import {merge} from 'rxjs/observable/merge';
4647
import {of as observableOf} from 'rxjs/observable/of';
@@ -202,9 +203,17 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
202203
}
203204

204205
/** Stream of autocomplete option selections. */
205-
get optionSelections(): Observable<MatOptionSelectionChange> {
206-
return merge(...this.autocomplete.options.map(option => option.onSelectionChange));
207-
}
206+
optionSelections: Observable<MatOptionSelectionChange> = defer(() => {
207+
if (this.autocomplete && this.autocomplete.options) {
208+
return merge(...this.autocomplete.options.map(option => option.onSelectionChange));
209+
}
210+
211+
// If there are any subscribers before `ngAfterViewInit`, the `autocomplete` will be undefined.
212+
// Return a stream that we'll replace with the real one once everything is in place.
213+
return this._zone.onStable
214+
.asObservable()
215+
.pipe(take(1), switchMap(() => this.optionSelections));
216+
});
208217

209218
/** The currently active option, coerced to MatOption type. */
210219
get activeOption(): MatOption | null {

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
flush,
3232
} from '@angular/core/testing';
3333
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
34-
import {MatOption} from '@angular/material/core';
34+
import {MatOption, MatOptionSelectionChange} from '@angular/material/core';
3535
import {MatFormField, MatFormFieldModule} from '@angular/material/form-field';
3636
import {By} from '@angular/platform-browser';
3737
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
@@ -1309,6 +1309,45 @@ describe('MatAutocomplete', () => {
13091309
expect(componentOptions[0].deselect).toHaveBeenCalled();
13101310
componentOptions.slice(1).forEach(option => expect(option.deselect).not.toHaveBeenCalled());
13111311
}));
1312+
1313+
it('should emit an event when an option is selected', fakeAsync(() => {
1314+
const spy = jasmine.createSpy('option selection spy');
1315+
const subscription = fixture.componentInstance.trigger.optionSelections.subscribe(spy);
1316+
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
1317+
option.click();
1318+
fixture.detectChanges();
1319+
1320+
expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange));
1321+
subscription.unsubscribe();
1322+
}));
1323+
1324+
it('should handle `optionSelections` being accessed too early', fakeAsync(() => {
1325+
overlayContainer.ngOnDestroy();
1326+
fixture.destroy();
1327+
fixture = TestBed.createComponent(SimpleAutocomplete);
1328+
1329+
let spy = jasmine.createSpy('option selection spy');
1330+
let subscription: Subscription;
1331+
1332+
expect(fixture.componentInstance.trigger.autocomplete).toBeFalsy();
1333+
expect(() => {
1334+
subscription = fixture.componentInstance.trigger.optionSelections.subscribe(spy);
1335+
}).not.toThrow();
1336+
1337+
fixture.detectChanges();
1338+
fixture.componentInstance.trigger.openPanel();
1339+
fixture.detectChanges();
1340+
zone.simulateZoneExit();
1341+
1342+
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
1343+
1344+
option.click();
1345+
fixture.detectChanges();
1346+
zone.simulateZoneExit();
1347+
1348+
expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange));
1349+
}));
1350+
13121351
});
13131352

13141353
describe('panel closing', () => {

0 commit comments

Comments
 (0)