美文网首页
Angular cdk 学习之 Portals

Angular cdk 学习之 Portals

作者: tuacy | 来源:发表于2018-12-09 13:28 被阅读158次

           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 | undefined ng-template需要传递的参数,可以参考ngTemplateOutletContext的用法
    isAttached: boolean 是否attach到节点里面去了
    templateRef: TemplateRef<C> 嵌入视图
    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<any> | null @Input(cdkPortalOutlet) CdkPortalOutlet位置attach的Portal
    attached: EventEmitter<CdkPortalOutletAttachedRef> @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咱们就讲这么多,如果大家有什么疑问,欢迎大家提问。

    相关文章

      网友评论

          本文标题:Angular cdk 学习之 Portals

          本文链接:https://www.haomeiwen.com/subject/noeqhqtx.html