ng-content 中隱藏的內容

閱讀 Angular 6/RxJS 最新教程,請訪問 前端修仙之路

若是你嘗試在 Angular 中編寫可重複使用的組件,則可能會接觸到內容投射的概念。而後你發現了 <ng-content>,並找到了一些關於它的文章,進而實現了所需的功能。html

接下來咱們來經過一個簡單的示例,一步步介紹 <ng-content> 所涉及的內容。前端

Simple example

在本文中咱們使用一個示例,來演示不一樣的方式實現內容投影。因爲許多問題與Angular 中的組件生命週期相關,所以咱們的主要組件將顯示一個計數器,用於展現它已被實例化的次數:node

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

let instances = 0;

@Component({
  selector: 'counter',
  template: '<h1>{{this.id}}</h1>'
})
class Counter {
  id: number;
  
  constructor() {
    this.id = ++instances;
  }
}

上面示例中咱們定義了 Counter 組件,組件類中的 id 屬性用於顯示本組件被實例化的次數。接着咱們繼續定義一個 Wrapper 組件:typescript

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

@Component({
  selector: 'wrapper',
  template: `
    <div class="box">
      <ng-content></ng-content>
    </div>
  `
})
class Wrapper {}

如今咱們來驗證一下效果:app

<wrapper>
  <counter></counter>
  <counter></counter>
  <counter></counter>
</wrapper>

Targeted projection

有時你但願將包裝器的不一樣子項投影到模板的不一樣部分。爲了處理這個問題,<ng-content> 支持一個 select 屬性,可讓你在特定的地方投射具體的內容。該屬性支持 CSS 選擇器(my-element,.my-class,[my-attribute],...)來匹配你想要的內容。若是 ng-content 上沒有設置 select 屬性,它將接收所有內容,或接收不匹配任何其餘 ng-content 元素的內容。長話短說:ide

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

@Component({
  selector: 'wrapper',
  template: `
  <div class="box red">
    <ng-content></ng-content>
  </div>
  <div class="box blue">
    <ng-content select="counter"></ng-content>
  </div>
  `,
  styles: [`
    .red {background: red;}
    .blue {background: blue;}
  `]
})
export class Wrapper { }

上面示例中,咱們引入了 select 屬性,來選擇投射的內容:性能

<wrapper>
  <span>This is not a counter</span>
  <counter></counter>
</wrapper>

上述代碼成功運行後,counter 組件被正確投影到第二個藍色框中,而 span 元素最終會在所有紅色框中。請注意,目標 ng-content 會優先於 catch-all,即便它在模板中的位置靠後。測試

ngProjectAs

有時你的內部組件會被隱藏在另外一個更大的組件中。有時你只須要將其包裝在額外的容器中便可應用 ngIfngSwitch。不管什麼緣由,一般狀況下,你的內部組件不是包裝器的直接子節點。爲了演示上述狀況,咱們將 Counter 組件包裝在一個 <ng-container> 中,看看咱們的目標投影會發生什麼:this

<wrapper>
  <ng-container>
    <counter></counter>
  </ng-container>
</wrapper>

如今咱們的 couter 組件會被投影到第一個紅色框中。由於 ng-container 容器再也不匹配 select="counter"。爲了解決這個問題,咱們必須使用 ngProjectAs 屬性,它能夠應用於任何元素上。具體以下:spa

<wrapper>
  <ng-container ngProjectAs="counter">
    <counter></counter>
  </ng-container>
</wrapper>

經過設置 ngProjectAs 屬性,終於讓咱們的 counter 組件重回藍色框的懷抱了。

Time to poke and prod

咱們從一個簡單的實驗開始:將兩個 <ng-content> 塊放在咱們的模板中,沒有選擇器。會出現什麼狀況?

頁面中會顯示一個或兩個框,若是咱們包含兩個框,它們的內容是顯示 1 和 1 或 1 和 2?

<div class="box red">
    <ng-content></ng-content>
</div>
<div class="box blue">
    <ng-content></ng-content>
</div>

