Angular 中 ngTemplateOutlet 的用法以及ng-zorro源碼分析!

1、引言

今天看到文章:https://segmentfault.com/a/1190000015944548 。因而專門研究一下ngTemplateOutlet用法!!!!javascript

官方定義 :

NgTemplateOutlet: 它是結構指令,根據一個提早備好的 TemplateRef 插入一個內嵌視圖。 你能夠經過設置 [ngTemplateOutletContext] 來給 EmbeddedViewRef 附加一個上下文對象。 [ngTemplateOutletContext] 是一個對象,該對象的 key 可在模板中使用 let 語句進行綁定。html

示例:  <ng-container *ngTemplateOutlet="templateRefExp; context: contextExp"></ng-container>       java

*我測試:<template>  <ng-template> <ng-container> 均可承載這個指令, 但不支持<div>這樣的標籤,提示:segmentfault

使用目的

須要要自定義標題或頁腳的內容。 
好比編寫彈窗組件,不建議在組件模板中寫死標題和頁腳的內容,當在頁面上使用該組件時, 頁面能夠動態向指定組件內佔位傳入「一些內容」,組件會把它們插入到它想要的地方!
    好比ng-zorro中,也大量這樣用法,https://ng.ant.design/components/card/zh#components-card-demo-simple
api

2、組件自定義輸入內容

好比在一個頁面上,引用nz-card時,把主頁面上<ng-template>內容插入到nz-card中去,文檔上示例代碼:
app

若是要編寫這樣的組件,須要考慮幾個問題,框架

一、如何引用主頁面上的一個模板元素   ( 使用本地變量 # ,我記得之前官方文檔叫「局部模版變量"或「模板引用變量」)
二、引用的變量如何傳遞給子組件中      (
子組件定義@Input  ,使用類型爲TemplateRef<T>的變量接收
三、子組件如何使用這個引用變量          ( 在模版中,用
ngTemplateOutlet  綁定這個變量便可)
四、引用的模板元素從主頁面上來,  如何把子組件的數據(即子組件中上下文)傳遞給這個引用元素上來?(向
ngTemplateOutlet 傳入 context: myContext」)函數

上下文傳遞很重要。組件爲了使用上的靈活,一部份內容定義在組件以外的(即主頁面上),當它插入到子組件中的時候,必然要顯示子組件內的一些數據,它纔有意義。
ngTemplateOutlet 不只用於綁定元素,還負責把子組件中的一個數據上下文傳遞進去.測試

五、模板元素如何使用上下文? (使用 模板輸入變量let  的形式,接收上下文屬性值,再用  {{  }}語法插入值)this

   定義上下文數據時,  myContext = { $implicit: 'World', valueInContent: "子組件內的value" };   
 $implicit:是默認導出值。當let-item  後沒= 號時,item引用它
  當 let- valueInContent =valueInContent 時,在模板元素內部能夠插值{{valueInContent}}
參考官方文檔:

 

完整而精巧的小例子,包含上面5個要點,要認真融會貫通呀

源碼以下:

/// 主頁面
@Component({
  selector: 'app-root',
  template: `
  <h1>Angular's ngTemplateOutlet 完整示意-----我是主頁</h1>
  <app-content [dynamicRef]="usedByContent"></app-content>

  <ng-template #usedByContent let-name let-valueInContent=valueInContent>
  <div style="margin-left:20px;border:dashed;">
    <div>Hello {{name}}!    組件內的上下文綁定: {{valueInContent}} ....</div>
    <div> 主頁面的變量綁定: {{valueInApp}}</div>
  </div>
  </ng-template>`,
})
export class AppComponent {
  public valueInApp = "valueInApp :)";
}

/// 一個子組件
@Component({
  selector: 'app-content',
  template: `
     <div>我是子組件,下面的內容是動態加載   :)</div>
    <template
        *ngTemplateOutlet="dynamicRef context: myContext">
    </template>
  `,
})
export class AppContent {
  display = false;
  @Input() dynamicRef: TemplateRef<HTMLDivElement>;
  myContext = { $implicit: 'World',  valueInContent: "子組件內的value" };
}

3、ng-zorro 的組件的實現

支持TemplateRef參數的組件的實現

ng-zorro中,大量的組件的參數支持傳入模板引用,它的使用無處不在,要研究框架是如何實現的傳入模板引用的參數。 

nz-card爲例,它的API說明

凡是支持插入外部的模板引用的參數,   它們的類型就是: string|TemplateRef<void>    或者  TemplateRef<void>

翻看ng-zorro的nz-card 源碼,摘引以下,如今看它是否是超級簡單:

// 組件TS  
@Input() nzExtra: string | TemplateRef<void>;

// 組件HTML
<div class="ant-card-extra" *ngIf="nzExtra">
      <ng-container *nzStringTemplateOutlet="nzExtra">{{ nzExtra }}</ng-container>
</div>
// 組件TS
 @Input() nzActions: Array<TemplateRef<void>> = [];

// 組件HTML
<ul class="ant-card-actions" *ngIf="nzActions.length">
  <li *ngFor="let action of nzActions">
    <span><ng-template [ngTemplateOutlet]="action"></ng-template></span>
  </li>
</ul>

對於TemplateRef<void>的參數, 直接用[ngTemplateOutlet] 來綁定便可。框架使用ngTemplateOutlet的方式和咱們上一節看到的同樣。
             我測試 
[ngTemplateOutlet] 不可綁定上下文,但*ngTemplateOutlet就能夠,因此咱們儘可能使用*的格式吧!

 對於string|TemplateRef<void>的參數,用*nzStringTemplateOutlet 來綁定,這是什麼鬼東西呢? 

*nzStringTemplateOutlet探險

 *nzStringTemplateOutlet 是ng-zorro官方擴展的指令,很實用且複雜的指令,它即接受string|TemplateRef<void>的兩種參數值,插入到子組件的模板中去。
如今要當心翼翼的進入源碼探險之旅了!

 

2個知識必備:

一、構造函數注入變量
viewContainer表明指令的宿主元素,此處它承擔渲染容器的做用,
defalultTemplate注入的是宿主元素的內容元素,就是上面的{{nzExtra}}部分。

//  注入viewContainer 宿主元素,   注入  defaultTemplate的 TemplateRef<void>
constructor(private viewContainer: ViewContainerRef, private defaultTemplate: TemplateRef<void>) {}

爲何能這樣注入變量,這得參見官方文檔:

       viewContainer注入:    

      defalultTemplate注入: 

二、動態建立EmbeddedView,  這是動態組件時,常常要使用的函數!

this.viewContainer.createEmbeddedView( 模版引用,上下文數據  );  //返回一個EmbeddedView

分析源碼邏輯

使用nzStringTemplateOutlet的情景以下,nzExtra的值:string  |  TemplateRef<void> 兩種可能 :

<ng-container *nzStringTemplateOutlet="nzExtra">{{ nzExtra }}</ng-container>

一、首先得到各自的  TemplateRef<any> 對象。 (見上圖紅綠線)
參數爲:字符串string時,            經過注入,        {{nzExtra}}  ==> defalultTemplate.
參數爲:模板TemplateRef時,     經過指令賦值, 把 變量nzExtra==>inputTemplate.

二、建立EmeddedView。 
使用viewContainer.createEmbeddedView方法,  把TemplateRef和上下文來生成View.

三、監聽指令ngOnChanges。
             四、判斷是否從新建立視圖。由於用戶動態修改引用或上下文時,整個視圖要重建!
                          是否重建View,就是判斷nzStringTemplateOutletContext, nzStringTemplateOutlet 是否變化
                           a)  若是nzStringTemplateOutlet第一次賦值,或nzStringTemplateOutlet變化先後不都是string, 要重建視圖
                           b)  若是nzStringTemplateOutletContext 的屬性shape有變化時,也要重建視圖。
                                                                    (注:新上下文的屬性比舊上下文屬性個數多時才變化, 若只是屬性值變化,是不用更新視圖)

              五、若須要重建視圖,則重建視圖。
                                     六、無需重建視圖,則進一步判斷是不是TemplateRef參數狀況,若是上下文數據變化了,動態更新EmeddedView的值!

