详解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 相关文章推荐
获取Javscript执行函数名称的方法
Dec 22 Javascript
Javascript学习笔记6 prototype的提出
Jan 11 Javascript
jQuery 三击事件实现代码
Sep 11 Javascript
多引号嵌套的变量命名的问题
May 09 Javascript
JavaScript中this详解
Sep 01 Javascript
javascript url几种编码方式详解
Jun 06 Javascript
jquery过滤特殊字符',防sql注入的实现方法
Aug 17 Javascript
javascript淘宝主图放大镜功能
Oct 20 Javascript
PHP获取当前页面完整URL的方法
Dec 02 Javascript
jQuery 获取select选中值及清除选中状态
Dec 13 Javascript
JavaScript获取用户所在城市及地理位置
Apr 21 Javascript
js简单遍历获取对象中的属性值的方法示例
Jun 19 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制作静态网站的模板框架
2006/10/09 PHP
Symfony学习十分钟入门经典教程
2016/02/03 PHP
thinkPHP3.x常量整理(预定义常量/路径常量/系统常量)
2016/05/20 PHP
身份证号码前六位所代表的省,市,区, 以及地区编码下载
2007/04/12 Javascript
jQuery实战之品牌展示列表效果
2011/04/10 Javascript
js jquery数组介绍
2012/07/15 Javascript
jQuery获得页面元素的绝对/相对位置即绝对X,Y坐标
2014/03/06 Javascript
JavaScript中的值是按值传递还是按引用传递问题探讨
2015/01/30 Javascript
Bootstrap下拉菜单效果实例代码分享
2016/06/30 Javascript
手机端 HTML5使用photoswipe.js仿微信朋友圈图片放大效果
2016/08/25 Javascript
Node.js与MySQL交互操作及其注意事项
2016/10/05 Javascript
完美解决node.js中使用https请求报CERT_UNTRUSTED的问题
2017/01/08 Javascript
bootstarp modal框居中显示的实现代码
2017/02/18 Javascript
@ResponseBody 和 @RequestBody 注解的区别
2017/03/08 Javascript
微信小程序 动态绑定数据及动态事件处理
2017/03/14 Javascript
用npm-run实现自动化任务的方法示例
2019/01/14 Javascript
node省市区三级数据性能测评实例分析
2019/11/06 Javascript
vue中 数字相加为字串转化为数值的例子
2019/11/07 Javascript
React实现轮播效果
2020/08/25 Javascript
Vue Elenent实现表格相同数据列合并
2020/11/30 Vue.js
[04:03]辉夜杯主赛事 12月25日RECAP精彩回顾
2015/12/26 DOTA
在Python中实现贪婪排名算法的教程
2015/04/17 Python
Python编程argparse入门浅析
2018/02/07 Python
Sanic框架基于类的视图用法示例
2018/07/18 Python
对python中list的拷贝与numpy的array的拷贝详解
2019/01/29 Python
使用py-spy解决scrapy卡死的问题方法
2020/09/29 Python
iframe跨域的几种常用方法
2019/11/11 HTML / CSS
意大利奢侈品购物网站:Giglio
2018/01/05 全球购物
Marlies Dekkers内衣法国官方网上商店:国际知名的荷兰内衣品牌
2019/03/18 全球购物
大学生职业生涯规划范文
2014/01/08 职场文书
学年末自我鉴定
2014/01/21 职场文书
房地产营销策划方案
2014/02/08 职场文书
销售人员职业生涯规划范文
2014/03/01 职场文书
解除合同协议书
2014/04/17 职场文书
2014年底个人工作总结
2015/03/10 职场文书
当你焦虑迷茫时,请读读这6句话
2019/07/24 职场文书