Angular Advanced Templates - ng-template, ng-container and ngTemplateOutlet
撰寫 Angular templates 時一定會用到 ng-template
和 ngTemplateOutlet
來實現進階的功能,而它們經常與 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>
此轉換也見於 ngFor
和 ngSwitch
,這也是為什麼我們無法使用多個 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 一起傳給 ngTemplateOutlet
的 context 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-template
、ng-container
和 ngTemplateOutlet
的介紹,希望有讓大家更理解它們能做到什麼事。