Angular 4 動態(tài)創(chuàng)建組件

這篇文章我們將介紹在 Angular 中如何動態(tài)創(chuàng)建組件。

定義 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 元素朗徊,因此如果我們需要在頁面中插入該元素首妖,我們就需要考慮在哪里放置該元素。

創(chuàng)建組件容器

在 Angular 中放置組件的地方稱為 container 容器爷恳。接下來有缆,我們將在 exe-app 組件中創(chuàng)建一個模板元素,此外我們使用模板變量的語法,聲明一個模板變量棚壁。接下來模板元素 <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 裝飾器來獲取視圖中的模板元素史隆,如果沒有指定第二個查詢參數(shù),則默認返回的組件實例或相應(yīng)的 DOM 元素曼验,但這個示例中泌射,我們需要獲取 ViewContainerRef 實例。

ViewContainerRef 用于表示一個視圖容器鬓照,可添加一個或多個視圖熔酷。通過 ViewContainerRef 實例,我們可以基于 TemplateRef 實例創(chuàng)建內(nèi)嵌視圖豺裆,并能指定內(nèi)嵌視圖的插入位置拒秘,也可以方便對視圖容器中已有的視圖進行管理。簡而言之臭猜,ViewContainerRef 的主要作用是創(chuàng)建和管理內(nèi)嵌視圖或組件視圖躺酒。

根據(jù)以上需求,更新后的代碼如下:

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

動態(tài)創(chuàng)建組件

接下來获讳,在 AppComponent 組件中阴颖,我們來添加兩個按鈕活喊,用于創(chuàng)建 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 服務(wù)對象钾菊。該 ComponentFactoryResolver 服務(wù)對象中帅矗,提供了一個很重要的方法 - resolveComponentFactory() ,該方法接收一個組件類作為參數(shù)煞烫,并返回 ComponentFactory 浑此。

ComponentFactoryResolver 抽象類:

export abstract class ComponentFactoryResolver {
  static NULL: ComponentFactoryResolver = new _NullComponentFactoryResolver();
  abstract resolveComponentFactory<T>(component: Type<T>): ComponentFactory<T>;
}

在 AppComponent 組件構(gòu)造函數(shù)中,注入 ComponentFactoryResolver 服務(wù):

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 抽象類滞详,我們知道可以通過調(diào)用 ComponentFactory 實例的 create() 方法凛俱,來創(chuàng)建組件。介紹完上面的知識料饥,我們來實現(xiàn) AppComponent 組件的 createComponent() 方法:

createComponent(type) {
   this.container.clear(); 
   const factory: ComponentFactory = 
     this.resolver.resolveComponentFactory(AlertComponent);
   this.componentRef: ComponentRef = this.container.createComponent(factory);
}

接下來我們來分段解釋一下上面的代碼蒲犬。

this.container.clear();

每次我們需要創(chuàng)建組件時,我們需要刪除之前的視圖岸啡,否則組件容器中會出現(xiàn)多個視圖 (如果允許多個組件的話原叮,就不需要執(zhí)行清除操作 )。

const factory: ComponentFactory = this.resolver.resolveComponentFactory(AlertComponent);

正如我們之前所說的,resolveComponentFactory() 方法接受一個組件并返回如何創(chuàng)建組件的 ComponentFactory 實例奋隶。

this.componentRef: ComponentRef = this.container.createComponent(factory);

在上面代碼中擂送,我們調(diào)用容器的 createComponent() 方法,該方法內(nèi)部將調(diào)用 ComponentFactory 實例的 create() 方法創(chuàng)建對應(yīng)的組件唯欣,并將組件添加到我們的容器嘹吨。

現(xiàn)在我們已經(jīng)能獲取新組件的引用,即可以我們可以設(shè)置組件的輸入類型:

this.componentRef.instance.type = type;

同樣我們也可以訂閱組件的輸出屬性境氢,具體如下:

this.componentRef.instance.output.subscribe(event => console.log(event));

另外不能忘記銷毀組件:

