Skip to content
35 changes: 21 additions & 14 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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';
Expand Down Expand Up @@ -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. */
Expand All @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -190,8 +193,8 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
this._autocompleteDisabled = coerceBooleanProperty(value);
}

constructor(private _element: ElementRef<HTMLInputElement>, private _overlay: Overlay,
private _viewContainerRef: ViewContainerRef,
constructor(private _element: ElementRef<HTMLInputElement>,
private _overlay: Overlay,
private _zone: NgZone,
private _changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_AUTOCOMPLETE_SCROLL_STRATEGY) scrollStrategy: any,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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();
}

Expand All @@ -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(),
Expand Down
47 changes: 47 additions & 0 deletions src/lib/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -2607,3 +2636,21 @@ class AutocompleteWithNativeAutocompleteAttribute {
})
class InputWithoutAutocompleteAndDisabled {
}

@Component({
template: `
<input type="number" matInput [matAutocomplete]="autoOne">
<mat-autocomplete #autoOne>
<mat-option [value]="0">First</mat-option>
</mat-autocomplete>

<mat-autocomplete #autoTow>
<mat-option [value]="1">Second</mat-option>
</mat-autocomplete>
`,
})
class DynamicallyChangingAutocomplete {
@ViewChild('autoOne') autoOne: MatAutocomplete;
@ViewChild('autoTow') autoTow: MatAutocomplete;
@ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger;
}
13 changes: 12 additions & 1 deletion src/lib/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
TemplateRef,
ViewChild,
ViewEncapsulation,
AfterViewInit,
ViewContainerRef,
} from '@angular/core';
import {
CanDisableRipple,
Expand All @@ -33,6 +35,7 @@ import {
MatOption,
mixinDisableRipple,
} from '@angular/material/core';
import {TemplatePortal} from '@angular/cdk/portal';


/**
Expand Down Expand Up @@ -92,14 +95,17 @@ 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<MatOption>;

/** 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;
Expand Down Expand Up @@ -165,12 +171,17 @@ export class MatAutocomplete extends _MatAutocompleteMixinBase implements AfterC
constructor(
private _changeDetectorRef: ChangeDetectorRef,
private _elementRef: ElementRef<HTMLElement>,
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<MatOption>(this.options).withWrap();
// Set the initial visibility state.
Expand Down