答案是咱們在最後一個 <ng-content> 中獲得一個計數器,另外一個是空的!在咱們嘗試解釋爲何以前,讓咱們再來驗證一個問題,即在 ng-content 指令的外層容器中添加 ngIf 指令:

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

@Component({
  selector: 'wrapper',
  template: `
    <button (click)="show = !show">
      {{ show ? 'Hide' : 'Show' }}
    </button>
    <div class="box" *ngIf="show">
      <ng-content></ng-content>
    </div>
  `
})
class Wrapper {
  show = true;
}

乍一看,彷佛正常運行。可是若是你經過按鈕進行切換操做,你會注意到計數器的值不會增長。這意味着咱們的計數器組件只被實例化了一次 - 從未被銷燬和從新建立。難道這是 ngIf 指令產生的問題,讓咱們測試一下 ngFor 指令,看看是否有一樣的問題:

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

@Component({
  selector: 'wrapper',
  template: `
    <div class="box" *ngFor="let item of items">
      <ng-content></ng-content>
    </div>
  `
})
class Wrapper {
  items = [0, 0, 0];
}

以上代碼運行後與咱們使用多個 <ng-content> 的效果是同樣的,只會顯示一個計數器!爲何不按照咱們的預期運行?

The explanation

<ng-content> 不會 "產生" 內容,它只是投影現有的內容。你能夠認爲它等價於 node.appendChild(el) 或 jQuery 中的 $(node).append(el) 方法:使用這些方法,節點不被克隆,它被簡單地移動到它的新位置。所以,投影內容的生命週期將被綁定到它被聲明的地方,而不是顯示在地方。

這種行爲有兩個緣由:指望一致性和性能。什麼 "指望的一致性" 意味着做爲開發人員,能夠基於應用程序的代碼,猜想其行爲。假設我寫了如下代碼:

<div class="my-wrapper">
  <counter></counter>
</div>

很顯然計數器將被實例化一次,但如今假如咱們使用第三方庫的組件:

<third-party-wrapper>
  <counter></counter>
</third-party-wrapper>

若是第三方庫可以控制 counter 組件的生命週期,我將沒法知道它被實例化了多少次。其中惟一方法就是查看第三方庫的代碼,瞭解它們的內部處理邏輯。將組件的生命週期被綁定到咱們的應用程序組件而不是包裝器的意義是,開發者能夠掌控計數器只被實例化一次,而不用瞭解第三方庫的內部代碼。

性能的緣由更爲重要。由於 ng-content 只是移動元素,因此能夠在編譯時完成,而不是在運行時,這大大減小了實際應用程序的工做量。

The solution

爲了讓包裝器可以控制其子元素的實例化,咱們能夠經過兩種方式完成:在咱們的內容周圍使用 <ng-template> 元素,或者使用帶有 "*" 語法的結構指令。爲簡單起見,咱們將在示例中使用 <ng-template> 語法,咱們的新應用程序以下所示:

<wrapper>
  <ng-template>
    <counter></counter>
  </ng-template>
</wrapper>

包裝器再也不使用 <ng-content>,由於它接收到一個模板。咱們須要使用 @ContentChild 訪問模板,並使用ngTemplateOutlet 來顯示它:

@Component({
  selector: 'wrapper',
  template: `
    <button (click)="show = !show">
      {{ show ? 'Hide' : 'Show' }}
    </button>
    <div class="box" *ngIf="show">
      <ng-container [ngTemplateOutlet]="template"></ng-container>
    </div>
  `
})
class Wrapper {
  show = true;
  @ContentChild(TemplateRef) template: TemplateRef;
}

如今咱們的 counter 組件,每當咱們隱藏並從新顯示時都正確遞增!讓咱們再驗證一下 *ngFor 指令:

@Component({
  selector: 'wrapper',
  template: `
    <div class="box" *ngFor="let item of items">
      <ng-container [ngTemplateOutlet]="template"></ng-container>
    </div>
  `
})
class Wrapper {
  items = [0, 0, 0];
  @ContentChild(TemplateRef) template: TemplateRef;
}

上面代碼成功運行後,每一個盒子中有一個計數器,顯示 1,2 和 3,這正是咱們以前預期的結果。

參考資源

相關文章
相關標籤/搜索