diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 42e5dcf77f96..9fe9b9b0635c 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -19,6 +19,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, @@ -116,7 +117,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; @@ -145,6 +147,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(); @@ -211,6 +219,15 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { this._scrollStrategy = scrollStrategy; } + 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); @@ -340,7 +357,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 84d7a3551bc7..a40d8262c01e 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -2319,6 +2319,16 @@ describe('MatAutocomplete', () => { expect(formControl.value).toBe('Cal', 'Expected new value to be propagated to model'); })); + 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({ @@ -2707,3 +2717,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; + } +}