1. 程式人生 > >ng-template, ng-container and ngTemplateOutlet - 全方位剖析 Angular 模板

ng-template, ng-container and ngTemplateOutlet - 全方位剖析 Angular 模板

ng-template, ng-container and ngTemplateOutlet - 全方位剖析 Angular 模板

在這篇文章中, 我們將會深入介紹一些 Angular Core 的高階功能!

你可能已經通過一些angular core 的指令間接的使用過 ng-template 了,例如 ngIf/else, ngSwitch.

ng-template 和 ngTemplateOutlet 命令是 angular 為我們提供的非常強大的功能為了支援更多樣化的使用場景。

這些指令經常與 ng-container 一起使用,並且因為這些指令被設計為一起使用,所以我們這次將一起學習它們。

讓我們來看看這些指令啟用的一些更高階的用例。注意:這個帖子的所有程式碼都可以在這個GITHUB中找到。

ng-template 指令介紹

與名稱相同,ng-template 指令 表示 Angular 模板:這意味著該標籤的內容將包含模板的一部分,然後可以與其他模板組合在一起,以形成最終的元件模板。

Angular 已經在許多結構指令中悄悄地使用了 ng-template,我們經常使用的有:ngIf,ngFor 和 ngSwitch。

讓我們用一個例子開始學習 ng-template。這裡我們定義了選項卡元件的兩個選項卡按鈕(稍後對此進行詳細說明):

@Component
({ selector: 'app-root', template: ` <ng-template> <button class="tab-button" (click)="login()">{{loginText}}</button> <button class="tab-button" (click)="signUp()">{{signUpText}}</button> </ng-template> `}) export class
AppComponent {
loginText = 'Login'; signUpText = 'Sign Up'; lessons = ['Lesson 1', 'Lessons 2']; login() { console.log('Login'); } signUp() { console.log('Sign Up'); } }

你首先將會注意到的是 ng-template

如果您嘗試上面的示例,您可能會驚訝地發現,該示例 不會向螢幕呈現任何內容

這是正常的,這是預期的行為。這是因為NG模板標籤只是定義了一個模板,但是我們現在還沒有使用它。

先不要急,之後我們在其他例子中使模板輸出。

ng-template 指令和 ngIf

你可能在第一次使用 ng-template 是在實現 if/else 場景時,例如:

<div class="lessons-list" *ngIf="lessons else loading">
  ... 
</div>

<ng-template #loading>
    <div>Loading...</div>
</ng-template>

這是 ngIf/else 功能的一個非常普遍的用法:在等待資料從後端返回時,我們顯示了另一個 “loading“ 模板。

正如我們所看到的,esle 指向一個模板,該模板具有名稱:loading,該名稱通過模板引用被分配給它,使用的是 ”#loading“ 這樣的語法。

但是除了 loading 模板之外,我們使用 ngIf 時還建立了第二個隱式 ng-template!讓我們來看看語法糖包裹下到底發生了什麼:

<ng-template [ngIf]="lessons" [ngIfElse]="loading">
   <div class="lessons-list">
     ... 
   </div>
</ng-template>

<ng-template #loading>
    <div>Loading...</div>
</ng-template>

這就是解開語法糖之後 Angular 所做的事情:
- 用結構指令 *ngIf 宣告的元素移動到了 ng-template 中
- *ngIf 的表示式已經被分解並應用於兩個獨立的指令,使用[ngIf]和[ngIfElse]模板輸入變數語法。

這是一個 ngIf 的例子,ngFor 和 ngSwitch 這兩個語法糖其實原理也是一樣的。

這些指令都是非常常用的,因此這意味著這些模板以隱式或顯式的形式出現在 Angular 應用的各個地方。

但基於這個例子,可能會想到一個問題:
如果應用到同一個元素上有多個結構指令,它是如何工作的?

多結構指令

讓我們看看如果我們嘗試在同一個元素中使用 ngIf 和 ngFor會發生什麼:

<div class="lesson" *ngIf="lessons" 
       *ngFor="let lesson of lessons">
    <div class="lesson-detail">
        {{lesson | json}}
    </div>
