From 1483b0ffc9a34f94bd1615d0303c4d72cb4178bb Mon Sep 17 00:00:00 2001 From: thomaspink Date: Thu, 25 Oct 2018 16:20:32 +0200 Subject: [PATCH] fix(autocomplete): not updating if panel is changed after init --- src/lib/autocomplete/autocomplete-trigger.ts | 35 +++++++++------ src/lib/autocomplete/autocomplete.spec.ts | 47 ++++++++++++++++++++ src/lib/autocomplete/autocomplete.ts | 15 ++++++- tools/public_api_guard/lib/autocomplete.d.ts | 8 ++-- 4 files changed, 86 insertions(+), 19 deletions(-) diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 2283fc670efc..038998805436 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. @@ -572,7 +572,6 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { let overlayRef = this._overlayRef; if (!overlayRef) { - this._portal = new TemplatePortal(this.autocomplete.template, this._viewContainerRef); overlayRef = this._overlay.create(this._getOverlayConfig()); this._overlayRef = overlayRef; @@ -603,7 +602,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { } if (overlayRef && !overlayRef.hasAttached()) { - overlayRef.attach(this._portal); + overlayRef.attach(this.autocomplete._portal); this._closingActionsSubscription = this._subscribeToClosingActions(); } @@ -619,6 +618,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 63a1c3ac912b..f4bc05ee3129 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -2285,6 +2285,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({ @@ -2673,3 +2702,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..f8d888a35f75 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, - @Inject(MAT_AUTOCOMPLETE_DEFAULT_OPTIONS) defaults: MatAutocompleteDefaultOptions) { + @Inject(MAT_AUTOCOMPLETE_DEFAULT_OPTIONS) defaults: MatAutocompleteDefaultOptions, + private _viewContainerRef: ViewContainerRef) { 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. diff --git a/tools/public_api_guard/lib/autocomplete.d.ts b/tools/public_api_guard/lib/autocomplete.d.ts index a1d16b68674e..88b191e52339 100644 --- a/tools/public_api_guard/lib/autocomplete.d.ts +++ b/tools/public_api_guard/lib/autocomplete.d.ts @@ -22,12 +22,13 @@ export declare const MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY_PROVIDER: { export declare const MAT_AUTOCOMPLETE_VALUE_ACCESSOR: any; -export declare class MatAutocomplete extends _MatAutocompleteMixinBase implements AfterContentInit, CanDisableRipple { +export declare class MatAutocomplete extends _MatAutocompleteMixinBase implements AfterContentInit, AfterViewInit, CanDisableRipple { _classList: { [key: string]: boolean; }; _isOpen: boolean; _keyManager: ActiveDescendantKeyManager; + _portal: TemplatePortal; autoActiveFirstOption: boolean; classList: string; readonly closed: EventEmitter; @@ -42,12 +43,13 @@ export declare class MatAutocomplete extends _MatAutocompleteMixinBase implement panelWidth: string | number; showPanel: boolean; template: TemplateRef; - constructor(_changeDetectorRef: ChangeDetectorRef, _elementRef: ElementRef, defaults: MatAutocompleteDefaultOptions); + constructor(_changeDetectorRef: ChangeDetectorRef, _elementRef: ElementRef, defaults: MatAutocompleteDefaultOptions, _viewContainerRef: ViewContainerRef); _emitSelectEvent(option: MatOption): void; _getScrollTop(): number; _setScrollTop(scrollTop: number): void; _setVisibility(): void; ngAfterContentInit(): void; + ngAfterViewInit(): void; } export declare class MatAutocompleteBase { @@ -85,7 +87,7 @@ export declare class MatAutocompleteTrigger implements ControlValueAccessor, OnD readonly optionSelections: Observable; readonly panelClosingActions: Observable; readonly panelOpen: boolean; - constructor(_element: ElementRef, _overlay: Overlay, _viewContainerRef: ViewContainerRef, _zone: NgZone, _changeDetectorRef: ChangeDetectorRef, scrollStrategy: any, _dir: Directionality, _formField: MatFormField, _document: any, _viewportRuler?: ViewportRuler | undefined); + constructor(_element: ElementRef, _overlay: Overlay, _zone: NgZone, _changeDetectorRef: ChangeDetectorRef, scrollStrategy: any, _dir: Directionality, _formField: MatFormField, _document: any, _viewportRuler?: ViewportRuler | undefined); _handleFocus(): void; _handleInput(event: KeyboardEvent): void; _handleKeydown(event: KeyboardEvent): void;