Angular Advanced Templates - ng-template, ng-container and ngTemplateOutlet

撰寫 Angular templates 時一定會用到 ng-templatengTemplateOutlet 來實現進階的功能,而它們經常與 ng-container 搭配使用。

我們將基於 Angular ng-template, ng-container and ngTemplateOutlet - The Complete Guide To Angular Templates 這篇寫得非常好的文章來介紹它們的用途和使用方式。

ng-template Directive

顧名思義,ng-template directive 代表一段 Angular template,它們可以和其他 templates 組合成最終的 component template。ng-template 其實被廣泛應用在 Angular 自身的 structural directives, e.g. ngIf, ngFor, ngSwitch 當中。

如果我們今天撰寫一段 ng-template

 1@Component({
 2  selector: 'app-root',
 3  template: `      
 4    <ng-template>
 5      <button class="tab-button" (click)="login()">{{loginText}}</button>
 6      <button class="tab-button" (click)="signUp()">{{signUpText}}</button>
 7    </ng-template>
 8  `})
 9export class AppComponent {
10  loginText = 'Login';
11  signUpText = 'Sign Up';
12  lessons = ['Lesson 1', 'Lessons 2'];
13
14  login() {
15    console.log('Login');
16  }
17
18  signUp() {
19    console.log('Sign Up');
20  }
21}

大家注意到的第一件事應該會是以上程式碼並沒有 render 任何東西到畫面中。因為 ng-template 只是單純用來定義 template,而我們尚未使用它。

The ng-template Directive and ngIf

1<div class="lessons-list" *ngIf="lessons else loading">
2  ... 
3</div>
4
5<ng-template #loading>
6  <div>Loading...</div>
7</ng-template>

這是一個最常見的例子。我們透過定義 loading (#loading) 為 template reference,在沒有 lessons 資料時顯示 loading template。這段程式碼實際上會被 Angular 解讀為以下形式:

1<ng-template [ngIf]="lessons" [ngIfElse]="loading">
2  <div class="lessons-list">
3    ... 
4  </div>
5</ng-template>
6
7<ng-template #loading>
8  <div>Loading...</div>
9</ng-template>

此轉換也見於 ngForngSwitch,這也是為什麼我們無法使用多個 structural directives 在同一元素上的原因,Angular 無法順利將它們解讀成 implicit ng-template 的格式。

如果要使用多個 structural directives 我們可以這樣寫:

1<div *ngIf="lessons">
2  <div class="lesson" *ngFor="let lesson of lessons">
3    <div class="lesson-detail">
4      {{lesson | json}}
5    </div>
6  </div>
7</div>

不過你應該會想問:可以不要有多餘的 div 嗎?答案是可以的,而正是 ng-container 讓我們可以這麼做!

ng-container Directive

1<ng-container *ngIf="lessons">
2  <div class="lesson" *ngFor="let lesson of lessons">
3    <div class="lesson-detail">
4      {{lesson | json}}
5    </div>
6  </div>
7</ng-container>

ng-container 使我們得以在不必生成多餘元素的情況下,將由 structural directive 控制的元素附帶到畫面上。而它也能用來動態地注入 (inject) template 到畫面,一樣是透過 template reference #loading,並藉由 ngTemplateOutlet directive,我們能在頁面任何地方實例化 template。

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

Template Context

這時衍生出一個關鍵的問題:template 有自己的 variable scope 嗎?template 能取用哪些變數呢?

ng-template instances 能取得它們被內嵌 (embedded) 之處的 context,像是之前的例子中,我們能在 ng-template 裡面使用 lessons。然而各個 template 亦可定義它們自己的 context object

 1@Component({
 2  selector: 'app-root',
 3  template: `      
 4    <ng-template #estimateTemplate let-lessonsCounter="estimate">
 5      <div> Approximately {{lessonsCounter}} lessons ...</div>
 6    </ng-template>
 7    <ng-container *ngTemplateOutlet="estimateTemplate;context:ctx"></ng-container>
 8  `})
 9export class AppComponent {
10  totalEstimate = 10;
11  ctx = {estimate: this.totalEstimate};
12}

Template 中我們以 prefix let- 定義它的 property lessonsCounter,此變數可在 template 中使用。而變數的 context 來自於一個在實例化時跟著 template 一起傳給 ngTemplateOutletcontext object

除了在 template 中使用 ng-template,我們也能在 component level 經由 template references 跟 template 互動。

Template References

我們用 ViewChild decorator 把 template 直接地注入到 component。

 1@Component({
 2  selector: 'app-root',
 3  template: `      
 4    <ng-template #defaultTabButtons>
 5      <button class="tab-button" (click)="login()">{{loginText}}</button>
 6      <button class="tab-button" (click)="signUp()">{{signUpText}}</button>
 7    </ng-template>
 8  `})
 9export class AppComponent implements OnInit {
10  @ViewChild('defaultTabButtons')
11  private defaultTabButtonsTpl: TemplateRef<any>;
12
13  ngOnInit() {
14    console.log(this.defaultTabButtonsTpl);
15  }
16}

由於能在 component 中取得 template,我們就能將該 template 傳遞給 child components。

這常見於當我們想建立更客製化的 component,我們不只傳遞 configuration parameter 或 configuration object,我們亦能將 template 作為 input parameter。

Parent Component: AppComponent

 1@Component({
 2  selector: 'app-root',
 3  template: `      
 4  <ng-template #customTabButtons>
 5    <div class="custom-class">
 6      <button class="tab-button" (click)="login()">{{loginText}}</button>
 7      <button class="tab-button" (click)="signUp()">{{signUpText}}</button>
 8    </div>
 9  </ng-template>
10  <tab-container [headerTemplate]="customTabButtons"></tab-container>      
11`})
12export class AppComponent implements OnInit {}

Child Component TabContainerComponent

在沒有 input headerTemplate 時,render defaultTabButtons

 1@Component({
 2  selector: 'tab-container',
 3  template: `   
 4    <ng-template #defaultTabButtons>
 5      <div class="default-tab-buttons">
 6        ...
 7      </div>
 8    </ng-template>
 9    <ng-container *ngTemplateOutlet="headerTemplate ? headerTemplate: defaultTabButtons"></ng-container>
10    ... rest of tab container component ...
11  `})
12export class TabContainerComponent {
13  @Input()
14  headerTemplate: TemplateRef<any>;
15}

以上就是關於 ng-templateng-containerngTemplateOutlet 的介紹,希望有讓大家更理解它們能做到什麼事。