CDK里面Portals模块的功能就是将动态内容(这个内容可以是Component也可以是一个TemplateRef)呈现到应用程序中。更加直接点的解释就是把Portal放到指定位置(我们把他叫做插槽PortalOutlet)。Portal有两个子类ComponentPortal(对应组件)或者TemplatePortal(对应TemplateRef)。
Portal:表示内容。PortalOutlet: 放置内容的位置。
Selector: [cdk-portal] [cdkPortal] [portal]
Exported as: cdkPortal
CdkPortal指令继承自TemplatePortal,CdkPortal指令代表当前元素是一个Portal,这个Portal是可以放置到PortalOutlet里面去的。
注意:CdkPortal指令没有提供@Input()和@Output()。
CdkPortal属性 | 解释 |
---|---|
context: C | undefined | ng-template需要传递的参数,可以参考ngTemplateOutletContext的用法 |
isAttached: boolean | 是否attach到节点里面去了 |
templateRef: TemplateRef | 嵌入视图 |
viewContainerRef: ViewContainerRef | 视图容器(内部会通过上下文拿到),内部应该是要通过视图容器来插入元素 |
/**
* 把CdkPortal对应的Portal attach 到 PortalOutlet里面去(把组件显示出来)
* @Param host PortalOutlet
* @Param context ng-template需要传入的内容
*/
attach(host: PortalOutlet, context?: C | undefined): C;
/**
* 把CdkPortal对应的Portal从PortalOutlet detach移除掉
*/
detach(): void;
属性 | 解释 | |
---|---|---|
portal: Portal | null | @Input(cdkPortalOutlet) | CdkPortalOutlet位置attach的Portal |
attached: EventEmitter | @Output() | 当有Portal attach到CdkPortalOutlet位置的时候会回调 |
attachedRef: CdkPortalOutletAttachedRef | 已经attach的宿主视图或者嵌入视图(ComponentRef-Component提供|EmbeddedViewRef-Template提供) |
/** 是否有Portal attach到当前PortalOutlet上了 */
hasAttached(): boolean;
/**
* 把Portal(ComponentPortal、TemplatePortal) attach 到PortalOutlet上去
*/
attach<T>(portal: ComponentPortal<T>): ComponentRef<T>;
attach<T>(portal: TemplatePortal<T>): EmbeddedViewRef<T>;
attach(portal: any): any;
/**
* 把ComponentPortal attach 到PortalOutlet上去,使用ComponentFactoryResolver
*/
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T>;
/**
* 把TemplatePortal attach 到PortalOutlet上去,使用ComponentFactoryResolver
*/
attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C>;
/**
* 把当前PortalOutlet里面的Portal detach掉
*/
detach(): void;
/**
* 永久性的dispatch掉Portal
*/
dispose(): void;
大概看下cdk Portals模块里面的类,主要也是为了了解下类里面一些属性代表啥意思。
Portal类代表我们将要显示的动态内容。
/**
* Portal代表动态内容(可以代表)
*/
export declare abstract class Portal<T> {
private _attachedHost;
/** Portal attach 到PortalOutlet里面去 */
attach(host: PortalOutlet): T;
/** 把Portal从PortalOutlet里面移除掉*/
detach(): void;
/**
* 当前portal是否attach到PortalOutlet上了
*/
readonly isAttached: boolean;
/**
* 在 attach() 和 detach()调用的时候,这个函数会被PortalOutlet调用。我们使用的时候是不需要调用这个函数的
*/
setAttachedHost(host: PortalOutlet | null): void;
}
ComponentPortal类是组件视图对应的Portal,重点在构造函数。
/**
* ComponentPortal组件对应的Portal
*/
export declare class ComponentPortal<T> extends Portal<ComponentRef<T>> {
component: ComponentType<T>;
viewContainerRef?: ViewContainerRef | null;
injector?: Injector | null;
componentFactoryResolver?: ComponentFactoryResolver | null;
/**
* 构造函数
* @param component: 组件
* @param viewContainerRef: 试图容器
* @param injector: 注入器(用它来给组件传递参数),关于怎么传递参数,我们会在下面的实例里面讲到
* @param componentFactoryResolver: 组件工厂解析器
*/
constructor(component: ComponentType<T>, viewContainerRef?: ViewContainerRef | null, injector?: Injector | null, componentFactoryResolver?: ComponentFactoryResolver | null);
}
TemplatePortal类是嵌入视图对应的Portal,
/**
* TemplatePortal是嵌入试图对应的Portal
*/
export declare class TemplatePortal<C = any> extends Portal<C> {
templateRef: TemplateRef<C>;
viewContainerRef: ViewContainerRef;
context: C | undefined;
/**
*
* @param template: 嵌入试图对应的模板
* @param viewContainerRef: 试图容器
* @param context: 嵌入试图需要传递的参数
*/
constructor(template: TemplateRef<C>, viewContainerRef: ViewContainerRef, context?: C);
readonly origin: ElementRef;
attach(host: PortalOutlet, context?: C | undefined): C;
detach(): void;
}
PortalOutlet是代表我们动态内容需要放置的地方(插槽),用来放置ComponentPortal或者TemplatePortal。 PortalOutlet是一个接口。
/**
* Portal所要放置的位置
*/
export interface PortalOutlet {
/**
* 把Portal对应的内容放置在PortalOutlet
*/
attach(portal: Portal<any>): any;
/**
* D把PortalOutlet的内容移除掉
*/
detach(): any;
/**
* 在destroyed之前清理掉 PortalOutlet里面的内容,一帮我们自己很少调用
*/
dispose(): void;
/**
* 判断PortalOutlet里面是否attach了Portal
*/
hasAttached(): boolean;
}
/**
* BasePortalOutlet是一个抽象类,实现了PortalOutlet,话句话说BasePortalOutlet实现了ComponentPortal、 TemplatePortal的放置
* ComponentPortal and TemplatePortal.
*/
export declare abstract class BasePortalOutlet implements PortalOutlet {
protected _attachedPortal: Portal<any> | null;
private _disposeFn;
private _isDisposed;
/**
* 判断PortalOutlet里面是否attach了Portal
*/
hasAttached(): boolean;
/**
* 在PortalOutlet里面放置ComponentPortal
*/
attach<T>(portal: ComponentPortal<T>): ComponentRef<T>;
/**
* 在PortalOutlet里面放置TemplatePortal
*/
attach<T>(portal: TemplatePortal<T>): EmbeddedViewRef<T>;
attach(portal: any): any;
abstract attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T>;
abstract attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C>;
/**
* 把PortalOutlet的内容移除掉
*/
detach(): void;
/**
* 永久性的移除PortalOutlet的内容,在destroy之前调用
*/
dispose(): void;
/** @docs-private */
setDisposeFn(fn: () => void): void;
private _invokeDisposeFn;
}
/**
* Dom 形式下的PortalOutlet
*/
export declare class DomPortalOutlet extends BasePortalOutlet {
/**
* PortalOutlet 对应的Element,Portal的内容将会添加在这个Element下面
*/
outletElement: Element;
private _componentFactoryResolver;
private _appRef;
private _defaultInjector;
/**
*
* @param outletElement: PortalOutlet对应的节点Element,Portal添加的位置
* @param _componentFactoryResolver:组件工厂解析器
* @param _appRef:变化检测工具类
* @param _defaultInjector:注入器,用于传递参数
*/
constructor(
/** Element into which the content is projected. */
outletElement: Element, _componentFactoryResolver: ComponentFactoryResolver, _appRef: ApplicationRef, _defaultInjector: Injector);
/**
* attach ComponentPortal
*/
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T>;
/**
* attach TemplatePortal
*/
attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C>;
/**
* 情况DomPortalOutlet下面所有的Portal
*/
dispose(): void;
/** Gets the root HTMLElement for an instantiated component. */
private _getComponentRootNode;
}
前面扯皮了一大堆,最终的目的都是为了明白怎么来使用Portals。怎么把动态内容显示出来。
在使用Portals之前我们先要import PortalModule。
...
import {PortalModule} from "@angular/cdk/portal";
...
@NgModule({
...
imports: [
...
PortalModule,
...
],
...
})
export class AppModule {
}
动态显示宿主视图(Host View),由Component提供,也就是我们常说的组件了。
第一步,我们肯定会有一个自定义的组件的。比如我们新建一个非常简单的组件PortalChildComponent,并且给组件传递一个参数,代码如下
这里特别提醒下,因为PortalChildComponent是动态组件,所以@NgModule里面除了declarations里面需要添加PortalChildComponent,entryComponents里面也需要添加PortalChildComponent
import {Component, EventEmitter, Inject, InjectionToken} from '@angular/core';
/**
* 用于动态创建PortalChildComponent的时候传递参数
*/
export const PORTAL_CHILD_DATA = new InjectionToken<any>('PORTAL_CHILD_DATA');
@Component({
selector: 'app-portal-child',
template: `
<h1>portal child show</h1>
<button (click)="onButtonClick()">点击</button>
`
})
export class PortalChildComponent {
outEvent: EventEmitter<string>;
/**
* 构造函数
* @param initData 创建组件的时候传递过来的参数(为了测试用了any类型,推荐根据业务使用特定的类型,尽量不要使用any)
*/
constructor(@Inject(PORTAL_CHILD_DATA) public initData: any) {
console.log(initData);
}
/**
* 用来测试把Portal里面的事件返回上去
*/
onButtonClick() {
if (this.outEvent != null) {
this.outEvent.emit('child 里面被点击了');
}
}
}
第二步,动态显示PortalChildComponent,这里我们新建一个PortalComponentComponent 组件,把PortalChildComponent动态的添加到PortalComponentComponent里面去。代码如下。(注意下是怎么传递参数的,传出参数,业务绝大部分的场景都是需要给动态组件传递参数的哦)
import {
ApplicationRef,
Component,
ComponentFactoryResolver, ComponentRef,
ElementRef, EventEmitter,
Injector, OnDestroy,
OnInit,
ViewContainerRef
} from '@angular/core';
import {ComponentPortal, DomPortalHost, PortalInjector} from '@angular/cdk/portal';
import {PORTAL_CHILD_DATA, PortalChildComponent} from '../portal-child-component/portal-child.component';
import {Subject} from "rxjs";
import {takeUntil} from "rxjs/operators";
@Component({
selector: 'app-portal-component',
template: ``
})
export class PortalComponentComponent implements OnInit, OnDestroy {
private portalHost: DomPortalHost;
private _$destroy = new Subject();
constructor(
private elementRef: ElementRef,
private injector: Injector,
private appRef: ApplicationRef,
private viewContainerRef: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver,
) {
}
ngOnInit() {
// 1. 创建DomPortalHost
this.portalHost = new DomPortalHost(
this.elementRef.nativeElement as HTMLElement,
this.componentFactoryResolver,
this.appRef,
this.injector
);
// injectionTokens用于传递参数,如果不想传递参数,直接const templatePortal = new ComponentPortal(PortalChildComponent) 就可以了
const injectionTokens = new WeakMap();
injectionTokens.set(PORTAL_CHILD_DATA, '构建组件传递的参数');
// 2. 创建ComponentPortal
const templatePortal = new ComponentPortal(PortalChildComponent
, this.viewContainerRef
, new PortalInjector(this.injector, injectionTokens)
, this.componentFactoryResolver);
// 3. ComponentPortal attach 到DomPortalHost里面去, 并且把ComponentPortal里面的时间返回上来
// 如果不需要传出参数,this.portalHost.attach(templatePortal); 就可以了
const portalComponentRef: ComponentRef<PortalChildComponent> = this.portalHost.attachComponentPortal(templatePortal);
// 处理返回回来的事件
const eventEmitter: EventEmitter<string> = new EventEmitter<string>();
portalComponentRef.instance.outEvent = eventEmitter;
eventEmitter.pipe(takeUntil(this._$destroy))
.subscribe((event: string) => this.handlerPortalEvent(event));
}
private handlerPortalEvent(event: string): void {
console.log('收到了Portal返回上来的事件信息:' + event);
}
ngOnDestroy(): void {
this._$destroy.next();
this._$destroy.complete();
}
}
稍稍总结下动态显示ComponentPortal比较重要的地方:
动态显示嵌入视图(Embedded View),由Template提供,和我们常说的ng-template标签对应。把ng-template的内容在指定的位置显示出来。下面的例子我们创建一个PortalTemplateComponent组件,然后把ng-template标签对应的内容在这个组件里面显示出来。
import {
ApplicationRef,
Component,
ComponentFactoryResolver,
ElementRef,
Injector,
OnInit,
TemplateRef,
ViewChild,
ViewContainerRef
} from '@angular/core';
import {DomPortalHost, TemplatePortal} from "@angular/cdk/portal";
@Component({
selector: 'app-portal-template',
template: `
<!-- 我们定义一个ng-template节点,并且需要传递一个参数 -->
<ng-template #portalTemplate let-data>
<div>参数: {{ data }}</div>
</ng-template>
`
})
export class PortalTemplateComponent implements OnInit {
@ViewChild('portalTemplate') testTemplate: TemplateRef<any>;
constructor(
private elementRef: ElementRef,
private injector: Injector,
private appRef: ApplicationRef,
private viewContainerRef: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver,
) {
}
ngOnInit() {
// 1. DomPortalHost
const portalHost = new DomPortalHost(
this.elementRef.nativeElement as HTMLElement,
this.componentFactoryResolver,
this.appRef,
this.injector
);
// 2. TemplatePortal
const templatePortal = new TemplatePortal(
this.testTemplate,
this.viewContainerRef,
{
$implicit: "我是传递进来的数据",
}
);
// 3. attach
portalHost.attach(templatePortal);
}
}
动态显示TemplatePortal的重点估计也是怎么传递参数了。其他的应该都不难。
模板语法中某个标签使用了CdkPortal指令,可以简单的认为这个标签对应的元素就已经被封装成TemplatePortal了。
添加了CdkPortal指令的元素是不会在页面中显示出来的,除非你给CdkPortal指定了CdkPortalOutlet的位置。即使是div元素你给添加了*cdkPortal也是不会显示出来的。
比如我们有如下的代码,ng-templat和div都添加了CdkPortal指令。如果我们不做任何处理,他们是不会显示的。
<!-- #divPortal="cdkPortal",CdkPortal指令有exportAs: 'cdkPortal'元数据,所以我们才可以这么写来获取,来获取CdkPortal对象 -->
<ng-template #divPortal="cdkPortal" cdkPortal let-obj let-location="location">
<h2>ng-template 指定的内容(first) 外部参数 {{obj.age}}</h2>
</ng-template>
<!-- 不建议在div上添加*cdkPortal指令,完全可以用ng-template代替 -->
<div *cdkPortal>
<h2>ng-template 指定的内容(last)</h2>
</div>
接下来得给他们指定一个位置让他们显示出来。在显示之前,咱们得在对应的ts文件里面得到cdkPortal指令对应的TemplatePortal。使用@ViewChildren,@ViewChild找到他们。
// 获取到对应html里面所有添加了cdkPortal指令的元素的TemplatePortal
@ViewChildren(TemplatePortalDirective) templatePortals: QueryList<TemplatePortal<any>>;
// 获取单个的cdkPortal指令的元素的TemplatePortal 【#templatePortal="cdkPortal"】
@ViewChild('templatePortal') divTemplatePortal: TemplatePortal<any>;
CdkPortal指令对应的视图内容(嵌入视图)已经自动帮我们封装成了TemplatePortal,我们也已经拿到了这些TemplatePortal,剩下的就是在什么位置显示他们了。有两种方式显示他们,第一种自己new DomPortalHost来attach,这个我们上面的例子中有实际的例子、第二种通过CdkPortalOutlet指令来显示。这个也是我们下面要讲到的内容。
CdkPortalOutlet指令。表示添加了该指令的元素是一个PortalOutlet,可以在他上面添加Portal元素。
比如我们有如下的代码,我们用一段最简单的代码来看看CdkPortalOutlet和CdkPortal两个指令怎么配合起来一起使用。
import {Component, ViewChild} from '@angular/core';
import {TemplatePortal} from '@angular/cdk/portal';
@Component({
selector: 'app-cdk-portal',
template: `
<!-- Portal显示的位置 -->
<div class="demo-portal-host">
<!-- cdkPortalOutlet来指定动态内容需要放置的地方,参数是selectedPortal他是一个ComponentPortal或者TemplatePortal
显示的内容会根据selectedPortal的改变而改变-->
<ng-template cdkPortalHost [cdkPortalOutlet]="templatePortal" (attached)="onPortalAttached()"></ng-template>
</div>
<!-- #divPortal="cdkPortal",CdkPortal指令有exportAs: 'cdkPortal'元数据,所以我们才可以这么写来获取,来获取CdkPortal对象 -->
<ng-template #templatePortal="cdkPortal" cdkPortal>
<h2> cdkPortalHost cdkPortal 配合使用动态显示</h2>
</ng-template>
`,
styleUrls: ['./cdk-portal.component.less']
})
export class CdkPortalComponent {
// cdkPortal指令对应的Portal
@ViewChild('templatePortal') templatePortal: TemplatePortal<any>;
onPortalAttached() {
console.log('PortalOutlet 有元素attach上来了');
}
}
总结下cdk Portals的内容,cdk Portals整个的都是在围绕Portal【Portal有两种ComponentPortal、TemplatePortal 】和PortalOutlet【DomPortalOutlet】。都是在想办法new Portal 和 PortalOutlet。然后再把他们两attach起来。
关于cdk Portals咱们就讲这么多,如果大家有什么疑问,欢迎大家提问。