經過以上源碼,nzStringTemplateOutlet指令就能夠監聽綁定模板變化,以及上下文數據變化,更新視圖。 正因爲這個指令強大,方便,咱們在使用ng-zorro的組件時,才倍感方便順手。

綁定上下文件的語法

最後,在ng-zorro源碼中,搜索 nzStringTemplateOutlet綁定上下文時的語法,引自nz-form-control源碼:

// ng-zorro的官方寫法  
<ng-container *nzStringTemplateOutlet="nzSuccessTip;context:{$implicit:validateControl};">
     {{ nzSuccessTip }}
  </ng-container>

 忽然很意外,爲何不是下面這樣使用? 爲何寫context就能自動賦值到nzStringTemplateOutletContext上去?? 
可能這是因爲Angular編譯時,特殊的處理吧!

// 這個寫法編譯報錯,container 不存在nzStringTemplateOutletContext屬性
  <ng-container *nzStringTemplateOutlet="nzSuccessTip"  
                [nzStringTemplateOutletContext]="{$implicit:validateControl}「 >
      {{ nzSuccessTip }}
  </ng-container>

4、它的同類指令:NgComponentOutlet

偶然看到Common 模塊中,還有NgComponentOutlet指令,根據名稱也能猜出它的做用,直接把一個組件綁定過來,此時只要傳遞一個組件的類名便可!

我想起去年編寫實時數據看板的功能時,每個看板的子組件是根據配置,動態生成一個類的工廠函數,再建立類view,再插入到相應的containerview中去。 當時覺得很完美的方法,實際上是繞了不少彎路,直接用NgComponentOutlet一句話不就夠了嗎,當時真的蠢,不過看了許多文章,也沒有講到這個指令的地方。Angular的知識點太多太碎了呀!

相關文章
相關標籤/搜索