ngOnDestroy() {
 this.componentRef.destroy(); 
}

最后我們需要將動態(tài)組件添加到 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

總結(jié)

動態(tài)加載組件的流程:

  • 獲取裝載動態(tài)組件的容器
  • 在組件類的構(gòu)造函數(shù)中躺苦,注入 ComponentFactoryResolver 對象
  • 調(diào)用 ComponentFactoryResolver 對象的 resolveComponentFactory() 方法創(chuàng)建 ComponentFactory 對象
  • 調(diào)用組件容器對象的 createComponent() 方法創(chuàng)建組件并自動添加動態(tài)組件到組件容器中
  • 基于返回的 ComponentRef 組件實例,配置組件相關(guān)屬性 (可選)
  • 在模塊 Metadata 對象的 entryComponents 屬性中添加動態(tài)組件
    • declarations - 用于指定屬于該模塊的指令和管道列表
    • entryComponents - 用于指定在模塊定義時产还,需要編譯的組件列表匹厘。對于列表中聲明的每個組件,Angular 將會創(chuàng)建對應(yīng)的一個 ComponentFactory 對象脐区,并將其存儲在 ComponentFactoryResolver 對象中

我有話說

<ng-template><ng-container> 有什么區(qū)別愈诚?

通常情況下,當我們使用結(jié)構(gòu)指令時牛隅,我們需要添加額外的標簽來封裝內(nèi)容炕柔,如使用 *ngIf 指令:

<section *ngIf="show">
 <div>
   <h2>Div one</h2>
 </div>
 <div>
   <h2>Div two</h2>
 </div>
</section>

上面示例中,我們在 section 標簽上應(yīng)用了 ngIf 指令媒佣,從而實現(xiàn) section 標簽內(nèi)容的動態(tài)顯示匕累。這種方式有個問題是,我們必須添加額外的 DOM 元素默伍。要解決該問題欢嘿,我們可以使用 <ng-template> 的標準語法 (非*ngIf語法糖):

<ng-template [ngIf]="show">
 <div>
   <h2>Div one</h2>
 </div>
 <div>
   <h2>Div two</h2>
 </div>
</ng-template>

問題是解決了但我們不再使用 * 語法糖語法,這樣會導致我們代碼的不統(tǒng)一也糊。雖然解決了問題炼蹦,但又帶來了新問題。那我們還有其它的方案么狸剃?答案是有的掐隐,我們可以使用 ng-container 指令。

<ng-container>

<ng-container> 是一個邏輯容器钞馁,可用于對節(jié)點進行分組虑省,但不作為 DOM 樹中的節(jié)點,它將被渲染為 HTML中的 comment 元素僧凰。使用 <ng-container> 的示例如下:

<ng-container *ngIf="show">
 <div>
   <h2>Div one</h2>
 </div>
 
  <div>
    <h2>Div two</h2>
  </div>
 </ng-container>

有時我們需要根據(jù) switch 語句探颈,動態(tài)顯示文本,這時我們需要添加一個額外的標簽如 <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 指令有什么區(qū)別架馋?我們先看以下示例:

<ng-template>
    <p> In template, no attributes. </p>
</ng-template>

<ng-container>
    <p> In ng-container, no attributes. </p>
</ng-container>

以上代碼運行后狞山,瀏覽器中輸出結(jié)果是:

In ng-container, no attributes.

<ng-template> 中的內(nèi)容不會顯示。當在上面的模板中添加 ngIf 指令:

<template [ngIf]="true">
   <p> ngIf with a template.</p>
</template>

<ng-container *ngIf="true">
   <p> ngIf with an ng-container.</p>
</ng-container>

以上代碼運行后叉寂,瀏覽器中輸出結(jié)果是:

ngIf with a template.
ngIf with an ng-container.

