Angular開發實踐(八): 使用ng-content進行組件內容投射

在Angular中,組件屬於特殊的指令,它的特殊之處在於它有本身的模板(html)和樣式(css)。所以使用組件可使咱們的代碼具備強解耦、可複用、易擴展等特性。一般的組件定義以下:javascript

demo.component.ts:css

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

@Component({
    selector: 'demo-component',
    templateUrl: './demo.component.html',
    styleUrls: ['./demo.component.scss']
})
export class DemoComponent implements OnInit {

    constructor() {
    }

    ngOnInit() {
    }
}

demo.component.html:html

<div class="demo">
    <h2>
        demo-component - 我是一個簡單的組件
    </h2>
</div>

demo.component.scss:java

.demo {
    padding: 10px;
    border: 2px solid red;

    h2 {
        margin: 0;
        color: #262626;
    }
}

此時咱們引用該組件,就會呈現該組件解析以後的內容:node

<demo-component></demo-component>

假設如今有這樣的需求,這個組件可以接受外部投射進來的內容,也就是說組件最終呈現的內容不只僅是自己定義的那些,那該怎麼作呢?這時就要請出本文的主角 ng-contentapi

簡單投射

咱們先從最簡單開始,在 demo.component.html 中添加 <ng-content></ng-content>,修改後的 demo.component.html 和 demo.component.scss 以下:app

demo.component.html:ide

<div class="demo">
    <h2>
        demo-component - 可嵌入外部內容的組件
    </h2>
    <div class="content">
        <ng-content></ng-content>
    </div>
</div>

demo.component.scss:性能

.demo {
    padding: 10px;
    border: 2px solid red;

    h2 {
        margin: 0;
        color: #262626;
    }

    .content {
        padding: 10px;
        margin-top: 10px;
        line-height: 20px;
        color: #FFFFFF;
        background-color: #de7d28;
    }
}

爲了效果展現特地將 <ng-content></ng-content> 所在的容器背景色定義爲橙色。spa

這時咱們在引用該組件時能夠從外部投射內容,外部內容將在橙色區域顯示:

<demo-component>
    我是外部嵌入的內容
</demo-component>

針對性投射

若是同時存在幾個 <ng-content></ng-content>,那外部內容將如何進行投射呢?

咱們先看個示例,爲了區別,我再新增一個藍色區域的 <ng-content></ng-content>,修改後的 demo.component.html 和 demo.component.scss 以下:

demo.component.html:

<div class="demo">
    <h2>
        demo-component - 可嵌入外部內容的組件
    </h2>
    <div class="content">
        <ng-content></ng-content>
    </div>
    <div class="content blue">
        <ng-content></ng-content>
    </div>
</div>

demo.component.scss:

.demo {
    padding: 10px;
    border: 2px solid red;

    h2 {
        margin: 0;
        color: #262626;
    }

    .content {
        padding: 10px;
        margin-top: 10px;
        line-height: 20px;
        color: #FFFFFF;
        background-color: #de7d28;
        
        &.blue {
            background-color: blue;
        }
    }
}

引用該組件:

<demo-component>
    我是外部嵌入的內容
</demo-component>

此時,咱們將看到外部內容投射到了藍色區域:

固然,若是你將橙色區域代碼放在藍色區域代碼的後面,那麼外部內容就會投射到橙色區域:

因此從上面的示例咱們能夠看出,若是同時存在簡單的 <ng-content></ng-content> ,那麼外部內容將投射在組件模板最後的那個 <ng-content></ng-content> 中。

那麼知道這個問題,咱們可能會想,能不能將外部內容有針對性的投射相應的 <ng-content></ng-content> 中呢?答案顯然是能夠的。

爲了處理這個問題,<ng-content> 支持一個 select 屬性,可讓你在特定的地方投射具體的內容。該屬性支持 CSS 選擇器(標籤選擇器、類選擇器、屬性選擇器、...)來匹配你想要的內容。若是 ng-content 上沒有設置 select 屬性,它將接收所有內容,或接收不匹配任何其餘 ng-content 元素的內容。

直接看例子,修改後的 demo.component.html 和 demo.component.scss 以下:

demo.component.html:

<div class="demo">
    <h2>
        demo-component - 可嵌入外部內容的組件
    </h2>
    <div class="content">
        <ng-content></ng-content>
    </div>
    <div class="content blue">
        <ng-content select="header"></ng-content>
    </div>
    <div class="content red">
        <ng-content select=".demo2"></ng-content>
    </div>
    <div class="content green">
        <ng-content select="[name=demo3]"></ng-content>
    </div>
</div>

demo.component.scss:

.demo {
    padding: 10px;
    border: 2px solid red;

    h2 {
        margin: 0;
        color: #262626;
    }

    .content {
        padding: 10px;
        margin-top: 10px;
        line-height: 20px;
        color: #FFFFFF;
        background-color: #de7d28;

        &.blue {
            background-color: blue;
        }

        &.red {
            background-color: red;
        }

        &.green {
            background-color: green;
        }
    }
}

從上面代碼能夠看到,藍色區域將接收 標籤 header 那部份內容,紅色區域將接收 class爲"demo2"的div 的那部份內容,綠色區域將接收 屬性name爲"demo3"的div 的那部份內容,橙色區域將接收其他的外部內容(開始,我是外部嵌入的內容,結束)。

引用該組件:

<demo-component>
    開始,我是外部嵌入的內容,
    <header>
        我是外部嵌入的內容,我在header中
    </header>
    <div class="demo2">
        我是外部嵌入的內容,我所在div的class爲"demo2"
    </div>
    <div name="demo3">
        我是外部嵌入的內容demo,我所在div的屬性name爲"demo3"
    </div>
    結束
</demo-component>

此時,咱們將看到外部內容投射到了指定的 <ng-content></ng-content> 中。

擴展知識

ngProjectAs

如今咱們知道經過 ng-content 的 select 屬性能夠指定外部內容投射到指定的 <ng-content></ng-content> 中。

而要能正確的根據 select 屬性投射內容,有個限制就是 - 不論是 標籤 headerclass爲"demo2"的div仍是 屬性name爲"demo3"的div,這幾個標籤都是做爲 組件標籤 <demo-component></demo-component> 的直接子節點

那若是不是做爲直接子節點,會是什麼狀況呢?咱們簡單修改下引用 demo-component 組件的代碼,將 標籤header 放在一個div中,修改以下:

<demo-component>
    開始,我是外部嵌入的內容,
    <div>
        <header>
            我是外部嵌入的內容,我在header中
        </header>
    </div>
    <div class="demo2">
        我是外部嵌入的內容,我所在div的class爲"demo2"
    </div>
    <div name="demo3">
        我是外部嵌入的內容demo,我所在div的屬性name爲"demo3"
    </div>
    結束
</demo-component>

此時,咱們看到 標籤 header 那部份內容再也不投射到藍色區域中了,而是投射到橙色區域中了。緣由就是 <ng-content select="header"></ng-content> 沒法匹配到以前的 標籤 header,故而將這部份內容投射到了橙色區域的 <ng-content></ng-content> 中了。

爲了解決這個問題,咱們必須使用 ngProjectAs 屬性,它能夠應用於任何元素上。具體以下:

<demo-component>
    開始,我是外部嵌入的內容,
    <div ngProjectAs="header">
        <header>
            我是外部嵌入的內容,我在header中
        </header>
    </div>
    <div class="demo2">
        我是外部嵌入的內容,我所在div的class爲"demo2"
    </div>
    <div name="demo3">
        我是外部嵌入的內容demo,我所在div的屬性name爲"demo3"
    </div>
    結束
</demo-component>

經過設置 ngProjectAs 屬性,讓 標籤header 所在的 div 指向了 select="header",此時 標籤 header 那部份內容有投射到藍色區域了:

<ng-content> 不「產生」內容

作個試驗

作個試驗,先定義一個 demo-child-component 組件:

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

@Component({
    selector: 'demo-child-component',
    template: '<h3>我是demo-child-component組件</h3>'
})
export class DemoChildComponent implements OnInit {

    constructor() {
    }

    ngOnInit() {
        console.log('demo-child-component初始化完成!');
    }
}

demo-component 組件修改成:

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

@Component({
    selector: 'demo-component',
    template: `
        <button (click)="show = !show">
            {{ show ? 'Hide' : 'Show' }}
        </button>
        <div class="content" *ngIf="show">
            <ng-content></ng-content>
        </div>
    `
})
export class DemoComponent implements OnInit {
    show = true;

    constructor() {
    }

    ngOnInit() {
    }
}

而後在 demo-component 中 投射 demo-child-component:

<demo-component>
    <demo-child-component></demo-child-component>
</demo-component>

此時,在控制檯咱們看到打印出 demo-child-component初始化完成! 這些文字。可是當咱們點擊按鈕進行切換操做時,demo-child-component初始化完成! 就再也不打印了,這意味着咱們的 demo-child-component 組件只被實例化了一次 - 從未被銷燬和從新建立。

爲何會出現這樣的狀況呢?

出現緣由

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

這也從原理解釋了前面那個問題:若是同時存在幾個 <ng-content></ng-content>,那外部內容將如何進行投射呢?

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

<demo-component>
    <demo-child-component></demo-child-component>
</demo-component>

很顯然 demo-child-component 組件將被實例化一次,但如今假如咱們使用第三方庫的組件:

<third-party-wrapper>
    <demo-child-component></demo-child-component>
</third-party-wrapper>

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

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

解決方法

爲了讓組件可以控制投射進來的子組件的實例化,咱們能夠經過兩種方式完成:在咱們的內容周圍使用 <ng-template> 元素及 ngTemplateOutlet,或者使用帶有 "*" 語法的結構指令。爲簡單起見,咱們將在示例中使用 <ng-template> 語法。

demo-component 組件修改成:

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

@Component({
    selector: 'demo-component',
    template: `
        <button (click)="show = !show">
            {{ show ? 'Hide' : 'Show' }}
        </button>
        <div class="content" *ngIf="show">
            <ng-container [ngTemplateOutlet]="template"></ng-container>
        </div>
    `
})
export class DemoComponent implements OnInit {
    @ContentChild(TemplateRef) template: TemplateRef;
    show = true;

    constructor() {
    }

    ngOnInit() {
    }
}

而後咱們將 demo-child-component 包含在 ng-template 中:

<demo-component>
    <ng-template>
        <demo-child-component></demo-child-component>
    </ng-template>
</demo-component>

此時,咱們在點擊按鈕進行切換操做時,控制檯都會打印出 demo-child-component初始化完成! 這些文字。

參考資源

ng-content: The hidden docs

相關文章
相關標籤/搜索