Angular动态创建组件

2,938 阅读5分钟

摘要

在angular开发中经常需要根据不同的情况动态显示一些组件,ngIf,ngSwitch能很快的解决其中一些问题,但当动态显示的组件不确定时,我们就需要去动态创建组件。angular动态创建组件主要分为2种,一种通过angular的ViewContainerRef类去创建组件或比组件更小的单元视图(Template);另一种是通过angular-cdk来动态创建视图。相比ViewContainerRef,angular-cdk能提供一些更强大的功能如位置策略,能让开发者更灵活的去控制创建出来的组件的位置。

介绍

本文主要介绍2种Angular动态创建组件的方法。

第一种是通过ViewContainerRef类的createComponent()创建组件,通过createEmbededView()创建Template。

第二种是用angular-cdk的PortalPortal可以被理解为一个视图单元,可以是component也可以是template,而PortalOutlet则是装载Portal的容器;同时我们还可以使用cdk里的Overlay模块让动态视图元素处在之外的一个图层里,避免出现事件冒泡带来的副作用。

Ovelay的位置策略也能自定义视图的位置,实际上Overlay的实例就是一个PortalOutlet,这意味着我们可以在Overlay这个图层上挂载Portal,实际例子如在angular中动态创建弹框。

一、使用ViewContainerRef动态创建组件

ViewContainerRef表示可以将一个或多个视图附着到组件中的容器

它主要有2个方法:

1. 动态创建Template使用createEmbeddedView()

ViewContainerRef.createEmbeddedView<any>(templateRef: TemplateRef<any>, context?: any, index?: number): EmbeddedViewRef<any> 
// templateRef是要附着的template实例 
// context表示上下文 也就是传参, 在template中可通过let-关键词拿到该传参 
// index表示在容器中的索引位置

实际上*ngIf等结构性指令就是通过这个方法去控制视图显示或不显示,我们可以用这个方法实现一个ngUnless指令,值为true就不显示,值为false就是显示。

import { Directive, Input, TemplateRef, ViewContainerRef } from `@angular/core`; 
@Directive({ selector: `[ngUnless]`}) 
export class UnlessDirective { 
    private hasView = false; 
    constructor( 
        private templateRef: TemplateRef<any>, 
        private viewContainer: ViewContainerRef
    ) { } 
    @Input() set ngUnless(condition: boolean) { 
        if (!condition && !this.hasView) { 
            this.viewContainer.createEmbeddedView(this.templateRef); // 将注入的视图放进容器中 
            this.hasView = true; 
        } else if (condition && this.hasView) { 
            this.viewContainer.clear(); // 清空容器 
            this.hasView = false; 
        } 
    } 
} 
//使用 
<div ngUnless=`isHide`>测试</div>

2. 动态创建Component使用createComponent()

ViewContainerRef.createComponent(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, ...): ComponentRef<...> 
// componentFactory 传入一个要动态创建的组件对应的组件工厂,最新的angular13是直接传该组件 
// index 表示在容器中的索引位置 
// injector 注入器,在注入器中可注入服务,达到传值等效果

创建一个组件首先需要确定容器的位置,上面的例子是指令的形式,容器自然就是在指令所在元素的位置,我们还可以通过模版元素确定容器的位置:

export class NoCdkDemoComponent implements OnInit { 
    @ViewChild(`container`, {read: ViewContainerRef}) container: ViewContainerRef; 
    constructor( 
        private resolver: ComponentFactoryResolver
    ){} 
    
    createComponent(){ 
        // 创建组件工厂, TimeComponent为要动态创建的组件 
        const factory: ComponentFactory<TimeComponent> = this.resolver.resolveComponentFactory(TimeComponent); 
        this.componentRef = this.container.createComponent(factory); 
        this.componentRef.instance.time = new Date(); // 向组件传参 
    } 
} 

html: 
<div #container>我是Container</div> 
<button (click)=`createComponent()`>创建组件</button> 
time组件: 
<div>我是time组件:{{time | date: `HH:mm:ss`}}</div>

运行结果:

download (1).png

  1. 我们可以发现创建出来的组件在模版元素外面,因为这个容器是附着在宿主元素上,并不是在宿主元素里面,而且这个容器没有标签,即所有的都是在一个虚拟的容器里。

  2. 最新创建的组件的在最下面,因为我们没有指定index,所以默认就在容器最后一项了。

二、使用Anguar-cdk动态创建组件

1. Angular-cdk的Portal其实就是一个视图单元,基本使用如下

// .ts 
import { PortalModule } from `@angular/cdk/portal`; // 在ngModule中导入Portal模块 
const injector = Injector.create({ 
    // 通过注入器传参 
    providers: [{ provide: TimeToken, useValue: {time: new Date()}}] 
});
this.myPortal = new ComponentPortal(TimeComponent, null, Injector); // 创建portal 

// .html
<ng-template [cdkPortalOutlet]=`myPortal`></ng-template> // 将myPortal挂载到PortalOutlet上 

// time-component 
time: Date; 
constructor( 
    private timeToken: TimeToken 
){ 
    this.time = timeToken.time; // 获取注入器传入的参数 
} 

// TimeValue注入令牌 
export class TimeToken{ time: Date }

2. 使用Overlay去挂载Portal

import { OverlayModule } from `@angular/cdk/overlay`; // 在ngModule中导入Overlay模块 
...... 
const overlayRef = this.overlay.create(); // 创建Overlay实例,overlay实例上就是一个PortalOutlet 
const myPortal = new ComponentPortal(TimeComponent, null, Injector); // 创建portal 
overlayRef.attach(portal); // 将portal挂载到Ovelay实例上

202231518749.png

我们可以看到Time组件被创建在了之外的cdk-overlay-conatiner图层上。

3. 使用overlay位置策略

既然Overlay是一个独立出来的图层,那怎么控制portal在这个图层的位置呢?

Overlay提供了3种位置策略: GlobalPositionStrategy, ConnectedPositionStrategy, FlexibleConnectedPositionStrategy。

a.GlobalPositionStrategy 全局位置策略

这个全局位置有点类似于css里的fix布局,常用于页面的全局提示与反馈。比如我要把弹框放在离浏览器顶部200像素水平居中的地方:

// 创建位置策略 
const positionStrategy = this.overlay.position().global().top(`200px`).centerHorizontally(); 
// 创建Overlay实例 
this.overlayRef = this.overlay.create({ positionStrategy }); 
// 创建弹框组件portal 
const portal = new ComponentPortal(ModalComponent, this.viewContainerRef);
this.overlayRef.attach(portal);

b. FlexibleConnectedPositionStrategy 灵活连接点位置策略

当我们要动态创建下拉框等popover式的组件时,下拉框通常要和被点击元素挨在一起,可能在被点击元素的上下左右,如果我们在被点击元素上找个点,称之为Origin, 下拉框上找一个点,称之为Overlay连接点,并把这两个点重叠,那下拉框相对于被点击元素的位置就可以确定下来了。

202231518722.png

创建灵活连接点位置策略的代码:

const positionStrategy = this.overlay.position().flexibleConnectedTo(this.overlayOrigin.elementRef) 
    .withPositions([{ 
        originX: `start`, 
        originY: `bottom`, 
        overlayX: `start`, 
        overlayY: `top`
    },{ 
        originX: `start`, 
        originY: `top`,
        overlayX: `start`, 
        overlayY: `bottom`, 
        panelClass: `red` // 可以给动态创建的组件所在容器加类名 
}]);

withPositions传入了一个position数组,数组里越靠前的position优先级越高,当优先级高的位置会导致元素放不下(如被浏览器遮挡)时,就会使用优先级低一些的position。

三、总结

不使用cdk时,我们可以通过ViewContainerRef的createComponent方法创建组件,和它的createEmbededView方法创建Template。

使用cdk时,portal为视图单元,portal可以分为ComponentPortal和TemplatePortal。可以使用Overlay去挂载这些Portal, 使之创建在之外,也可以使用Overlay的灵活位置策略去定义Portal的位置,全局反馈类的组件推荐使用全局位置策略,popover类的组件推荐使用灵活的连接点位置策略