详解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 相关文章推荐
日期 时间js控件
May 07 Javascript
CSS+Jquery实现页面圆角框方法大全
Dec 24 Javascript
javascript游戏开发之《三国志曹操传》零部件开发(二)人物行走的实现
Jan 23 Javascript
jquery submit ie6下失效的原因分析及解决方法
Nov 15 Javascript
Jquery validation remote 验证的缓存问题解决方法
Mar 25 Javascript
javascript的switch用法注意事项分析
Feb 02 Javascript
JS获取地址栏参数的两种方法(简单实用)
Jun 14 Javascript
jQuery flip插件实现的翻牌效果示例【附demo源码下载】
Sep 20 Javascript
JavaScript原生节点操作小结
Jan 17 Javascript
解决vue项目使用font-awesome,build后路径的问题
Sep 01 Javascript
微信小程序使用gitee进行版本管理
Sep 20 Javascript
js实现滚动条自动滚动
Dec 13 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
如何实现给定日期的若干天以后的日期
2006/10/09 PHP
用js进行url编码后用php反解以及用php实现js的escape功能函数总结
2010/02/08 PHP
PHP中static关键字原理的学习研究分析
2011/07/18 PHP
简洁短小的 JavaScript IE 浏览器判定代码
2010/03/21 Javascript
基于jquery的不规则矩形的排列实现代码
2012/04/16 Javascript
JQuery each()函数如何优化循环DOM结构的性能
2012/12/10 Javascript
onkeypress字符按键兼容所有浏览器使用介绍
2013/04/24 Javascript
php实例分享之实现显示网站运行时间
2014/05/20 Javascript
JS小游戏之宇宙战机源码详解
2014/09/25 Javascript
浅谈jQuery中height与width
2015/07/06 Javascript
浅谈jquery的map()和each()方法
2016/06/12 Javascript
深入剖析JavaScript面向对象编程
2016/07/12 Javascript
jquery实现转盘抽奖功能
2017/01/06 Javascript
关于AngularJs数据的本地存储详解
2017/01/20 Javascript
微信小程序动态的加载数据实例代码
2017/04/14 Javascript
JQuery实现table中tr上移下移的示例(超简单)
2018/01/08 jQuery
Vue axios设置访问基础路径方法
2018/09/19 Javascript
前端天气插件tpwidget使用方法详解
2019/06/24 Javascript
微信公众号平台接口开发 获取微信服务器IP地址方法解析
2019/08/14 Javascript
vue通过v-html指令渲染的富文本无法修改样式的解决方案
2020/05/20 Javascript
python批量修改文件夹及其子文件夹下的文件内容
2019/03/15 Python
python GUI库图形界面开发之PyQt5动态加载QSS样式文件
2020/02/25 Python
学会python自动收发邮件 代替你问候女友
2020/05/20 Python
Python控制台实现交互式环境执行
2020/06/09 Python
Django-celery-beat动态添加周期性任务实现过程解析
2020/11/26 Python
关于PySnooper 永远不要使用print进行调试的问题
2021/03/04 Python
goodhealth官方海外旗舰店:新西兰国民营养师
2017/12/15 全球购物
意大利时尚奢侈品店:D’Aniello Boutique
2021/01/19 全球购物
过滤器的用法
2013/10/08 面试题
军校制空专业毕业生自我鉴定
2013/11/16 职场文书
说明书范文
2014/05/07 职场文书
乡镇党建工作汇报材料
2014/08/14 职场文书
男性健康日的活动方案
2014/08/18 职场文书
党的群众路线教育实践活动学习笔记
2014/11/05 职场文书
详解Vue中$props、$attrs和$listeners的使用方法
2022/02/18 Vue.js
剧场版《转生恶役只好拔除破灭旗标》公开最新视觉图 2023年上映
2022/04/02 日漫