当前位置: 首页 > 工具软件 > cdk-clj > 使用案例 >

Angular cdk 学习之 Portals

冯和硕
2023-12-01

       CDK里面Portals模块的功能就是将动态内容(这个内容可以是Component也可以是一个TemplateRef)呈现到应用程序中。更加直接点的解释就是把Portal放到指定位置(我们把他叫做插槽PortalOutlet)。Portal有两个子类ComponentPortal(对应组件)或者TemplatePortal(对应TemplateRef)。

Portal:表示内容。PortalOutlet: 放置内容的位置。

一 Portals 提供的指令

1.1 CdkPortal extends TemplatePortal

       Selector: [cdk-portal] [cdkPortal] [portal]

       Exported as: cdkPortal

       CdkPortal指令继承自TemplatePortal,CdkPortal指令代表当前元素是一个Portal,这个Portal是可以放置到PortalOutlet里面去的。

       注意:CdkPortal指令没有提供@Input()和@Output()。

1.1.1 CdkPortal指令里面的属性(其实就是TemplatePortal的属性)

CdkPortal属性解释
context: C | undefinedng-template需要传递的参数,可以参考ngTemplateOutletContext的用法
isAttached: boolean是否attach到节点里面去了
templateRef: TemplateRef嵌入视图
viewContainerRef: ViewContainerRef视图容器(内部会通过上下文拿到),内部应该是要通过视图容器来插入元素

1.1.2 CdkPortal指令里面的方法

/**
 * 把CdkPortal对应的Portal attach 到 PortalOutlet里面去(把组件显示出来)
 * @Param host PortalOutlet
 * @Param context ng-template需要传入的内容
 */
attach(host: PortalOutlet, context?: C | undefined): C;

/**
 * 把CdkPortal对应的Portal从PortalOutlet detach移除掉
 */
detach(): void;

1.2 CdkPortalOutlet extends BasePortalOutlet

1.2.1 CdkPortalOutlet指令里面的属性

属性解释
portal: Portal | null@Input(cdkPortalOutlet)CdkPortalOutlet位置attach的Portal
attached: EventEmitter@Output()当有Portal attach到CdkPortalOutlet位置的时候会回调
attachedRef: CdkPortalOutletAttachedRef已经attach的宿主视图或者嵌入视图(ComponentRef-Component提供|EmbeddedViewRef-Template提供)

1.2.2 CdkPortalOutlet指令里面的方法

/** 是否有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;

二 Portals 提供的类或者接口

       大概看下cdk Portals模块里面的类,主要也是为了了解下类里面一些属性代表啥意思。

2.1 Portal类

       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;
}

2.2 ComponentPortal类 extends Portal

       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);
}

2.3 TemplatePortal类 extends Portal

       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;
}

2.4 PortalOutlet接口

       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;
}

2.5 BasePortalOutlet类 implements PortalOutlet


/**
 * 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;
}

2.6 DomPortalOutlet类 extends BasePortalOutlet

/**
* 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。怎么把动态内容显示出来。

       在使用Portals之前我们先要import PortalModule。

...
import {PortalModule} from "@angular/cdk/portal";
...

@NgModule({
    ...
    imports: [
        ...
        PortalModule,    
        ...
    ],
    ...
})
export class AppModule {
}

3.1 动态显示ComponentPortal

       动态显示宿主视图(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比较重要的地方:

  • 第一动态组件需要在declarations和entryComponents里面申明。
  • 第二动态组件传入参数。(@Input())
  • 第三动态组件传出参数。(@Output())

3.2 动态显示TemplatePortal

       动态显示嵌入视图(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的重点估计也是怎么传递参数了。其他的应该都不难。

3.3 CdkPortal指令的使用

       模板语法中某个标签使用了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找到他们。

  • 通过@ViewChildren获取CdkPortal,@ViewChildren selector参数是TemplatePortalDirective,这样我们就拿到了当前html里面所有添加了CdkPortal指令的对象的TemplatePortal。
    // 获取到对应html里面所有添加了cdkPortal指令的元素的TemplatePortal
    @ViewChildren(TemplatePortalDirective) templatePortals: QueryList<TemplatePortal<any>>;
  • 通过@ViewChild获取指定添加了CdkPortal指令的TemplatePortal,html模板语法里面对应CdkPortal指令标签需要写上#templatePortal=“cdkPortal”
    // 获取单个的cdkPortal指令的元素的TemplatePortal 【#templatePortal="cdkPortal"】
    @ViewChild('templatePortal') divTemplatePortal: TemplatePortal<any>;

       CdkPortal指令对应的视图内容(嵌入视图)已经自动帮我们封装成了TemplatePortal,我们也已经拿到了这些TemplatePortal,剩下的就是在什么位置显示他们了。有两种方式显示他们,第一种自己new DomPortalHost来attach,这个我们上面的例子中有实际的例子、第二种通过CdkPortalOutlet指令来显示。这个也是我们下面要讲到的内容。

3.4 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咱们就讲这么多,如果大家有什么疑问,欢迎大家提问。

 类似资料: