详解Angular 4.x 动态创建组件

详解Angular 4.x 动态创建组件

本篇文章主要介绍了详解Angular 4.x 动态创建组件,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

动态创建组件

这篇文章我们将介绍在 Angular 中如何动态创建组件。

定义 AlertComponent 组件

首先,我们需要定义一个组件。

exe-alert.component.ts

 import { Component, Input } from '@angular/core'; @Component({ selector: "exe-alert", template: ` 

Alert {{type}}

`, }) export class AlertComponent { @Input() type: string = "success"; }

上面代码中,我们定义了一个简单的 alert 组件,该组件有一个输入属性 type ,用于让用户自定义提示的类型。我们的自定义组件最终是一个实际的 DOM 元素,因此如果我们需要在页面中插入该元素,我们就需要考虑在哪里放置该元素。

创建组件容器

在 Angular 中放置组件的地方称为 container 容器。接下来,我们将在 exe-app 组件中创建一个模板元素,此外我们使用模板变量的语法,声明一个模板变量。接下来模板元素 将会作为我们的组件容器,具体示例如下:

app.component.ts

 import { Component } from '@angular/core'; @Component({ selector: 'exe-app', template: `  ` }) export class AppComponent { } 

友情提示:容器可以是任意的 DOM 元素或组件。

在 AppComponent 组件中,我们可以通过 ViewChild 装饰器来获取视图中的模板元素,如果没有指定第二个查询参数,则默认返回的组件实例或相应的 DOM 元素,但这个示例中,我们需要获取 ViewContainerRef 实例。

ViewContainerRef 用于表示一个视图容器,可添加一个或多个视图。通过 ViewContainerRef 实例,我们可以基于 TemplateRef 实例创建内嵌视图,并能指定内嵌视图的插入位置,也可以方便对视图容器中已有的视图进行管理。简而言之,ViewContainerRef 的主要作用是创建和管理内嵌视图或组件视图。

根据以上需求,更新后的代码如下:

 import { Component, ViewChild, ViewContainerRef } from '@angular/core'; @Component({ selector: 'exe-app', template: `  ` }) export class AppComponent { @ViewChild("alertContainer", { read: ViewContainerRef }) container: ViewContainerRef; } 

动态创建组件

接下来,在 AppComponent 组件中,我们来添加两个按钮,用于创建 AlertComponent 组件。

 import { Component, ViewChild, ViewContainerRef } from '@angular/core'; @Component({ selector: 'exe-app', template: `  ` }) export class AppComponent { @ViewChild("alertContainer", { read: ViewContainerRef }) container: ViewContainerRef; } 

在我们定义 createComponent() 方法前,我们需要注入 ComponentFactoryResolver 服务对象。该 ComponentFactoryResolver 服务对象中,提供了一个很重要的方法 - resolveComponentFactory() ,该方法接收一个组件类作为参数,并返回 ComponentFactory

ComponentFactoryResolver 抽象类:

 export abstract class ComponentFactoryResolver { static NULL: ComponentFactoryResolver = new _NullComponentFactoryResolver(); abstract resolveComponentFactory(component: Type): ComponentFactory; }

在 AppComponent 组件构造函数中,注入 ComponentFactoryResolver 服务:

 constructor(private resolver: ComponentFactoryResolver) {}

