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

动态创建组件

这篇文章我们将介绍在 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;
}

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

(0)

相关推荐

  • 动态创建Angular组件实现popup弹窗功能

    起步: 直接使用ngIf 把弹窗的DOM直接放在页面底下隐藏,通过ngIf这样的指令控制其显示. 改进: 封装成angular模块,通过服务控制其显示 直接使用ngIf的话,让人不爽的地方就在于不够通用,每个页面都得加DOM.改进的话可以把比较通用的一些DOM封装成组件,统一添加到全局页面中,并将显示的控制交给一个angular服务来控制其显示. 比如定义了两个组件(DialogComponent, AlertComponent),将他们都添加到AppComponent下,然后提供一个Popup

  • angular 动态组件类型详解(四种组件类型)

    组件类型1:纯函数功能,而没有视图部分,即Factory(类似于$http) promise.component.html 常见的有内置的$http,$q之类的.一般使用promise与作用域进行交互 组件类型2:不是常驻于视图,而是动态插入的.有UI的一类组件,有输入交互.不常被调用(类似于Model对话框) factory.component.html 并发性.这里收到es6的启发.在factory内使用了构造函数,来区分不同的实例.当然,factory接口返回的类型要根据需求来定:仅仅是一

  • Angular.js实现动态加载组件详解

    前言 有时候需要根据URL来渲染不同组件,我所指的是在同一个URL地址中根据参数的变化显示不同的组件:这是利用Angular动态加载组件完成的,同时也会设法让这部分动态组件也支持AOT. 动态加载组件 下面以一个Step组件为示例,完成一个3个步骤的示例展示,并且可以通过URL user?step=step-one 的变化显示第N个步骤的内容. 1.resolveComponentFactory 首先,还是需要先创建动态加载组件模块. import { Component, Input, Vie

  • Angular 4根据组件名称动态创建出组件的方法教程

    一.理解angular组件 组件是一种特殊的指令,使用更简单的配置项来构建基于组件的应用程序架构,这样他能简单地写app,通过类似的web Component 或者angular2的样式.web Component 是一个规范.马上就要成为标准. 应用组件的优点: 比普通指令配置还简单 提供更好的默认设置和最好的实践 对基于组建的应用架构更优化. 对angular2的升级更平滑. 不用组建的情况: 对那些在 compile或者pre-link阶段要执行操作的指令,组件不能用,因为无法到达那个阶段

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

    动态创建组件 这篇文章我们将介绍在 Angular 中如何动态创建组件. 定义 AlertComponent 组件 首先,我们需要定义一个组件. exe-alert.component.ts import { Component, Input } from '@angular/core'; @Component({ selector: "exe-alert", template: ` <h1>Alert {{type}}</h1> `, }) export cl

  • 详解Angular 4.x NgTemplateOutlet

    该指令用于基于已有的 TemplateRef 对象,插入对应的内嵌视图.在应用 NgTemplateOutlet 指令时,我们可以通过 [ngTemplateOutletContext] 属性来设置 EmbeddedViewRef 的上下文对象.绑定的上下文应该是一个对象,此外可通过 let 语法来声明绑定上下文对象属性名. 友情提示:若 let 语法未绑定任何属性名,则上下文对象中 $implicit 属性,对应的值将作为默认值. NgTemplateOutlet 指令语法 复制代码 代码如下

  • Angular 4.x 动态创建表单实例

    本文将介绍如何动态创建表单组件,我们最终实现的效果如下: 在阅读本文之前,请确保你已经掌握 Angular 响应式表单和动态创建组件的相关知识,如果对相关知识还不了解,推荐先阅读一下 Angular 4.x Reactive Forms和 Angular 4.x 动态创建组件 这两篇文章.对于已掌握的读者,我们直接进入主题. 创建动态表单 创建 DynamicFormModule 在当前目录先创建 dynamic-form 目录,然后在该目录下创建 dynamic-form.module.ts

  • 详解Angular动态组件

    使用场景 我们先明确下动态组件的使用场景,在代码运行时要动态加载组件,换成普通人话,代码需要根据具体情况(比如用户的操作,向后台请求结果)确定在某些地方加载某些组件,这些组件不是静态的(不是固定的). 官网的举例就是,构建动态广告条,广告组件不断会推出新的,再用只支持静态组件结构的模板显然是不现实的. 再举一个常见的例子,动态弹出框,弹出的组件是不确定的.不断更新的,这里那里弹出个购买框,那那那又需要弹出样式选择框,静态组件结构模板是不能满足群众日渐增长的需求. 怎么实现 然后我们来找个把手,看

  • 详解Angular组件之投影

    概述 运行时动态改变组件模版的内容.没路由那么复杂,只是一段html,没有业务逻辑. ngContent指令将父组件模版上的任意片段投影到子组件上. 一.简单例子 1.子组件中使用<ng-content>指令来标记投影点 <div class="wrapper"> <h2>我是子组件</h2> <div>这个div定义在子组件中</div> <ng-content></ng-content>

  • 详解Angular组件之生命周期(二)

    一.view钩子 view钩子有2个,ngAfterViewInit和ngAfterViewChecked钩子. 1.实现ngAfterViewInit和ngAfterViewChecked钩子时注意事项 以父组件调用子组件方法中例子为基础,在父组件中实现ngAfterViewInit和ngAfterViewChecked钩子. 这两个钩子是在组件的模版所有内容组装完成后,组件模版已经呈现给用户看了,之后这两个钩子方法会被调用. @ViewChild('child1') child1:Child

  • 详解Angular组件之中间人模式

    一.中间人模式 该组件树中除了组件1以外,每个组件都有一个父组件可以扮演中间人的角色.顶级的中间人是组件1,它可以使组件2,组件3,组件6之间互相通讯.依次类推,组件2是组件4和组件5的中间人.组件3是组件7和组件8的中间人. 中间人负责从一个组件接收数据并将其传递给另一个组件. 二.例子 股票报价组件为例,假设交易员在监看着报价组件的价格,当股票价格达到某一个值的时候,交易员会点一个购买按钮,来购买股票.问题:报价组件并不知道应该如何下单来买股票,它只是用来监控股票价格的.所以报价组件在这时应

  • 详解Angular父子组件通讯

    概述 Angular组件间通讯 组件树,1号是根组件AppComponent. 组件之间松耦合,组件之间知道的越少越好. 组件4里面点击按钮,触发组件5的初始化逻辑. 传统做法:在按钮4的点击事件里调用组件5的方法.紧密耦合. Angular:在组件4根本不知道组件5存在的情况下实现. 使用松耦合的方式在组件之间传递数据开发出高重用性的组件. 使用输入输出属性在父子关系的组件之间传递数据. 一.输入输出属性概述 组件设计成黑盒模型,用输入属性声明从外部世界接收什么东西.不需要知道这些东西从哪里来

  • 详解Angular组件生命周期(一)

    概述 组件声明周期以及angular的变化发现机制 红色方法只执行一次. 变更检测执行的绿色方法和和组件初始化阶段执行的绿色方法是一个方法. 总共9个方法. 每个钩子都是@angular/core库里定义的接口. import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-life', templateUrl: './life.component.html', styleUrls: ['./life

  • 详解Angular组件数据不能实时更新到视图上的问题

    目录 问题起源 OnPush策略 当前组件或子组件之一触发了事件 总结 问题起源 MainComponent: @Component({ selector: 'main', template: ` <MenuComponent [isReport]="isReport"> </MenuComponent> `, changeDetection:ChangeDetectionStrategy.OnPush }) export class MainComponent

随机推荐