UNPKG

@angular/cdk

Version:

Angular Material Component Development Kit

654 lines (648 loc) 27.1 kB
import * as i0 from '@angular/core'; import { ElementRef, NgModuleRef, EnvironmentInjector, createComponent, Injector, inject, TemplateRef, ViewContainerRef, Directive, EventEmitter, Input, Output, NgModule } from '@angular/core'; import { DOCUMENT } from '@angular/common'; /** * 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; /** * @deprecated No longer in use. To be removed. * @breaking-change 18.0.0 */ componentFactoryResolver; /** * List of DOM nodes that should be projected through `<ng-content>` of the attached component. */ projectableNodes; constructor(component, viewContainerRef, injector, /** * @deprecated No longer in use. To be removed. * @breaking-change 18.0.0 */ _componentFactoryResolver, 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; } } } /** * @deprecated Use `BasePortalOutlet` instead. * @breaking-change 9.0.0 */ class BasePortalHost extends BasePortalOutlet { } /** * A PortalOutlet for attaching portals to an arbitrary DOM element outside of the Angular * application context. */ class DomPortalOutlet extends BasePortalOutlet { outletElement; _appRef; _defaultInjector; _document; /** * @param outletElement Element into which the content is projected. * @param _unusedComponentFactoryResolver Used to resolve the component factory. * Only required when attaching component portals. * @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. * @param _document Reference to the document. Used when attaching a DOM portal. Will eventually * become a required parameter. */ constructor( /** Element into which the content is projected. */ outletElement, /** * @deprecated No longer in use. To be removed. * @breaking-change 18.0.0 */ _unusedComponentFactoryResolver, _appRef, _defaultInjector, /** * @deprecated `_document` Parameter to be made required. * @breaking-change 10.0.0 */ _document) { super(); this.outletElement = outletElement; this._appRef = _appRef; this._defaultInjector = _defaultInjector; this._document = _document; } /** * 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._document.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]; } } /** * @deprecated Use `DomPortalOutlet` instead. * @breaking-change 9.0.0 */ class DomPortalHost extends DomPortalOutlet { } /** * 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: "19.2.6", ngImport: i0, type: CdkPortal, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: CdkPortal, isStandalone: true, selector: "[cdkPortal]", exportAs: ["cdkPortal"], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", 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: "19.2.6", ngImport: i0, type: TemplatePortalDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", 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: "19.2.6", 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: "19.2.6", ngImport: i0, type: CdkPortalOutlet, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", 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: "19.2.6", 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: "19.2.6", ngImport: i0, type: PortalHostDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", 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: "19.2.6", 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: "19.2.6", ngImport: i0, type: PortalModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.6", ngImport: i0, type: PortalModule, imports: [CdkPortal, CdkPortalOutlet, TemplatePortalDirective, PortalHostDirective], exports: [CdkPortal, CdkPortalOutlet, TemplatePortalDirective, PortalHostDirective] }); static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PortalModule }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: PortalModule, decorators: [{ type: NgModule, args: [{ imports: [CdkPortal, CdkPortalOutlet, TemplatePortalDirective, PortalHostDirective], exports: [CdkPortal, CdkPortalOutlet, TemplatePortalDirective, PortalHostDirective], }] }] }); export { BasePortalOutlet as B, ComponentPortal as C, DomPortal as D, Portal as P, TemplatePortal as T, BasePortalHost as a, DomPortalOutlet as b, DomPortalHost as c, CdkPortal as d, TemplatePortalDirective as e, CdkPortalOutlet as f, PortalHostDirective as g, PortalModule as h }; //# sourceMappingURL=portal-directives-Bw5woq8I.mjs.map