From f2c81d14ace01bef8bc0ed62e14b8565cfbe13a5 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 17 May 2021 18:03:55 +0200 Subject: [PATCH] fix(material/autocomplete): optionSelections not emitting when the list of options changes Fixes the `MatAutocompleteTrigger.optionSelections` stopping to emit when the list options has been swapped out. Fixes #14777. --- .../mdc-autocomplete/autocomplete.spec.ts | 28 +++++++++++++++++++ .../autocomplete/autocomplete-trigger.ts | 15 ++++++---- .../autocomplete/autocomplete.spec.ts | 28 +++++++++++++++++++ 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts index 73e3db5957fd..2801606ff637 100644 --- a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts +++ b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts @@ -2120,6 +2120,34 @@ describe('MDC-based MatAutocomplete', () => { subscription!.unsubscribe(); })); + it('should emit to `optionSelections` if the list of options changes', fakeAsync(() => { + const spy = jasmine.createSpy('option selection spy'); + const subscription = fixture.componentInstance.trigger.optionSelections.subscribe(spy); + const openAndSelectFirstOption = () => { + fixture.detectChanges(); + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + (overlayContainerElement.querySelector('mat-option') as HTMLElement).click(); + fixture.detectChanges(); + zone.simulateZoneExit(); + }; + + fixture.componentInstance.states = [{code: 'OR', name: 'Oregon'}]; + fixture.detectChanges(); + + openAndSelectFirstOption(); + expect(spy).toHaveBeenCalledTimes(1); + + fixture.componentInstance.states = [{code: 'WV', name: 'West Virginia'}]; + fixture.detectChanges(); + + openAndSelectFirstOption(); + expect(spy).toHaveBeenCalledTimes(2); + + subscription!.unsubscribe(); + })); + it('should reposition the panel when the amount of options changes', fakeAsync(() => { let formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement; let inputReference = formField.querySelector('.mdc-text-field'); diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index 165c24055fc9..013fb33f2770 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -47,7 +47,7 @@ import { } from '@angular/material/core'; import {MAT_FORM_FIELD, MatFormField} from '@angular/material/form-field'; import {defer, fromEvent, merge, Observable, of as observableOf, Subject, Subscription} from 'rxjs'; -import {delay, filter, map, switchMap, take, tap} from 'rxjs/operators'; +import {delay, filter, map, switchMap, take, tap, startWith} from 'rxjs/operators'; import { _MatAutocompleteBase, @@ -304,10 +304,15 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso ); } - /** Stream of autocomplete option selections. */ + /** Stream of changes to the selection state of the autocomplete options. */ readonly optionSelections: Observable = defer(() => { - if (this.autocomplete && this.autocomplete.options) { - return merge(...this.autocomplete.options.map(option => option.onSelectionChange)); + const options = this.autocomplete ? this.autocomplete.options : null; + + if (options) { + return options.changes.pipe( + startWith(options), + switchMap(() => merge(...options.map(option => option.onSelectionChange))) + ); } // If there are any subscribers before `ngAfterViewInit`, the `autocomplete` will be undefined. @@ -349,7 +354,7 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso // Implemented as part of ControlValueAccessor. writeValue(value: any): void { - Promise.resolve(null).then(() => this._setTriggerValue(value)); + Promise.resolve().then(() => this._setTriggerValue(value)); } // Implemented as part of ControlValueAccessor. diff --git a/src/material/autocomplete/autocomplete.spec.ts b/src/material/autocomplete/autocomplete.spec.ts index 7e7b05b838e5..91ac6ad5ce11 100644 --- a/src/material/autocomplete/autocomplete.spec.ts +++ b/src/material/autocomplete/autocomplete.spec.ts @@ -2102,6 +2102,34 @@ describe('MatAutocomplete', () => { subscription!.unsubscribe(); })); + it('should emit to `optionSelections` if the list of options changes', fakeAsync(() => { + const spy = jasmine.createSpy('option selection spy'); + const subscription = fixture.componentInstance.trigger.optionSelections.subscribe(spy); + const openAndSelectFirstOption = () => { + fixture.detectChanges(); + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + zone.simulateZoneExit(); + (overlayContainerElement.querySelector('mat-option') as HTMLElement).click(); + fixture.detectChanges(); + zone.simulateZoneExit(); + }; + + fixture.componentInstance.states = [{code: 'OR', name: 'Oregon'}]; + fixture.detectChanges(); + + openAndSelectFirstOption(); + expect(spy).toHaveBeenCalledTimes(1); + + fixture.componentInstance.states = [{code: 'WV', name: 'West Virginia'}]; + fixture.detectChanges(); + + openAndSelectFirstOption(); + expect(spy).toHaveBeenCalledTimes(2); + + subscription!.unsubscribe(); + })); + it('should reposition the panel when the amount of options changes', fakeAsync(() => { let formField = fixture.debugElement.query(By.css('.mat-form-field'))!.nativeElement; let inputReference = formField.querySelector('.mat-form-field-flex');