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

參考資源

相關文章
相關標籤/搜索