現(xiàn)在我們來總結(jié)一下 <ng-template><ng-container> 的區(qū)別:

  • <ng-template> :使用 * 語法糖的結(jié)構(gòu)指令萍启,最終都會轉(zhuǎn)換為 <ng-template><template> 模板指令,模板內(nèi)的內(nèi)容如果不進行處理屏鳍,是不會在頁面中顯示的勘纯。
  • <ng-container>:是一個邏輯容器,可用于對節(jié)點進行分組钓瞭,但不作為 DOM 樹中的節(jié)點驳遵,它將被渲染為 HTML中的 comment 元素,它可用于避免添加額外的元素來使用結(jié)構(gòu)指令山涡。

最后再來看一個 <ng-container> 的使用示例:

模板定義

<div>
  <ng-container *ngIf="true">
     <h2>Title</h2>
     <div>Content</div>
   </ng-container>
</div>

渲染結(jié)果

<div>
    <!--bindings={
  "ng-reflect-ng-if": "true"
    }--><!---->
    <h2>Title</h2>
    <div>Content</div>
</div>

TemplateRef 與 ViewContainerRef 有什么作用?

TemplateRef

用于表示內(nèi)嵌的 template 模板元素堤结,通過 TemplateRef 實例,我們可以方便創(chuàng)建內(nèi)嵌視圖(Embedded Views)鸭丛,且可以輕松地訪問到通過 ElementRef 封裝后的 nativeElement竞穷。需要注意的是組件視圖中的 template 模板元素,經(jīng)過渲染后會被替換成 comment 元素鳞溉。

ViewContainerRef

用于表示一個視圖容器瘾带,可添加一個或多個視圖。通 ViewContainerRef 實例熟菲,我們可以基于 TemplateRef 實例創(chuàng)建內(nèi)嵌視圖看政,并能指定內(nèi)嵌視圖的插入位置,也可以方便對視圖容器中已有的視圖進行管理科盛。簡而言之帽衙,ViewContainerRef 的主要作用是創(chuàng)建和管理內(nèi)嵌視圖或組件視圖菜皂。(本示例就是通過 ViewContainerRef 對象提供的 API來動態(tài)地創(chuàng)建組件視圖)贞绵。

詳細的內(nèi)容可以參考 - Angular 2 TemplateRef & ViewContainerRef

ViewChild 裝飾器還支持哪些查詢條件?

ViewChild 裝飾器用于獲取模板視圖中的元素恍飘,它支持 Type 類型或 string 類型的選擇器榨崩,同時支持設(shè)置 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;
}

詳細的內(nèi)容可以參考 - Angular 2 ViewChild & ViewChildren

參考資源

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末母蛛,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子乳怎,更是在濱河造成了極大的恐慌彩郊,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異秫逝,居然都是意外死亡恕出,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進店門违帆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浙巫,“玉大人,你說我怎么就攤上這事刷后〉某耄” “怎么了?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵尝胆,是天一觀的道長丧裁。 經(jīng)常有香客問我,道長含衔,這世上最難降的妖魔是什么渣慕? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮抱慌,結(jié)果婚禮上逊桦,老公的妹妹穿的比我還像新娘。我一直安慰自己抑进,他們只是感情好强经,可當我...
    茶點故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著寺渗,像睡著了一般匿情。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上信殊,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天炬称,我揣著相機與錄音,去河邊找鬼涡拘。 笑死玲躯,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的鳄乏。 我是一名探鬼主播跷车,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼橱野!你這毒婦竟也來了朽缴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤水援,失蹤者是張志新(化名)和其女友劉穎密强,沒想到半個月后茅郎,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡或渤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年只洒,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片劳坑。...
    茶點故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡毕谴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出距芬,到底是詐尸還是另有隱情涝开,我是刑警寧澤,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布框仔,位于F島的核電站舀武,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏离斩。R本人自食惡果不足惜银舱,卻給世界環(huán)境...
    茶點故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望跛梗。 院中可真熱鬧寻馏,春花似錦、人聲如沸核偿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽漾岳。三九已至轰绵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間尼荆,已是汗流浹背左腔。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留捅儒,地道東北人液样。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像野芒,于是被迫代替她去往敵國和親蓄愁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,922評論 2 361

推薦閱讀更多精彩內(nèi)容