diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 54c2c17adc3b..73ec7b0ded7c 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, @@ -114,7 +115,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; @@ -135,6 +137,12 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { /** Subscription to viewport size changes. */ private _viewportSubscription = Subscription.EMPTY; + /** 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(); @@ -174,6 +182,15 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { // @deletion-target 7.0.0 Make `_viewportRuler` required. private _viewportRuler?: ViewportRuler) {} + ngAfterContentChecked() { + if (!this._isInitialized && typeof this._initialValueToSelect !== 'undefined') { + this._setTriggerValue(this._initialValueToSelect); + this._initialValueToSelect = undefined; + } + + this._isInitialized = true; + } + ngOnDestroy() { this._viewportSubscription.unsubscribe(); this._componentDestroyed = true; @@ -289,7 +306,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 912dcf6fc53a..929f484a6644 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -1998,6 +1998,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({ @@ -2367,3 +2378,28 @@ class AutocompleteWithDifferentOrigin { selectedValue: string; values = ['one', 'two', 'three']; } + +@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; + } +}