--- title : 像PrimeNG一樣注入模板進Component tags: ng-template ng-container directives *ngTemplateOutlet ViewChildren ContentChildren --- # 像PrimeNG一樣注入模板進Component 曾經第一次使用PrimeNG後被他的設計模式給驚艷了。後續才在開發模板上也發有些Component其實可以像PrimeNG一樣將共性的部分建立為Component,再將可動態化的部分注入進去。進而更有彈性的使用這些定義好的Component。這時候燃起了興趣,馬上去[PrimeNG Github](https://github.com/primefaces/primeng)翻看看身為開源程式碼是如何實現的。 <!-- 好啦經過一連串的Google和測試後,在這篇得出一些結論。而這部分的例子就是PrimeNG的p-table這個Component。接下來會討論針對這個設計模式基本上會用到的一些Angular提供的基本元素。像是ng-template、ng-container、directives、*ngTemplateOutlet、ViewChildren、ContentChildren。 --> ``` <p-table> <ng-template pTemplate="colgroup" let-columns> ... </ng-template> <ng-template pTemplate="header" let-columns> ... </ng-template> <ng-template pTemplate="body" let-row let-columns="columns"> ... </ng-template> </p-table> ``` <!-- 探討這樣的設計模式前,首先需要提出幾個問題 1. 什麼是ng-template? 2. 什麼是directive? - 結構型指令*ngTemplateOutlet在這個場景可以做什麼? 3. <ng-template>帶上Directive然後呢? 4. <p-*>開頭的模板(component)是如何做到指定對象注入模板? --> 初來乍到當然是要去看看==src\app\components\table\table.ts==這個p-table的家長什麼樣。 ### 首先是Template ``` 62 <div #container ...> ... 79 <div class="ui-table-wrapper" *ngIf="!scrollable"> 80 <table role="grid" #table [ngClass]="tableStyleClass" [ngStyle]="tableStyle"> 81 <ng-container *ngTemplateOutlet="colGroupTemplate; context {$implicit: columns}"></ng-container> 82 <thead class="ui-table-thead"> 83 <ng-container *ngTemplateOutlet="headerTemplate; context: {$implicit: columns}"></ng-container> 84 </thead> 85 <tbody class="ui-table-tbody" [pTableBody]="columns" [pTableBodyTemplate]="bodyTemplate"></tbody> 86 <tfoot *ngIf="footerTemplate" class="ui-table-tfoot"> 87 <ng-container *ngTemplateOutlet="footerTemplate; context {$implicit: columns}"></ng-container> 88 </tfoot> 89 </table> 90 </div> ... ``` 這邊呢可以看到在HTML的==table標籤中有一些特別的地方ng-container、*ngTemplateOutlet==。除了這些東西在table標籤內竟然沒有看到任何關於行列元素的直接定義,推測就是注入模板的地方。 ### 看看ts的部分有什麼特別的 ``` 295 @ContentChildren(PrimeTemplate) templates: QueryList<PrimeTemplate>; ``` 從命名上來看templates這個參數存放的是多個模板,並且以Angular的ContentChildren decorator注入的形式在angular的生命週期鉤子(life cycle)的ngAfterContentInit獲取初始值。詳情請看官方說明(備註1)。而table又沒有明確內容描述,看來這就是所有注入模板的地方。==注意這邊有個關鍵字PrimeTemplate== ### 繼續追蹤templates是什麼時候被用到的? ``` 419 ngAfterContentInit() { this.templates.forEach((item) => { switch (item.getType()) { ... 426 case 'header': 427 this.headerTemplate = item.template; 428 break; ... 485 } 486 }); 487 } ``` ``` 309 headerTemplate: TemplateRef<any>; ``` 這邊有兩個部分,第一個是ngAfterContentInit內獲取了templates的初始值後將其遍歷,並將item中的template屬性指派給==this.heaederTemplate==全域變量。而第二個部分明確指出headerTemplate指的是TemplateRef(備註2)。 ### 回過頭來看PrimeTemplate是什麼? src\app\components\api\shared.ts ``` 17 @Directive({ 18 selector: '[pTemplate]', 19 host: { 20 } 21 }) 22 export class PrimeTemplate { 23 24 @Input() type: string; 25 26 @Input('pTemplate') name: string; 27 28 constructor(public template: TemplateRef<any>) {} 29 30 getType(): string { 31 return this.name; 32 } 33} ``` 這邊明確的了解到這是Angular的Directive。在建構子中有一個==public template: TemplateRef<any>==,參見官方說明TemplateRef(備註2)。這也就是為什麼在table.ts的onAfterContentInit內(427行)要寫item.template了。建構子內注入的template就是我們在調用primeng時被pTemplate標記的對象。 #### 小結休息一下... 被Directive標記的對象,在其內部的模板是可以透過TemplateRef的形式注入directive的constructor中(這個部分在官方的TemplateRef有說到)。而primeng用的裝飾器ContentChildren對象可以在AfterContentInit的時候獲取==外部==被pTemplate標記的模板。也就是我們上面提到的table.ts內的onAfterContentInit方法中完成獲取外部模板,並指派給特定位置(headerTemplate ...等等)模板中。 ### 傳給primeng的參數是如何進到我們自己定義的模板上? 還記得一開始我們看到的table模板上有個很特別的地方,ng-container和*ngTemplateOutlet。從官方的ngTemplateOutlet說明文件上(備註3),大致上可以了解這一個directive可以將提前準備好的模板內嵌至被標記的對象。而ngTemplateOutlet可以透過EmbeddedViewRef提供上下文(這邊可以理解為環境變量),然後提供參數給預先定義好的模板來做後續的顯示。而提供的方式就是官方提到的。 ``` <ng-container *ngTemplateOutlet="templateRefExp; context: contextExp"></ng-container> ``` ng-container作為內容注入容器,而templateRefExp是模板上被明確以#符號標記成#templateRefExp的對象。緊接著context指的就是上下文了。而這部分有提供預設參數的寫法 ``` contextExp = {$implicit: 'World', localSk: 'Svet'}; ``` 做為$implicit後面接著的就是預設值,至於預設屬性名稱可以透過let-*方式宣告(*代表任意名稱)。而後面的localSk則是讓外部模板透過let-name="localSk"的方式獲取'Svet'這個值。 所以前面提到的headerTemplate,和他在ngAfterContentInit初始化的部分。其實就是primeng table.ts內模板的83行指的那個headerTemplate。 ``` 309 headerTemplate: TemplateRef<any>; ``` ``` 83 <ng-container *ngTemplateOutlet="headerTemplate; context: {$implicit: columns}"></ng-container> ``` ### 總結 到此我們已經了解了 1.primeng是如何透過directive獲取使用者定義的模板 2.獲取被注入在directive建構子中的模板是透過ContentChildren的decorator,並且在Lift cycle AfterContentInit獲取初始值。 3.ng-container作為注入模板的容器,他具有==不在最終dom中生成任何實際標籤==的特性(備註4) 4.如何透過ngTemplateOutlet將使用者傳入的參數再轉手交給使用者定義好的模板 5.如何提供上下文,提供預設參數或指定名稱的變數給使用者注入的模板 #### 因此我們可以透過這些步驟來實際做出相同設計模式的Component 1.將Component共性的部分定義好 2.將Component中需要彈性更動的部分透過<ng-container>和*ngTemplateOutlet來實現注入與給予上下文。 3.定義一個Directive,並將被標記的對象注入到建構子中。 4.在Component的AfterContentInit中獲取被注入的模板內容。 5.將注入的模板放到ngTemplateOutlet後,分號前的變量名稱。 6.==最後就可以像PrimeNG那樣使用我們自己定義好的Component== 備註1: [@ContentChildren 官方說明](https://angular.io/api/core/ContentChildren) 備註2: [TemplateRef 官方說明](https://angular.tw/api/core/TemplateRef) 備註3: [ngTemplateOutlet 官方說明](https://angular.io/api/common/NgTemplateOutlet) 備註4: [ng-container 官方說明](https://angular.tw/guide/structural-directives#ngcontainer)