fix(autocomplete): update template when changing autocomplete in trigger (#13814)

* fix(autocomplete): update template when changing autocomplete in trigger

* fix(autocomplet): improved test for changing autocomplete panel

fix(autocomplete): removed debug command in test

* fix(autocomplete): moved portal from autocomplete-trigger to autocomplete

* fix(autocomplete): setting currentPortal in trigger

* fix(autocomplete): fixes issue when detaching when panel updates

* feat(autocomplete): detaching portal when autocomplete input changes

* feat(autocomplete): moved overlay detach to its own method

* feat(autocomplete): typo & removed yarn-error.log
This commit is contained in:
Thomas Pink
2018-12-05 00:37:48 +01:00
committed by mmalerba
parent a0bcd81e39
commit 904a5eaeca
3 changed files with 80 additions and 15 deletions
+21 -14
View File
@@ -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<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,
@@ -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(),
+47
View File
@@ -2233,6 +2233,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({
@@ -2619,3 +2648,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;
}
+12 -1
View File
@@ -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<MatOption>;
@@ -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<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.