@angular/cdk
Version:
Angular Material Component Development Kit
615 lines (609 loc) • 25.8 kB
JavaScript
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