UNPKG

@angular/cdk

Version:

Angular Material Component Development Kit

615 lines (609 loc) 25.8 kB
import * as i0 from '@angular/core'; import { ElementRef, NgModuleRef, EnvironmentInjector, createComponent, Injector, inject, TemplateRef, ViewContainerRef, Directive, DOCUMENT, EventEmitter, Input, Output, NgModule } from '@angular/core'; /** * Throws an exception when attempting to attach a null portal to a host. * @docs-private */ function throwNullPortalError() { throw Error('Must provide a portal to attach'); } /** * Throws an exception when attempting to attach a portal to a host that is already attached. * @docs-private */ function throwPortalAlreadyAttachedError() { throw Error('Host already has a portal attached'); } /** * Throws an exception when attempting to attach a portal to an already-disposed host. * @docs-private */ function throwPortalOutletAlreadyDisposedError() { throw Error('This PortalOutlet has already been disposed'); } /** * Throws an exception when attempting to attach an unknown portal type. * @docs-private */ function throwUnknownPortalTypeError() { throw Error('Attempting to attach an unknown Portal type. BasePortalOutlet accepts either ' + 'a ComponentPortal or a TemplatePortal.'); } /** * Throws an exception when attempting to attach a portal to a null host. * @docs-private */ function throwNullPortalOutletError() { throw Error('Attempting to attach a portal to a null PortalOutlet'); } /** * Throws an exception when attempting to detach a portal that is not attached. * @docs-private */ function throwNoPortalAttachedError() { throw Error('Attempting to detach a portal that is not attached to a host'); } /** * A `Portal` is something that you want to render somewhere else. * It can be attach to / detached from a `PortalOutlet`. */ class Portal { _attachedHost; /** Attach this portal to a host. */ attach(host) { if (typeof ngDevMode === 'undefined' || ngDevMode) { if (host == null) { throwNullPortalOutletError(); } if (host.hasAttached()) { throwPortalAlreadyAttachedError(); } } this._attachedHost = host; return host.attach(this); } /** Detach this portal from its host */ detach() { let host = this._attachedHost; if (host != null) { this._attachedHost = null; host.detach(); } else if (typeof ngDevMode === 'undefined' || ngDevMode) { throwNoPortalAttachedError(); } } /** Whether this portal is attached to a host. */ get isAttached() { return this._attachedHost != null; } /** * Sets the PortalOutlet reference without performing `attach()`. This is used directly by * the PortalOutlet when it is performing an `attach()` or `detach()`. */ setAttachedHost(host) { this._attachedHost = host; } } /** * A `ComponentPortal` is a portal that instantiates some Component upon attachment. */ class ComponentPortal extends Portal { /** The type of the component that will be instantiated for attachment. */ component; /** * Where the attached component should live in Angular's *logical* component tree. * This is different from where the component *renders*, which is determined by the PortalOutlet. * The origin is necessary when the host is outside of the Angular application context. */ viewContainerRef; /** Injector used for the instantiation of the component. */ injector; /** * List of DOM nodes that should be projected through `<ng-content>` of the attached component. */ projectableNodes; constructor(component, viewContainerRef, injector, projectableNodes) { super(); this.component = component; this.viewContainerRef = viewContainerRef; this.injector = injector; this.projectableNodes = projectableNodes; } } /** * A `TemplatePortal` is a portal that represents some embedded template (TemplateRef). */ class TemplatePortal extends Portal { templateRef; viewContainerRef; context; injector; constructor( /** The embedded template that will be used to instantiate an embedded View in the host. */ templateRef, /** Reference to the ViewContainer into which the template will be stamped out. */ viewContainerRef, /** Contextual data to be passed in to the embedded view. */ context, /** The injector to use for the embedded view. */ injector) { super(); this.templateRef = templateRef; this.viewContainerRef = viewContainerRef; this.context = context; this.injector = injector; } get origin() { return this.templateRef.elementRef; } /** * Attach the portal to the provided `PortalOutlet`. * When a context is provided it will override the `context` property of the `TemplatePortal` * instance. */ attach(host, context = this.context) { this.context = context; return super.attach(host); } detach() { this.context = undefined; return super.detach(); } } /** * A `DomPortal` is a portal whose DOM element will be taken from its current position * in the DOM and moved into a portal outlet, when it is attached. On detach, the content * will be restored to its original position. */ class DomPortal extends Portal { /** DOM node hosting the portal's content. */ element; constructor(element) { super(); this.element = element instanceof ElementRef ? element.nativeElement : element; } } /** * Partial implementation of PortalOutlet that handles attaching * ComponentPortal and TemplatePortal. */ class BasePortalOutlet { /** The portal currently attached to the host. */ _attachedPortal; /** A function that will permanently dispose this host. */ _disposeFn; /** Whether this host has already been permanently disposed. */ _isDisposed = false; /** Whether this host has an attached portal. */ hasAttached() { return !!this._attachedPortal; } /** Attaches a portal. */ attach(portal) { if (typeof ngDevMode === 'undefined' || ngDevMode) { if (!portal) { throwNullPortalError(); } if (this.hasAttached()) { throwPortalAlreadyAttachedError(); } if (this._isDisposed) { throwPortalOutletAlreadyDisposedError(); } } if (portal instanceof ComponentPortal) { this._attachedPortal = portal; return this.attachComponentPortal(portal); } else if (portal instanceof TemplatePortal) { this._attachedPortal = portal; return this.attachTemplatePortal(portal); // @breaking-change 10.0.0 remove null check for `this.attachDomPortal`. } else if (this.attachDomPortal && portal instanceof DomPortal) { this._attachedPortal = portal; return this.attachDomPortal(portal); } if (typeof ngDevMode === 'undefined' || ngDevMode) { throwUnknownPortalTypeError(); } } // @breaking-change 10.0.0 `attachDomPortal` to become a required abstract method. attachDomPortal = null; /** Detaches a previously attached portal. */ detach() { if (this._attachedPortal) { this._attachedPortal.setAttachedHost(null); this._attachedPortal = null; } this._invokeDisposeFn(); } /** Permanently dispose of this portal host. */ dispose() { if (this.hasAttached()) { this.detach(); } this._invokeDisposeFn(); this._isDisposed = true; } /** @docs-private */ setDisposeFn(fn) { this._disposeFn = fn; } _invokeDisposeFn() { if (this._disposeFn) { this._disposeFn(); this._disposeFn = null; } } } /** * A PortalOutlet for attaching portals to an arbitrary DOM element outside of the Angular * application context. */ class DomPortalOutlet extends BasePortalOutlet { outletElement; _appRef; _defaultInjector; /** * @param outletElement Element into which the content is projected. * @param _appRef Reference to the application. Only used in component portals when there * is no `ViewContainerRef` available. * @param _defaultInjector Injector to use as a fallback when the portal being attached doesn't * have one. Only used for component portals. */ constructor( /** Element into which the content is projected. */ outletElement, _appRef, _defaultInjector) { super(); this.outletElement = outletElement; this._appRef = _appRef; this._defaultInjector = _defaultInjector; } /** * Attach the given ComponentPortal to DOM element. * @param portal Portal to be attached * @returns Reference to the created component. */ attachComponentPortal(portal) { let componentRef; // If the portal specifies a ViewContainerRef, we will use that as the attachment point // for the component (in terms of Angular's component tree, not rendering). // When the ViewContainerRef is missing, we use the factory to create the component directly // and then manually attach the view to the application. if (portal.viewContainerRef) { const injector = portal.injector || portal.viewContainerRef.injector; const ngModuleRef = injector.get(NgModuleRef, null, { optional: true }) || undefined; componentRef = portal.viewContainerRef.createComponent(portal.component, { index: portal.viewContainerRef.length, injector, ngModuleRef, projectableNodes: portal.projectableNodes || undefined, }); this.setDisposeFn(() => componentRef.destroy()); } else { if ((typeof ngDevMode === 'undefined' || ngDevMode) && !this._appRef) { throw Error('Cannot attach component portal to outlet without an ApplicationRef.'); } const appRef = this._appRef; const elementInjector = portal.injector || this._defaultInjector || Injector.NULL; const environmentInjector = elementInjector.get(EnvironmentInjector, appRef.injector); componentRef = createComponent(portal.component, { elementInjector, environmentInjector, projectableNodes: portal.projectableNodes || undefined, }); appRef.attachView(componentRef.hostView); this.setDisposeFn(() => { // Verify that the ApplicationRef has registered views before trying to detach a host view. // This check also protects the `detachView` from being called on a destroyed ApplicationRef. if (appRef.viewCount > 0) { appRef.detachView(componentRef.hostView); } componentRef.destroy(); }); } // At this point the component has been instantiated, so we move it to the location in the DOM // where we want it to be rendered. this.outletElement.appendChild(this._getComponentRootNode(componentRef)); this._attachedPortal = portal; return componentRef; } /** * Attaches a template portal to the DOM as an embedded view. * @param portal Portal to be attached. * @returns Reference to the created embedded view. */ attachTemplatePortal(portal) { let viewContainer = portal.viewContainerRef; let viewRef = viewContainer.createEmbeddedView(portal.templateRef, portal.context, { injector: portal.injector, }); // The method `createEmbeddedView` will add the view as a child of the viewContainer. // But for the DomPortalOutlet the view can be added everywhere in the DOM // (e.g Overlay Container) To move the view to the specified host element. We just // re-append the existing root nodes. viewRef.rootNodes.forEach(rootNode => this.outletElement.appendChild(rootNode)); // Note that we want to detect changes after the nodes have been moved so that // any directives inside the portal that are looking at the DOM inside a lifecycle // hook won't be invoked too early. viewRef.detectChanges(); this.setDisposeFn(() => { let index = viewContainer.indexOf(viewRef); if (index !== -1) { viewContainer.remove(index); } }); this._attachedPortal = portal; // TODO(jelbourn): Return locals from view. return viewRef; } /** * Attaches a DOM portal by transferring its content into the outlet. * @param portal Portal to be attached. * @deprecated To be turned into a method. * @breaking-change 10.0.0 */ attachDomPortal = (portal) => { const element = portal.element; if (!element.parentNode && (typeof ngDevMode === 'undefined' || ngDevMode)) { throw Error('DOM portal content must be attached to a parent node.'); } // Anchor used to save the element's previous position so // that we can restore it when the portal is detached. const anchorNode = this.outletElement.ownerDocument.createComment('dom-portal'); element.parentNode.insertBefore(anchorNode, element); this.outletElement.appendChild(element); this._attachedPortal = portal; super.setDisposeFn(() => { // We can't use `replaceWith` here because IE doesn't support it. if (anchorNode.parentNode) { anchorNode.parentNode.replaceChild(element, anchorNode); } }); }; /** * Clears out a portal from the DOM. */ dispose() { super.dispose(); this.outletElement.remove(); } /** Gets the root HTMLElement for an instantiated component. */ _getComponentRootNode(componentRef) { return componentRef.hostView.rootNodes[0]; } } /** * Directive version of a `TemplatePortal`. Because the directive *is* a TemplatePortal, * the directive instance itself can be attached to a host, enabling declarative use of portals. */ class CdkPortal extends TemplatePortal { constructor() { const templateRef = inject(TemplateRef); const viewContainerRef = inject(ViewContainerRef); super(templateRef, viewContainerRef); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: CdkPortal, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.0", type: CdkPortal, isStandalone: true, selector: "[cdkPortal]", exportAs: ["cdkPortal"], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: CdkPortal, decorators: [{ type: Directive, args: [{ selector: '[cdkPortal]', exportAs: 'cdkPortal', }] }], ctorParameters: () => [] }); /** * @deprecated Use `CdkPortal` instead. * @breaking-change 9.0.0 */ class TemplatePortalDirective extends CdkPortal { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TemplatePortalDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.0", type: TemplatePortalDirective, isStandalone: true, selector: "[cdk-portal], [portal]", providers: [ { provide: CdkPortal, useExisting: TemplatePortalDirective, }, ], exportAs: ["cdkPortal"], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TemplatePortalDirective, decorators: [{ type: Directive, args: [{ selector: '[cdk-portal], [portal]', exportAs: 'cdkPortal', providers: [ { provide: CdkPortal, useExisting: TemplatePortalDirective, }, ], }] }] }); /** * Directive version of a PortalOutlet. Because the directive *is* a PortalOutlet, portals can be * directly attached to it, enabling declarative use. * * Usage: * `<ng-template [cdkPortalOutlet]="greeting"></ng-template>` */ class CdkPortalOutlet extends BasePortalOutlet { _moduleRef = inject(NgModuleRef, { optional: true }); _document = inject(DOCUMENT); _viewContainerRef = inject(ViewContainerRef); /** Whether the portal component is initialized. */ _isInitialized = false; /** Reference to the currently-attached component/view ref. */ _attachedRef; constructor() { super(); } /** Portal associated with the Portal outlet. */ get portal() { return this._attachedPortal; } set portal(portal) { // Ignore the cases where the `portal` is set to a falsy value before the lifecycle hooks have // run. This handles the cases where the user might do something like `<div cdkPortalOutlet>` // and attach a portal programmatically in the parent component. When Angular does the first CD // round, it will fire the setter with empty string, causing the user's content to be cleared. if (this.hasAttached() && !portal && !this._isInitialized) { return; } if (this.hasAttached()) { super.detach(); } if (portal) { super.attach(portal); } this._attachedPortal = portal || null; } /** Emits when a portal is attached to the outlet. */ attached = new EventEmitter(); /** Component or view reference that is attached to the portal. */ get attachedRef() { return this._attachedRef; } ngOnInit() { this._isInitialized = true; } ngOnDestroy() { super.dispose(); this._attachedRef = this._attachedPortal = null; } /** * Attach the given ComponentPortal to this PortalOutlet. * * @param portal Portal to be attached to the portal outlet. * @returns Reference to the created component. */ attachComponentPortal(portal) { portal.setAttachedHost(this); // If the portal specifies an origin, use that as the logical location of the component // in the application tree. Otherwise use the location of this PortalOutlet. const viewContainerRef = portal.viewContainerRef != null ? portal.viewContainerRef : this._viewContainerRef; const ref = viewContainerRef.createComponent(portal.component, { index: viewContainerRef.length, injector: portal.injector || viewContainerRef.injector, projectableNodes: portal.projectableNodes || undefined, ngModuleRef: this._moduleRef || undefined, }); // If we're using a view container that's different from the injected one (e.g. when the portal // specifies its own) we need to move the component into the outlet, otherwise it'll be rendered // inside of the alternate view container. if (viewContainerRef !== this._viewContainerRef) { this._getRootNode().appendChild(ref.hostView.rootNodes[0]); } super.setDisposeFn(() => ref.destroy()); this._attachedPortal = portal; this._attachedRef = ref; this.attached.emit(ref); return ref; } /** * Attach the given TemplatePortal to this PortalHost as an embedded View. * @param portal Portal to be attached. * @returns Reference to the created embedded view. */ attachTemplatePortal(portal) { portal.setAttachedHost(this); const viewRef = this._viewContainerRef.createEmbeddedView(portal.templateRef, portal.context, { injector: portal.injector, }); super.setDisposeFn(() => this._viewContainerRef.clear()); this._attachedPortal = portal; this._attachedRef = viewRef; this.attached.emit(viewRef); return viewRef; } /** * Attaches the given DomPortal to this PortalHost by moving all of the portal content into it. * @param portal Portal to be attached. * @deprecated To be turned into a method. * @breaking-change 10.0.0 */ attachDomPortal = (portal) => { const element = portal.element; if (!element.parentNode && (typeof ngDevMode === 'undefined' || ngDevMode)) { throw Error('DOM portal content must be attached to a parent node.'); } // Anchor used to save the element's previous position so // that we can restore it when the portal is detached. const anchorNode = this._document.createComment('dom-portal'); portal.setAttachedHost(this); element.parentNode.insertBefore(anchorNode, element); this._getRootNode().appendChild(element); this._attachedPortal = portal; super.setDisposeFn(() => { if (anchorNode.parentNode) { anchorNode.parentNode.replaceChild(element, anchorNode); } }); }; /** Gets the root node of the portal outlet. */ _getRootNode() { const nativeElement = this._viewContainerRef.element.nativeElement; // The directive could be set on a template which will result in a comment // node being the root. Use the comment's parent node if that is the case. return (nativeElement.nodeType === nativeElement.ELEMENT_NODE ? nativeElement : nativeElement.parentNode); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: CdkPortalOutlet, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.0", type: CdkPortalOutlet, isStandalone: true, selector: "[cdkPortalOutlet]", inputs: { portal: ["cdkPortalOutlet", "portal"] }, outputs: { attached: "attached" }, exportAs: ["cdkPortalOutlet"], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: CdkPortalOutlet, decorators: [{ type: Directive, args: [{ selector: '[cdkPortalOutlet]', exportAs: 'cdkPortalOutlet', }] }], ctorParameters: () => [], propDecorators: { portal: [{ type: Input, args: ['cdkPortalOutlet'] }], attached: [{ type: Output }] } }); /** * @deprecated Use `CdkPortalOutlet` instead. * @breaking-change 9.0.0 */ class PortalHostDirective extends CdkPortalOutlet { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: PortalHostDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.0", type: PortalHostDirective, isStandalone: true, selector: "[cdkPortalHost], [portalHost]", inputs: { portal: ["cdkPortalHost", "portal"] }, providers: [ { provide: CdkPortalOutlet, useExisting: PortalHostDirective, }, ], exportAs: ["cdkPortalHost"], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: PortalHostDirective, decorators: [{ type: Directive, args: [{ selector: '[cdkPortalHost], [portalHost]', exportAs: 'cdkPortalHost', inputs: [{ name: 'portal', alias: 'cdkPortalHost' }], providers: [ { provide: CdkPortalOutlet, useExisting: PortalHostDirective, }, ], }] }] }); class PortalModule { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: PortalModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.0.0", ngImport: i0, type: PortalModule, imports: [CdkPortal, CdkPortalOutlet, TemplatePortalDirective, PortalHostDirective], exports: [CdkPortal, CdkPortalOutlet, TemplatePortalDirective, PortalHostDirective] }); static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: PortalModule }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: PortalModule, decorators: [{ type: NgModule, args: [{ imports: [CdkPortal, CdkPortalOutlet, TemplatePortalDirective, PortalHostDirective], exports: [CdkPortal, CdkPortalOutlet, TemplatePortalDirective, PortalHostDirective], }] }] }); export { BasePortalOutlet, CdkPortal, CdkPortalOutlet, ComponentPortal, DomPortal, DomPortalOutlet, Portal, PortalHostDirective, PortalModule, TemplatePortal, TemplatePortalDirective }; //# sourceMappingURL=portal.mjs.map