From 97ebd762e51e90ce179db2cc727e92255718aa7b Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 13 Dec 2017 22:41:12 +0100 Subject: [PATCH] fix(select): handle `optionSelectionChanges` being accessed early (#8830) 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. --- src/lib/select/select.spec.ts | 50 +++++++++++++++++++++++++++++++++++ src/lib/select/select.ts | 14 +++++++--- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index 490ba33ac4fc..677b6034f65c 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -43,12 +43,14 @@ import { FloatLabelType, MAT_LABEL_GLOBAL_OPTIONS, MatOption, + MatOptionSelectionChange, } from '@angular/material/core'; import {MatFormFieldModule} from '@angular/material/form-field'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {map} from 'rxjs/operators/map'; import {Subject} from 'rxjs/Subject'; +import {Subscription} from 'rxjs/Subscription'; import {MatSelectModule} from './index'; import {MatSelect} from './select'; import { @@ -1001,6 +1003,54 @@ describe('MatSelect', () => { it('should not throw if triggerValue accessed with no selected value', fakeAsync(() => { expect(() => fixture.componentInstance.select.triggerValue).not.toThrow(); })); + + it('should emit to `optionSelectionChanges` when an option is selected', fakeAsync(() => { + trigger.click(); + fixture.detectChanges(); + flush(); + + const spy = jasmine.createSpy('option selection spy'); + const subscription = fixture.componentInstance.select.optionSelectionChanges.subscribe(spy); + const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; + option.click(); + fixture.detectChanges(); + flush(); + + expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange)); + + subscription.unsubscribe(); + })); + + it('should handle accessing `optionSelectionChanges` before the options are initialized', + fakeAsync(() => { + fixture.destroy(); + fixture = TestBed.createComponent(BasicSelect); + + let spy = jasmine.createSpy('option selection spy'); + let subscription: Subscription; + + expect(fixture.componentInstance.select.options).toBeFalsy(); + expect(() => { + subscription = fixture.componentInstance.select.optionSelectionChanges.subscribe(spy); + }).not.toThrow(); + + fixture.detectChanges(); + trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement; + + trigger.click(); + fixture.detectChanges(); + flush(); + + const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; + option.click(); + fixture.detectChanges(); + flush(); + + expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange)); + + subscription!.unsubscribe(); + })); + }); describe('forms integration', () => { diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index bb5252c3753d..6f6e60aba625 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -20,6 +20,7 @@ import { import {filter} from 'rxjs/operators/filter'; import {take} from 'rxjs/operators/take'; import {map} from 'rxjs/operators/map'; +import {switchMap} from 'rxjs/operators/switchMap'; import {startWith} from 'rxjs/operators/startWith'; import {takeUntil} from 'rxjs/operators/takeUntil'; import { @@ -75,6 +76,7 @@ import {MatFormField, MatFormFieldControl} from '@angular/material/form-field'; import {Observable} from 'rxjs/Observable'; import {merge} from 'rxjs/observable/merge'; import {Subject} from 'rxjs/Subject'; +import {defer} from 'rxjs/observable/defer'; import {fadeInContent, transformPanel} from './select-animations'; import { getMatSelectDynamicMultipleError, @@ -397,9 +399,15 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit, private _id: string; /** Combined stream of all of the child options' change events. */ - get optionSelectionChanges(): Observable { - return merge(...this.options.map(option => option.onSelectionChange)); - } + optionSelectionChanges: Observable = defer(() => { + if (this.options) { + return merge(...this.options.map(option => option.onSelectionChange)); + } + + return this._ngZone.onStable + .asObservable() + .pipe(take(1), switchMap(() => this.optionSelectionChanges)); + }); /** Event emitted when the select has been opened. */ @Output() openedChange: EventEmitter = new EventEmitter();