接下来我们再来看一下 ComponentFactory 抽象类:

 export abstract class ComponentFactory { abstract get selector(): string; abstract get componentType(): Type; // selector for all  elements in the component. abstract get ngContentSelectors(): string[]; // the inputs of the component. abstract get inputs(): {propName: string, templateName: string}[]; // the outputs of the component. abstract get outputs(): {propName: string, templateName: string}[]; // Creates a new component. abstract create( injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string|any, ngModule?: NgModuleRef): ComponentRef; } 

通过观察 ComponentFactory 抽象类,我们知道可以通过调用 ComponentFactory 实例的 create() 方法,来创建组件。介绍完上面的知识,我们来实现 AppComponent 组件的 createComponent() 方法:

 createComponent(type) { this.container.clear(); const factory: ComponentFactory = this.resolver.resolveComponentFactory(AlertComponent); this.componentRef: ComponentRef = this.container.createComponent(factory); }

接下来我们来分段解释一下上面的代码。

 this.container.clear();

每次我们需要创建组件时,我们需要删除之前的视图,否则组件容器中会出现多个视图 (如果允许多个组件的话,就不需要执行清除操作 )。

复制代码 代码如下:

const factory: ComponentFactory = this.resolver.resolveComponentFactory(AlertComponent);

正如我们之前所说的,resolveComponentFactory() 方法接受一个组件并返回如何创建组件的 ComponentFactory 实例。

复制代码 代码如下:

this.componentRef: ComponentRef = this.container.createComponent(factory);

在上面代码中,我们调用容器的 createComponent() 方法,该方法内部将调用 ComponentFactory 实例的 create() 方法创建对应的组件,并将组件添加到我们的容器。

现在我们已经能获取新组件的引用,即可以我们可以设置组件的输入类型:

 this.componentRef.instance.type = type;

同样我们也可以订阅组件的输出属性,具体如下:

 this.componentRef.instance.output.subscribe(event => console.log(event));

另外不能忘记销毁组件:

 ngOnDestroy() { this.componentRef.destroy(); }

最后我们需要将动态组件添加到 NgModule 的 entryComponents 属性中:

 @NgModule({ ..., declarations: [AppComponent, AlertComponent], bootstrap: [AppComponent], entryComponents: [AlertComponent], }) export class AppModule { }

完整示例

exe-alert.component.ts

 import { Component, Input, Output, EventEmitter } from '@angular/core'; @Component({ selector: "exe-alert", template: ` 

Alert {{type}}

`, }) export class AlertComponent { @Input() type: string = "success"; @Output() output = new EventEmitter(); }

app.component.ts

 import { Component, ViewChild, ViewContainerRef, ComponentFactory, ComponentRef, ComponentFactoryResolver, OnDestroy } from '@angular/core'; import { AlertComponent } from './exe-alert.component'; @Component({ selector: 'exe-app', template: `  ` }) export class AppComponent implements OnDestroy { componentRef: ComponentRef; @ViewChild("alertContainer", { read: ViewContainerRef }) container: ViewContainerRef; constructor(private resolver: ComponentFactoryResolver) { } createComponent(type: string) { this.container.clear(); const factory: ComponentFactory = this.resolver.resolveComponentFactory(AlertComponent); this.componentRef = this.container.createComponent(factory); this.componentRef.instance.type = type; this.componentRef.instance.output.subscribe((msg: string) => console.log(msg)); } ngOnDestroy() { this.componentRef.destroy() } } 

app.module.ts

 import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { AlertComponent } from './exe-alert.component'; @NgModule({ imports: [BrowserModule], declarations: [AppComponent, AlertComponent], bootstrap: [AppComponent], entryComponents: [AlertComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class AppModule { } 

总结

  • 获取装载动态组件的容器

  • 在组件类的构造函数中,注入 ComponentFactoryResolver 对象

  • 调用 ComponentFactoryResolver 对象的 resolveComponentFactory() 方法创建 ComponentFactory 对象

  • 调用组件容器对象的 createComponent() 方法创建组件并自动添加动态组件到组件容器中

  • 基于返回的 ComponentRef 组件实例,配置组件相关属性 (可选)

  • 在模块 Metadata 对象的 entryComponents 属性中添加动态组件

    • declarations - 用于指定属于该模块的指令和管道列表

    • entryComponents - 用于指定在模块定义时,需要编译的组件列表。对于列表中声明的每个组件,Angular 将会创建对应的一个 ComponentFactory 对象,并将其存储在 ComponentFactoryResolver 对象中

我有话说

有什么区别?

通常情况下,当我们使用结构指令时,我们需要添加额外的标签来封装内容,如使用 *ngIf 指令:

 

Div one

Div two

上面示例中,我们在 section 标签上应用了 ngIf 指令,从而实现 section 标签内容的动态显示。这种方式有个问题是,我们必须添加额外的 DOM 元素。要解决该问题,我们可以使用 的标准语法 (非*ngIf语法糖):

  

Div one

Div two

问题是解决了但我们不再使用 * 语法糖语法,这样会导致我们代码的不统一。虽然解决了问题,但又带来了新问题。那我们还有其它的方案么?答案是有的,我们可以使用 ng-container 指令。

是一个逻辑容器,可用于对节点进行分组,但不作为 DOM 树中的节点,它将被渲染为 HTML中的 comment 元素。使用 的示例如下:

  

Div one

Div two

有时我们需要根据 switch 语句,动态显示文本,这时我们需要添加一个额外的标签如 ,具体示例如下:

 
Text oneText two

针对这种情况,理论上我们是不需要添加额外的 标签,这时我们可以使用 ng-container 来解决这个问题:

 
Text oneText two

介绍完 ng-container 指令,我们来分析一下它跟 ng-template 指令有什么区别?我们先看以下示例:

  

In template, no attributes.

In ng-container, no attributes.

以上代码运行后,浏览器中输出结果是:

In ng-container, no attributes.

中的内容不会显示。当在上面的模板中添加 ngIf 指令:

  

ngIf with an ng-container.

以上代码运行后,浏览器中输出结果是:

ngIf with a template.
ngIf with an ng-container.

现在我们来总结一下 的区别:

  1. :使用 * 语法糖的结构指令,最终都会转换为