摘要
在angular开发中经常需要根据不同的情况动态显示一些组件,ngIf,ngSwitch能很快的解决其中一些问题,但当动态显示的组件不确定时,我们就需要去动态创建组件。angular动态创建组件主要分为2种,一种通过angular的ViewContainerRef类去创建组件或比组件更小的单元视图(Template);另一种是通过angular-cdk来动态创建视图。相比ViewContainerRef,angular-cdk能提供一些更强大的功能如位置策略,能让开发者更灵活的去控制创建出来的组件的位置。
介绍
本文主要介绍2种Angular动态创建组件的方法。
第一种是通过ViewContainerRef类的createComponent()创建组件,通过createEmbededView()创建Template。
第二种是用angular-cdk的Portal,Portal可以被理解为一个视图单元,可以是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>
运行结果:
-
我们可以发现创建出来的组件在模版元素外面,因为这个容器是附着在宿主元素上,并不是在宿主元素里面,而且这个容器没有标签,即所有的都是在一个虚拟的容器里。
-
最新创建的组件的在最下面,因为我们没有指定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实例上
我们可以看到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连接点,并把这两个点重叠,那下拉框相对于被点击元素的位置就可以确定下来了。
创建灵活连接点位置策略的代码:
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类的组件推荐使用灵活的连接点位置策略。