From cbf47a7b82d66c890e3546b1ce7263e3995a754c Mon Sep 17 00:00:00 2001 From: Jeremy Elbourn Date: Wed, 22 Nov 2017 07:42:05 -0800 Subject: [PATCH] fix(overlay): make config immutable for existing refs This changes makes the OverlayConfig instance in OverlatRef immutable. Calling `getConfig` will now return a clone of the config to make clear that it cannot be modified directly to change the state of the overlay. This also updates the `updateSize` method to accept a partial config to apply to the existing config (and make a new config instance). This *also* tightens up the typings on Portal.attach BREAKING CHANGE: OverlayRef.getConfig returns an immutable version of the config object. BREAKING CHANGE: OverlayRef.updateSize now accepts a OverlaySizeConfig rather than being based on the existing config object. --- src/cdk/overlay/overlay-directives.ts | 2 +- src/cdk/overlay/overlay-ref.ts | 85 +++++++++++++------- src/cdk/portal/portal.ts | 4 + src/lib/autocomplete/autocomplete-trigger.ts | 3 +- 4 files changed, 63 insertions(+), 31 deletions(-) diff --git a/src/cdk/overlay/overlay-directives.ts b/src/cdk/overlay/overlay-directives.ts index d9261639d29b..6123df74b630 100644 --- a/src/cdk/overlay/overlay-directives.ts +++ b/src/cdk/overlay/overlay-directives.ts @@ -333,7 +333,7 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { } this._position.withDirection(this.dir); - this._overlayRef.getConfig().direction = this.dir; + this._overlayRef.setDirection(this.dir); this._document.addEventListener('keydown', this._escapeListener); if (!this._overlayRef.hasAttached()) { diff --git a/src/cdk/overlay/overlay-ref.ts b/src/cdk/overlay/overlay-ref.ts index 6a0919addc36..c1be7f8b78b2 100644 --- a/src/cdk/overlay/overlay-ref.ts +++ b/src/cdk/overlay/overlay-ref.ts @@ -6,14 +6,20 @@ * found in the LICENSE file at https://angular.io/license */ -import {NgZone} from '@angular/core'; -import {PortalOutlet, Portal} from '@angular/cdk/portal'; -import {OverlayConfig} from './overlay-config'; -import {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher'; +import {Direction} from '@angular/cdk/bidi'; +import {ComponentPortal, Portal, PortalOutlet, TemplatePortal} from '@angular/cdk/portal'; +import {ComponentRef, EmbeddedViewRef, NgZone} from '@angular/core'; import {Observable} from 'rxjs/Observable'; -import {Subject} from 'rxjs/Subject'; import {take} from 'rxjs/operators/take'; +import {Subject} from 'rxjs/Subject'; +import {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher'; +import {OverlayConfig} from './overlay-config'; + +/** An object where all of its properties cannot be written. */ +export type ImmutableObject = { + readonly [P in keyof T]: T[P]; +}; /** * Reference to an overlay that has been created with the Overlay service. @@ -31,7 +37,7 @@ export class OverlayRef implements PortalOutlet { constructor( private _portalOutlet: PortalOutlet, private _pane: HTMLElement, - private _config: OverlayConfig, + private _config: ImmutableObject, private _ngZone: NgZone, private _keyboardDispatcher: OverlayKeyboardDispatcher) { @@ -45,8 +51,14 @@ export class OverlayRef implements PortalOutlet { return this._pane; } + attach(portal: ComponentPortal): ComponentRef; + attach(portal: TemplatePortal): EmbeddedViewRef; + attach(portal: any): any; + /** - * Attaches the overlay to a portal instance and adds the backdrop. + * Attaches content, given via a Portal, to the overlay. + * If the overlay is configured to have a backdrop, it will be created. + * * @param portal Portal instance to which to attach the overlay. * @returns The portal attachment result. */ @@ -59,8 +71,8 @@ export class OverlayRef implements PortalOutlet { // Update the pane element with the given configuration. this._updateStackingOrder(); - this.updateSize(); - this.updateDirection(); + this._updateElementSize(); + this._updateElementDirection(); if (this._config.scrollStrategy) { this._config.scrollStrategy.enable(); @@ -133,9 +145,7 @@ export class OverlayRef implements PortalOutlet { return detachmentResult; } - /** - * Cleans up the overlay from the DOM. - */ + /** Cleans up the overlay from the DOM. */ dispose(): void { const isAttached = this.hasAttached(); @@ -161,16 +171,12 @@ export class OverlayRef implements PortalOutlet { this._detachments.complete(); } - /** - * Checks whether the overlay has been attached. - */ + /** Whether the overlay has attached content. */ hasAttached(): boolean { return this._portalOutlet.hasAttached(); } - /** - * Gets an observable that emits when the backdrop has been clicked. - */ + /** Gets an observable that emits when the backdrop has been clicked. */ backdropClick(): Observable { return this._backdropClick.asObservable(); } @@ -190,9 +196,7 @@ export class OverlayRef implements PortalOutlet { return this._keydownEvents.asObservable(); } - /** - * Gets the current config of the overlay. - */ + /** Gets the the current overlay configuration, which is immutable. */ getConfig(): OverlayConfig { return this._config; } @@ -204,13 +208,25 @@ export class OverlayRef implements PortalOutlet { } } + /** Update the size properties of the overlay. */ + updateSize(sizeConfig: OverlaySizeConfig) { + this._config = {...this._config, ...sizeConfig}; + this._updateElementSize(); + } + + /** Sets the LTR/RTL direction for the overlay. */ + setDirection(dir: Direction) { + this._config = {...this._config, direction: dir}; + this._updateElementDirection(); + } + /** Updates the text direction of the overlay panel. */ - private updateDirection() { + private _updateElementDirection() { this._pane.setAttribute('dir', this._config.direction!); } - /** Updates the size of the overlay based on the overlay config. */ - updateSize() { + /** Updates the size of the overlay element based on the overlay config. */ + private _updateElementSize() { if (this._config.width || this._config.width === 0) { this._pane.style.width = formatCssUnit(this._config.width); } @@ -259,10 +275,12 @@ export class OverlayRef implements PortalOutlet { this._backdropElement.addEventListener('click', () => this._backdropClick.next(null)); // Add class to fade-in the backdrop after one frame. - requestAnimationFrame(() => { - if (this._backdropElement) { - this._backdropElement.classList.add('cdk-overlay-backdrop-showing'); - } + this._ngZone.runOutsideAngular(() => { + requestAnimationFrame(() => { + if (this._backdropElement) { + this._backdropElement.classList.add('cdk-overlay-backdrop-showing'); + } + }); }); } @@ -323,3 +341,14 @@ export class OverlayRef implements PortalOutlet { function formatCssUnit(value: number | string) { return typeof value === 'string' ? value as string : `${value}px`; } + + +/** Size properties for an overlay. */ +export interface OverlaySizeConfig { + width?: number | string; + height?: number | string; + minWidth?: number | string; + minHeight?: number | string; + maxWidth?: number | string; + maxHeight?: number | string; +} diff --git a/src/cdk/portal/portal.ts b/src/cdk/portal/portal.ts index 911d01a6484c..981f12f562e1 100644 --- a/src/cdk/portal/portal.ts +++ b/src/cdk/portal/portal.ts @@ -181,6 +181,10 @@ export abstract class BasePortalOutlet implements PortalOutlet { return !!this._attachedPortal; } + attach(portal: ComponentPortal): ComponentRef; + attach(portal: TemplatePortal): EmbeddedViewRef; + attach(portal: any): any; + /** Attaches a portal. */ attach(portal: Portal): any { if (!portal) { diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index f848074a695b..2503da3e2d09 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -458,8 +458,7 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, OnDestroy { this._overlayRef = this._overlay.create(this._getOverlayConfig()); } else { /** Update the panel width, in case the host width has changed */ - this._overlayRef.getConfig().width = this._getHostWidth(); - this._overlayRef.updateSize(); + this._overlayRef.updateSize({width: this._getHostWidth()}); } if (this._overlayRef && !this._overlayRef.hasAttached()) {