</div>  

這是行不通的!相反,我們會得到以下錯誤資訊:

Uncaught Error: Template parse errors:
Can't have multiple template bindings on one element. Use only one attribute 
named 'template' or prefixed with *

這意味著不可能將兩個結構指令應用到同一個元素。為了做到這一點,我們必須做一些類似的事情:

<div *ngIf="lessons">
    <div class="lesson" *ngFor="let lesson of lessons">
        <div class="lesson-detail">
            {{lesson | json}}
        </div>
    </div>
</div>

在這個例子中,我們已經將 ngIf 指令移動到外層 div 元素上,但是為了使它工作,我們必須建立額外的 div 元素。

這個解決方案已經起作用了,但是有沒有一種方法可以將結構指令應用到頁面的一個部分,而不必建立額外的元素呢?

是的,這正是 ng-container 結構指令允許我們做的!

ng-container

為了避免建立額外的div,我們可以使用 ng-container:

<ng-container *ngIf="lessons">
    <div class="lesson" *ngFor="let lesson of lessons">
        <div class="lesson-detail">
            {{lesson | json}}
        </div>
    </div>
</ng-container>

正如我們所看到的,ng-container 為我們提供了一個元素,我們可以將結構指令附加到頁面的一個部分,而不必為此建立額外的元素。

ng-container 還有另一個主要用途:它還可以提供一個佔位符,用於將模板動態注入到頁面中。

使用 ngTemplateOutlet 指令建立動態模板

能夠建立模板引用、並將它們指向其他指令(例如ngIf)僅僅是開始。

我們也可以採用模板本身,並使用 ngTemplateOutlet 指令來例項化頁面上的任何地方:

<ng-container *ngTemplateOutlet="loading"></ng-container>

我們可以在這裡看到 ng-container 如何使用ngTemplateOutlet:我們使用它來在頁面上例項化上面定義的 #loading 模板。

我們通過模板引用載入來引用 #loading 模板,並且我們使用 ngTemplateOutlet 結構指令來例項化模板。

我們可以新增儘可能多的 ngTemplateOutlet 標籤到我們想要的頁面,並例項化一些不同的模板。傳遞給該指令的值可以是任何計算為模板引用的表示式,稍後將對此進行詳細說明。

現在我們知道如何例項化模板,讓我們來討論一下模板可以訪問哪些內容。

Template Context (模板的上下文)

關於模板的一個關鍵問題是,在它們裡面什麼是可見?

模板是否有自己的獨立變數範圍,模板可以看到哪些變數?

在 ng-template 中,我們可以訪問外部模板中可見的相同上下文變數,例如變數 lessons。

這是因為所有 ng-template 例項都具有訪問它們所嵌入的相同上下文的訪問許可權。

但是每個模板也可以定義它自己的一組輸入變數!實際上,每個模板都關聯了包含所有模板特定輸入變數的上下文物件。

讓我們來看一個例子:

@Component({
  selector: 'app-root',
  template: `      
<ng-template #estimateTemplate let-lessonsCounter="estimate">
    <div> Approximately {{lessonsCounter}} lessons ...</div>
</ng-template>
<ng-container 
   *ngTemplateOutlet="estimateTemplate;context:ctx">
</ng-container>
`})
export class AppComponent {
    totalEstimate = 10;
    ctx = {estimate: this.totalEstimate}; 
}

下面我們對這個例子進行一下解析:
- 這個模板與前面的模板不同,它有一個輸入變數(它也可以有幾個)。
- 輸入變數為 lessonsCounter,它是通過 ng-template 屬性使用字首 let- 定義的。
- 在 ng-template 內可見 lessonsCounter,但在外部不可見。
- 這個變數的內容是由其賦值給屬性的表示式決定的。
- 該表示式是針對上下文物件進行求值的,與模板一起傳遞到 ngTemplateOutlet,以例項化
- 該上下文物件必須具有key為“estimate”的屬性,以便在模板內顯示其值。
- 上下文物件通過上下文屬性傳遞給 ngTemplateOutlet,它可以接收任何對物件進行計算的表示式。

