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


Posted in Javascript onApril 25, 2017

动态创建组件

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

定义 AlertComponent 组件

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

exe-alert.component.ts

import { Component, Input } from '@angular/core';

@Component({
  selector: "exe-alert",
  template: `
   <h1>Alert {{type}}</h1>
  `,
})
export class AlertComponent {
  @Input() type: string = "success";
}

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

创建组件容器

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

app.component.ts

import { Component } from '@angular/core';

@Component({
 selector: 'exe-app',
 template: `
  <ng-template #alertContainer></ng-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: `
  <ng-template #alertContainer></ng-template>
 `
})
export class AppComponent {
 @ViewChild("alertContainer", { read: ViewContainerRef }) container: ViewContainerRef;
}

动态创建组件

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

import { Component, ViewChild, ViewContainerRef } from '@angular/core';

@Component({
 selector: 'exe-app',
 template: `
  <ng-template #alertContainer></ng-template>
  <button (click)="createComponent('success')">Create success alert</button>
  <button (click)="createComponent('danger')">Create danger alert</button>
 `
})
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<T>(component: Type<T>): ComponentFactory<T>;
}

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

constructor(private resolver: ComponentFactoryResolver) {}

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

export abstract class ComponentFactory<C> {
 abstract get selector(): string;
 abstract get componentType(): Type<any>;
 
 // selector for all <ng-content> 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<any>): ComponentRef<C>;
}

通过观察 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: `
   <h1 (click)="output.next(type)">Alert {{type}}</h1>
  `,
})
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: `
  <ng-template #alertContainer></ng-template>
  <button (click)="createComponent('success')">Create success alert</button>
  <button (click)="createComponent('danger')">Create danger alert</button>
 `
})
export class AppComponent implements OnDestroy {
 componentRef: ComponentRef<AlertComponent>;

 @ViewChild("alertContainer", { read: ViewContainerRef }) container: ViewContainerRef;

 constructor(private resolver: ComponentFactoryResolver) { }

 createComponent(type: string) {
  this.container.clear();
  const factory: ComponentFactory<AlertComponent> =
   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 对象中

我有话说

<ng-template> <ng-container> 有什么区别?

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

<section *ngIf="show">
 <div>
  <h2>Div one</h2>
 </div>
 <div>
  <h2>Div two</h2>
 </div>
</section>

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

<ng-template [ngIf]="show">
 <div>
  <h2>Div one</h2>
 </div>
 <div>
  <h2>Div two</h2>
 </div>
</ng-template>

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

<ng-container>

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

<ng-container *ngIf="show">
 <div>
  <h2>Div one</h2>
 </div>
 
 <div>
  <h2>Div two</h2>
 </div>
 </ng-container>

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

<div [ngSwitch]="value">
 <span *ngSwitchCase="0">Text one</span>
 <span *ngSwitchCase="1">Text two</span>
</div>

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

<div [ngSwitch]="value">
 <ng-container *ngSwitchCase="0">Text one</ng-container>
 <ng-container *ngSwitchCase="1">Text two</ng-container>
</div>

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

<ng-template>
  <p> In template, no attributes. </p>
</ng-template>

<ng-container>
  <p> In ng-container, no attributes. </p>
</ng-container>

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

In ng-container, no attributes.

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

<template [ngIf]="true">
  <p> ngIf with a template.</p>
</template>

<ng-container *ngIf="true">
  <p> ngIf with an ng-container.</p>
</ng-container>

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

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

现在我们来总结一下 <ng-template> <ng-container> 的区别:

  1. <ng-template> :使用 * 语法糖的结构指令,最终都会转换为 <ng-template> 或 <template> 模板指令,模板内的内容如果不进行处理,是不会在页面中显示的。
  2. <ng-container>:是一个逻辑容器,可用于对节点进行分组,但不作为 DOM 树中的节点,它将被渲染为 HTML中的 comment 元素,它可用于避免添加额外的元素来使用结构指令。

最后再来看一个 <ng-container> 的使用示例:

模板定义

<div>
 <ng-container *ngIf="true">
   <h2>Title</h2>
   <div>Content</div>
  </ng-container>
</div>

渲染结果

<div>
  <!--bindings={
 "ng-reflect-ng-if": "true"
  }--><!---->
  <h2>Title</h2>
  <div>Content</div>
</div>

TemplateRef 与 ViewContainerRef 有什么作用?

TemplateRef

用于表示内嵌的 template 模板元素,通过 TemplateRef 实例,我们可以方便创建内嵌视图(Embedded Views),且可以轻松地访问到通过 ElementRef 封装后的 nativeElement。需要注意的是组件视图中的 template 模板元素,经过渲染后会被替换成 comment 元素。

ViewContainerRef

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

ViewChild 装饰器还支持哪些查询条件?

ViewChild 装饰器用于获取模板视图中的元素,它支持 Type 类型或 string 类型的选择器,同时支持设置 read 查询条件,以获取不同类型的实例。

export interface ViewChildDecorator {
 // Type类型:@ViewChild(ChildComponent)
 // string类型:@ViewChild('tpl', { read: ViewContainerRef })
 (selector: Type<any>|Function|string, {read}?: {read?: any}): any;

 new (selector: Type<any>|Function|string, 
   {read}?: {read?: any}): ViewChild;
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
java和javascript获取word文档的书签位置对比
Jun 19 Javascript
javascript结合Canvas 实现简易的圆形时钟
Mar 11 Javascript
jquery 中toggle的2种用法详解(推荐)
Sep 02 Javascript
Javascript获取background属性中url的值
Oct 17 Javascript
前端开发必知的15个jQuery小技巧
Jan 22 Javascript
JavaScript组件开发之输入框加候选框
Mar 10 Javascript
js实现图片放大展示效果
Aug 30 Javascript
微信小程序 高德地图路线规划实现过程详解
Aug 05 Javascript
Vue 中可以定义组件模版的几种方式
Aug 06 Javascript
vue2 拖动排序 vuedraggable组件的实现
Aug 08 Javascript
jquery使用echarts实现有向图可视化功能示例
Nov 25 jQuery
解决vue页面渲染但dom没渲染的操作
Jul 27 Javascript
Angular 4.x中表单Reactive Forms详解
Apr 25 #Javascript
Angular 4.x 动态创建表单实例
Apr 25 #Javascript
AngularJS动态菜单操作指令
Apr 25 #Javascript
Angular.js 4.x中表单Template-Driven Forms详解
Apr 25 #Javascript
详解JS中的attribute属性
Apr 25 #Javascript
node.js中debug模块的简单介绍与使用
Apr 25 #Javascript
Node.js利用debug模块打印出调试日志的方法
Apr 25 #Javascript
You might like
深入php list()函数的详解
2013/06/05 PHP
ThinkPHP模型详解
2015/07/27 PHP
PHP + plupload.js实现多图上传并显示进度条加删除实例代码
2017/03/06 PHP
PHP设计模式之抽象工厂模式实例分析
2019/03/25 PHP
使用GruntJS构建Web程序之合并压缩篇
2014/06/06 Javascript
JS对象与json字符串格式转换实例
2014/10/28 Javascript
JavaScript中的关联数组问题
2015/03/04 Javascript
JavaScript中字符串分割函数split用法实例
2015/04/07 Javascript
jQuery实现切换页面过渡动画效果
2015/10/29 Javascript
Angular2 环境配置详细介绍
2016/09/21 Javascript
微信小程序 wxapp内容组件 icon详细介绍
2016/10/31 Javascript
javascript 取小数点后几位几种方法总结
2017/08/02 Javascript
node跨域请求方法小结
2017/08/25 Javascript
Vue2.0用 watch 观察 prop 变化(不触发)
2017/09/08 Javascript
highCharts提示框中显示当前时间的方法
2019/01/18 Javascript
微信小程序页面间跳转传参方式总结
2019/06/13 Javascript
Vue实现购物车实例代码两则
2020/05/30 Javascript
eslint+prettier统一代码风格的实现方法
2020/07/22 Javascript
[02:36]DOTA2英雄基础教程 帕格纳
2014/01/20 DOTA
利用python实现简单的循环购物车功能示例代码
2017/07/05 Python
使用python实现快速搭建简易的FTP服务器
2018/09/12 Python
Python 一键制作微信好友图片墙的方法
2019/05/16 Python
python 使用plt画图,去除图片四周的白边方法
2019/07/09 Python
Python利用逻辑回归分类实现模板
2020/02/15 Python
索引覆盖(Index Covering)查询含义
2012/02/18 面试题
电子商务专业自我鉴定
2013/12/18 职场文书
幼儿园家长会邀请函
2014/01/15 职场文书
法定代表人资格证明书
2014/09/11 职场文书
2014年党员评议表自我评价
2014/09/27 职场文书
2014年乡镇团委工作总结
2014/12/18 职场文书
消防安全培训工作总结
2015/10/23 职场文书
导游词之珠海轮廓
2019/10/25 职场文书
canvas多重阴影发光效果实现
2021/04/20 Javascript
python爬虫请求库httpx和parsel解析库的使用测评
2021/05/10 Python
Python实现滑雪小游戏
2021/09/25 Python
解决 Redis 秒杀超卖场景的高并发
2022/04/12 Redis