diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 776b87117479..b56b2fe9b328 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -15,7 +15,6 @@ import { PositionStrategy, ScrollStrategy, } from '@angular/cdk/overlay'; -import {TemplatePortal} from '@angular/cdk/portal'; import {DOCUMENT} from '@angular/common'; import {filter, take, switchMap, delay, tap, map} from 'rxjs/operators'; import { @@ -30,7 +29,6 @@ import { NgZone, OnDestroy, Optional, - ViewContainerRef, } from '@angular/core'; import {ViewportRuler} from '@angular/cdk/scrolling'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; @@ -117,9 +115,9 @@ export function getMatAutocompleteMissingPanelError(): Error { }) export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { private _overlayRef: OverlayRef | null; - private _portal: TemplatePortal; private _componentDestroyed = false; private _autocompleteDisabled = false; + private _autocomplete: MatAutocomplete; private _scrollStrategy: () => ScrollStrategy; /** Old value of the native input. Used to work around issues with the `input` event on IE. */ @@ -132,7 +130,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { private _manuallyFloatingLabel = false; /** The subscription for closing actions (some are bound to document). */ - private _closingActionsSubscription: Subscription; + private _closingActionsSubscription = Subscription.EMPTY; /** Subscription to viewport size changes. */ private _viewportSubscription = Subscription.EMPTY; @@ -166,7 +164,12 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { _onTouched = () => {}; /** The autocomplete panel to be attached to this trigger. */ - @Input('matAutocomplete') autocomplete: MatAutocomplete; + @Input('matAutocomplete') + get autocomplete(): MatAutocomplete { return this._autocomplete; } + set autocomplete(value: MatAutocomplete) { + this._autocomplete = value; + this._detachOverlay(); + } /** * Reference relative to which to position the autocomplete panel. @@ -190,8 +193,8 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { this._autocompleteDisabled = coerceBooleanProperty(value); } - constructor(private _element: ElementRef, private _overlay: Overlay, - private _viewContainerRef: ViewContainerRef, + constructor(private _element: ElementRef, + private _overlay: Overlay, private _zone: NgZone, private _changeDetectorRef: ChangeDetectorRef, @Inject(MAT_AUTOCOMPLETE_SCROLL_STRATEGY) scrollStrategy: any, @@ -246,12 +249,9 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { this.autocomplete.closed.emit(); } - this.autocomplete._isOpen = this._overlayAttached = false; + this.autocomplete._isOpen = false; + this._detachOverlay(); - if (this._overlayRef && this._overlayRef.hasAttached()) { - this._overlayRef.detach(); - this._closingActionsSubscription.unsubscribe(); - } // Note that in some cases this can end up being called after the component is destroyed. // Add a check to ensure that we don't try to run change detection on a destroyed view. @@ -570,7 +570,6 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { } if (!this._overlayRef) { - this._portal = new TemplatePortal(this.autocomplete.template, this._viewContainerRef); this._overlayRef = this._overlay.create(this._getOverlayConfig()); // Use the `keydownEvents` in order to take advantage of @@ -597,7 +596,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { } if (this._overlayRef && !this._overlayRef.hasAttached()) { - this._overlayRef.attach(this._portal); + this._overlayRef.attach(this.autocomplete._portal); this._closingActionsSubscription = this._subscribeToClosingActions(); } @@ -613,6 +612,14 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { } } + private _detachOverlay() { + this._overlayAttached = false; + this._closingActionsSubscription.unsubscribe(); + if (this._overlayRef) { + this._overlayRef.detach(); + } + } + private _getOverlayConfig(): OverlayConfig { return new OverlayConfig({ positionStrategy: this._getOverlayPosition(), diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index d03b7bc34729..668864bcae9f 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -2221,6 +2221,35 @@ describe('MatAutocomplete', () => { expect(formControl.value).toBe('Cal', 'Expected new value to be propagated to model'); })); + it('should work when dynamically changing the autocomplete', () => { + const fixture = createComponent(DynamicallyChangingAutocomplete); + fixture.detectChanges(); + const input = fixture.debugElement.query(By.css('input')).nativeElement; + + dispatchFakeEvent(input, 'focusin'); + fixture.detectChanges(); + + expect(overlayContainerElement.textContent).toContain('First', + `Expected panel to display the option of the first autocomplete.`); + expect(overlayContainerElement.textContent).not.toContain('Second', + `Expected panel to not display the option of the second autocomplete.`); + + dispatchFakeEvent(document, 'click'); + fixture.detectChanges(); + + fixture.componentInstance.trigger.autocomplete = fixture.componentInstance.autoTow; + fixture.detectChanges(); + + dispatchFakeEvent(input, 'focusin'); + fixture.detectChanges(); + + expect(overlayContainerElement.textContent).not.toContain('First', + `Expected panel to not display the option of the first autocomplete.`); + expect(overlayContainerElement.textContent).toContain('Second', + `Expected panel to display the option of the second autocomplete.`); + + }); + }); @Component({ @@ -2607,3 +2636,21 @@ class AutocompleteWithNativeAutocompleteAttribute { }) class InputWithoutAutocompleteAndDisabled { } + +@Component({ + template: ` + + + First + + + + Second + + `, +}) +class DynamicallyChangingAutocomplete { + @ViewChild('autoOne') autoOne: MatAutocomplete; + @ViewChild('autoTow') autoTow: MatAutocomplete; + @ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger; +} diff --git a/src/lib/autocomplete/autocomplete.ts b/src/lib/autocomplete/autocomplete.ts index bbd5a0ca1b26..8c31c34e4c47 100644 --- a/src/lib/autocomplete/autocomplete.ts +++ b/src/lib/autocomplete/autocomplete.ts @@ -24,6 +24,8 @@ import { TemplateRef, ViewChild, ViewEncapsulation, + AfterViewInit, + ViewContainerRef, } from '@angular/core'; import { CanDisableRipple, @@ -33,6 +35,7 @@ import { MatOption, mixinDisableRipple, } from '@angular/material/core'; +import {TemplatePortal} from '@angular/cdk/portal'; /** @@ -92,7 +95,7 @@ export function MAT_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY(): MatAutocompleteDefau ] }) export class MatAutocomplete extends _MatAutocompleteMixinBase implements AfterContentInit, - CanDisableRipple { + AfterViewInit, CanDisableRipple { /** Manages active item in option list based on key events. */ _keyManager: ActiveDescendantKeyManager; @@ -100,6 +103,9 @@ export class MatAutocomplete extends _MatAutocompleteMixinBase implements AfterC /** Whether the autocomplete panel should be visible, depending on option length. */ showPanel: boolean = false; + /** @docs-private */ + _portal: TemplatePortal; + /** Whether the autocomplete panel is open. */ get isOpen(): boolean { return this._isOpen && this.showPanel; } _isOpen: boolean = false; @@ -165,12 +171,17 @@ export class MatAutocomplete extends _MatAutocompleteMixinBase implements AfterC constructor( private _changeDetectorRef: ChangeDetectorRef, private _elementRef: ElementRef, + private _viewContainerRef: ViewContainerRef, @Inject(MAT_AUTOCOMPLETE_DEFAULT_OPTIONS) defaults: MatAutocompleteDefaultOptions) { super(); this._autoActiveFirstOption = !!defaults.autoActiveFirstOption; } + ngAfterViewInit() { + this._portal = new TemplatePortal(this.template, this._viewContainerRef); + } + ngAfterContentInit() { this._keyManager = new ActiveDescendantKeyManager(this.options).withWrap(); // Set the initial visibility state.