Angular 4.x 动态建立组件

动态建立组件

这篇文章咱们将介绍在 Angular 中如何动态建立组件。html

定义 AlertComponent 组件

首先,咱们须要定义一个组件。typescript

exe-alert.component.tsbootstrap

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 元素,所以若是咱们须要在页面中插入该元素,咱们就须要考虑在哪里放置该元素。segmentfault

建立组件容器

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

app.component.tsapp

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

@Component({
  selector: 'exe-app',
  template: `
    <ng-template #alertContainer></ng-template>
  `
})
export class AppComponent { }

友情提示:容器能够是任意的 DOM 元素或组件。函数

在 AppComponent 组件中,咱们能够经过 ViewChild 装饰器来获取视图中的模板元素,若是没有指定第二个查询参数,则默认返回的组件实例或相应的 DOM 元素,但这个示例中,咱们须要获取 ViewContainerRef 实例。this

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

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

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 { }

线上示例 - Plunker

总结

动态加载组件的流程:

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

  • 在组件类的构造函数中,注入 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> 的区别:

  • <ng-template> :使用 * 语法糖的结构指令,最终都会转换为 <ng-template><template> 模板指令,模板内的内容若是不进行处理,是不会在页面中显示的。

  • <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来动态地建立组件视图)。

详细的内容能够参考 - Angular 2 TemplateRef & ViewContainerRef

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;
}

详细的内容能够参考 - Angular 2 ViewChild & ViewChildren

参考资源