在上面的例子中,最終被渲染到螢幕上的事:

Approximately 10 lessons ...

我們還可以做的另一件事是在元件本身的層次上以程式設計方式與模板互動:讓我們看看如何做到這一點。

Template References (模板引用)

同樣,我們可以使用模板引用來引用載入模板,也可以使用 ViewChild 裝飾器將一個模板直接注入到我們的元件中:

@Component({
  selector: 'app-root',
  template: `      
      <ng-template #defaultTabButtons>
          <button class="tab-button" (click)="login()">
            {{loginText}}
          </button>
          <button class="tab-button" (click)="signUp()">
            {{signUpText}}
          </button>
      </ng-template>
`})
export class AppComponent implements OnInit {
    @ViewChild('defaultTabButtons')
    private defaultTabButtonsTpl: TemplateRef<any>;

    ngOnInit() {
        console.log(this.defaultTabButtonsTpl);
    }
}

正如我們所看到的,模板可以像任何其他DOM元素或元件一樣注入,通過提供模板引用名稱 defaultTabButtons 到 ViewChild 裝飾器。

這意味著模板也可以在元件類的級別訪問,並且我們可以做一些事情,例如將它們傳遞給子元件!

我們為什麼要這樣做呢?這裡有一個例子是建立一個可定製的元件,可以傳遞給它的不僅僅是一個配置引數或配置物件,我們也可以傳遞一個模板作為一個輸入引數。

可配置元件通過 @Input 匯入部分模板

舉一個Tab容器的例子,在這裡我們想給元件的使用者配置標籤按鈕。

下面這樣的情況,我們首先定義父元件中按鈕的自定義模板:

@Component({
  selector: 'app-root',
  template: `      
<ng-template #customTabButtons>
    <div class="custom-class">
        <button class="tab-button" (click)="login()">
          {{loginText}}
        </button>
        <button class="tab-button" (click)="signUp()">
          {{signUpText}}
        </button>
    </div>
</ng-template>
<tab-container [headerTemplate]="customTabButtons"></tab-container>      
`})
export class AppComponent implements OnInit {
}

然後,在Tab容器元件上,我們可以定義一個輸入屬性,它也是一個名為 headerTemplate 的模板:

@Component({
    selector: 'tab-container',
    template: `
<ng-template #defaultTabButtons>
    <div class="default-tab-buttons">
        ...
    </div> 
</ng-template>
<ng-container 
  *ngTemplateOutlet="headerTemplate ? headerTemplate: defaultTabButtons">

</ng-container>
... rest of tab container component ...
`})

export class TabContainerComponent {
    @Input()
    headerTemplate: TemplateRef<any>;
}

在這最後一個結合的例子中,讓我們來解析一下例子中都做了些什麼:
- 對於選項卡按鈕定義了預設模板,命名為defaultTabButtons
- 只有當輸入屬性標題模板未定義時,才會使用 defaultTabButtons 模板
- 如果定義了輸入屬性,則將通過 headerTemplate 模板傳遞的自定義輸入模板用於顯示按鈕
- headerTemplate 模板通過 ngTemplateOutlet 屬性在 ng-container 內例項化
- 使用三元表示式來決定使用哪個模板(預設或自定義),但是如果邏輯複雜,我們也可以把它委託給控制器方法。

此設計的最終結果是,如果沒有提供自定義模板,Tab容器將顯示選項卡按鈕的預設外觀,但如果提供了自定義模板,它將使用自定義模板。

總結

把 ng-container, ng-template 和 ngTemplateOutlet 指令都結合在一起,使我們能夠建立高度動態和可定製的元件。

我們甚至可以完全改變基於輸入模板的元件的外觀,並且我們可以在應用程式的多個位置定義模板並例項化。

我希望這篇文章有助於熟悉 angular core 的一些更高階的特性,如果你有問題,請在下面的評論中告訴我,我會給你回覆。

原文連線
https://blog.angular-university.io/angular-ng-template-ng-container-ngtemplateoutlet/