Skip to content

Commit 7aa65f6

Browse files
committed
fix(select): handle optionSelectionChanges being accessed early
Along the same lines #8802, `mat-select` will throw if the `optionSelectionChanges` is accessed before the options are initialized. These changes add a fallback that will replace the observable once the options become available.
1 parent b6f2484 commit 7aa65f6

File tree

2 files changed

+61
-3
lines changed

2 files changed

+61
-3
lines changed

src/lib/select/select.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@ import {
4343
FloatLabelType,
4444
MAT_LABEL_GLOBAL_OPTIONS,
4545
MatOption,
46+
MatOptionSelectionChange,
4647
} from '@angular/material/core';
4748
import {MatFormFieldModule} from '@angular/material/form-field';
4849
import {By} from '@angular/platform-browser';
4950
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
5051
import {map} from 'rxjs/operators/map';
5152
import {Subject} from 'rxjs/Subject';
53+
import {Subscription} from 'rxjs/Subscription';
5254
import {MatSelectModule} from './index';
5355
import {MatSelect} from './select';
5456
import {
@@ -1001,6 +1003,54 @@ describe('MatSelect', () => {
10011003
it('should not throw if triggerValue accessed with no selected value', fakeAsync(() => {
10021004
expect(() => fixture.componentInstance.select.triggerValue).not.toThrow();
10031005
}));
1006+
1007+
it('should emit to `optionSelectionChanges` when an option is selected', fakeAsync(() => {
1008+
trigger.click();
1009+
fixture.detectChanges();
1010+
flush();
1011+
1012+
const spy = jasmine.createSpy('option selection spy');
1013+
const subscription = fixture.componentInstance.select.optionSelectionChanges.subscribe(spy);
1014+
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
1015+
option.click();
1016+
fixture.detectChanges();
1017+
flush();
1018+
1019+
expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange));
1020+
1021+
subscription.unsubscribe();
1022+
}));
1023+
1024+
it('should handle accessing `optionSelectionChanges` before the options are initialized',
1025+
fakeAsync(() => {
1026+
fixture.destroy();
1027+
fixture = TestBed.createComponent(BasicSelect);
1028+
1029+
let spy = jasmine.createSpy('option selection spy');
1030+
let subscription: Subscription;
1031+
1032+
expect(fixture.componentInstance.select.options).toBeFalsy();
1033+
expect(() => {
1034+
subscription = fixture.componentInstance.select.optionSelectionChanges.subscribe(spy);
1035+
}).not.toThrow();
1036+
1037+
fixture.detectChanges();
1038+
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
1039+
1040+
trigger.click();
1041+
fixture.detectChanges();
1042+
flush();
1043+
1044+
const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
1045+
option.click();
1046+
fixture.detectChanges();
1047+
flush();
1048+
1049+
expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange));
1050+
1051+
subscription!.unsubscribe();
1052+
}));
1053+
10041054
});
10051055

10061056
describe('forms integration', () => {

src/lib/select/select.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
import {filter} from 'rxjs/operators/filter';
2222
import {take} from 'rxjs/operators/take';
2323
import {map} from 'rxjs/operators/map';
24+
import {switchMap} from 'rxjs/operators/switchMap';
2425
import {startWith} from 'rxjs/operators/startWith';
2526
import {takeUntil} from 'rxjs/operators/takeUntil';
2627
import {
@@ -74,6 +75,7 @@ import {MatFormField, MatFormFieldControl} from '@angular/material/form-field';
7475
import {Observable} from 'rxjs/Observable';
7576
import {merge} from 'rxjs/observable/merge';
7677
import {Subject} from 'rxjs/Subject';
78+
import {defer} from 'rxjs/observable/defer';
7779
import {fadeInContent, transformPanel} from './select-animations';
7880
import {
7981
getMatSelectDynamicMultipleError,
@@ -403,9 +405,15 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
403405
private _id: string;
404406

405407
/** Combined stream of all of the child options' change events. */
406-
get optionSelectionChanges(): Observable<MatOptionSelectionChange> {
407-
return merge(...this.options.map(option => option.onSelectionChange));
408-
}
408+
optionSelectionChanges: Observable<MatOptionSelectionChange> = defer(() => {
409+
if (this.options) {
410+
return merge(...this.options.map(option => option.onSelectionChange));
411+
}
412+
413+
return this._ngZone.onStable
414+
.asObservable()
415+
.pipe(take(1), switchMap(() => this.optionSelectionChanges));
416+
});
409417

410418
/** Event emitted when the select has been opened. */
411419
@Output() openedChange: EventEmitter<boolean> = new EventEmitter<boolean>();

0 commit comments

Comments
 (0)