diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 7f075637cfe6..62da00e5d78e 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -20,6 +20,7 @@ import {TemplatePortal} from '@angular/cdk/portal'; import {DOCUMENT} from '@angular/common'; import {filter, take, switchMap, delay, tap, map} from 'rxjs/operators'; import { + AfterContentChecked, ChangeDetectorRef, Directive, ElementRef, @@ -115,7 +116,8 @@ export function getMatAutocompleteMissingPanelError(): Error { exportAs: 'matAutocompleteTrigger', providers: [MAT_AUTOCOMPLETE_VALUE_ACCESSOR] }) -export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { +export class MatAutocompleteTrigger implements ControlValueAccessor, AfterContentChecked, + OnDestroy { private _overlayRef: OverlayRef | null; private _portal: TemplatePortal; private _componentDestroyed = false; @@ -143,6 +145,12 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { */ private _canOpenOnNextFocus = true; + /** Whether the component has been initializied. */ + private _isInitialized: boolean; + + /** Initial value that should be shown after the component is initialized. */ + private _initialValueToSelect: any; + /** Stream of keyboard events that can close the panel. */ private readonly _closeKeyEventStream = new Subject(); @@ -207,6 +215,15 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { } } + ngAfterContentChecked() { + if (!this._isInitialized && typeof this._initialValueToSelect !== 'undefined') { + this._setTriggerValue(this._initialValueToSelect); + this._initialValueToSelect = undefined; + } + + this._isInitialized = true; + } + ngOnDestroy() { if (typeof window !== 'undefined') { window.removeEventListener('blur', this._windowBlurHandler); @@ -336,7 +353,14 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { // Implemented as part of ControlValueAccessor. writeValue(value: any): void { - Promise.resolve(null).then(() => this._setTriggerValue(value)); + if (this._isInitialized) { + this._setTriggerValue(value); + } else { + // If the component isn't initialized yet, defer until the first CD pass, otherwise we'll + // miss the initial `displayWith` value. By deferring until the first `AfterContentChecked` + // we avoid making the method async while preventing "changed after checked" errors. + this._initialValueToSelect = value; + } } // Implemented as part of ControlValueAccessor. diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 8dfe106521f9..67f5fd124eff 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -2199,6 +2199,17 @@ describe('MatAutocomplete', () => { expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect.bottom), 'Expected autocomplete panel to align with the bottom of the new origin.'); }); + + it('should evaluate `displayWith` before assigning the initial value', fakeAsync(() => { + const fixture = createComponent(PreselectedAutocompleteDisplayWith); + const input = fixture.nativeElement.querySelector('input'); + + fixture.detectChanges(); + flush(); + + expect(input.value).toBe('Alaska'); + })); + }); @Component({ @@ -2585,3 +2596,29 @@ class AutocompleteWithNativeAutocompleteAttribute { }) class InputWithoutAutocompleteAndDisabled { } + + +@Component({ + template: ` + + + + + + + {{ state.name }} + + + ` +}) +class PreselectedAutocompleteDisplayWith { + stateCtrl = new FormControl({code: 'AK', name: 'Alaska'}); + states = [ + {code: 'AL', name: 'Alabama'}, + {code: 'AK', name: 'Alaska'} + ]; + + displayFn(value: any): string { + return value && typeof value === 'object' ? value.name : value; + } +}