iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 12
2
Modern Web

Angular 大師之路系列 第 12

[Angular 大師之路] Day 12 - *ngTemplateOutlet 與 ng-template 的完美組合

昨天我們稍微提到了 <ng-template> ,並說明了可以把 <ng-template> 當作是一種樣板上等著被呼叫的方法(function),這時候應該會有兩個問題是:

  1. 只有在 *ngIfelse 中可以使用嗎?
  2. 既然是方法,是否可以帶入參數呢?

今天介紹的 *ngTemplateOutlet 可以幫助我們解答這些問題!

類型:技巧

難度:4 顆星

實用度:4 顆星

使用 *ngTemplateOutlet

*ngTemplateOutlet 顧名思義,就是用來放置 template (也就是 <ng-template> 的地方),這個概念就跟 <router-outlet> 的想法是類似的,在 Angular 的命名中,當我們看到 xxxOutlet,都能想像成它是個放置某個東西的地方!

使用方式其實很簡單,如下:

<ng-template #data>
  Hello World
</ng-template>

<div *ngTemplateOutlet="data"></div>

在上述程式中,我們使用 <ng-template> 並設定一個名為 data 的樣板參考變數,來代表這個樣板,而在要使用這個樣板的地方使用 *ngTemplateOutlet=data 的方式,來決定顯示這個樣板!

我們可以想像成,透過 *ngTemplateOutlet 來決定要呼叫某個樣板上名為 data 的樣板方法,因此我們可以在這個樣板上的任何地方,放上 *ngTemplateOutlet 來顯示某個樣板,把重複的樣版內容抽出來,設計上就會更加靈活。

使用 ngTemplateOutletContext 帶入參數

下一個問題是,既然要把 <ng-template> 想像成是一個方法,我們能夠帶入參數嗎?答案當然是可以的,在使用 *ngTemplateOutlet 時,可以加上 context 代表要傳入 <ng-template> 的參數,如下:

<div *ngTemplateOutlet="data; context: {$implicit: {value: 1}}"></div>

在上面程式中,我們在指定要呼叫的樣板後,使用 context,並帶入一個物件 {$implicit: {value: 1}},這個 $implicit 是一個固定用法,當使用帶入一個物件並有個 $implicit 的屬性時,後面的內容就會被當作 <ng-template> 帶入的預設參數。

而在 <ng-template> 內該怎麼接受這個參數呢?我們只需要使用 let-xxx 的方式,想像是宣告一個變數名稱,會傳入 context 內物件的 $implicit 內的屬性:

<ng-template #data let-input>
  {{ input | json }}
</ng-template>

<div *ngTemplateOutlet="data; context: {$implicit: {value: 1}}"></div>

這裡我們使用 let-input,讓 <ng-template> 內有個 input 變數,並把它以 json 的格式顯示,我們就可以預期會顯示 {value : 1} 的資料囉。

ng-template 傳入多個參數

當然,我們要傳入多個參數也不是問題,$implicit 只是代表當設定 let-xxxx 時,有個預設傳入的值而已,實際上它等於 let-xxx="$implicit",因此當我們有其他的參數時,也可以直接放到 context 裡面:

<div *ngTemplateOutlet="data; context: {$implicit: {value: 1}, another: {value: 2}}"></div>

上面除了 $implicit 外,又額外多了個屬性 another,那麼要怎麼接收這個屬性呢?就可以用 let-xxx="another" 的方法,如下:

<ng-template #data let-input let-another="another">
  <div>{{ input | json }}</div>
  <div>{{ another | json }}</div>
</ng-template>

<div *ngTemplateOutlet="data; context: {$implicit: {value: 1}, another: {value: 2}}"></div>

是不是非常方便啊!

實際應用看看吧

有了上面的觀念後,我們來實做看看一個類似輪播的效果吧!

假設我們有 3 個等著被輪播的 template 如下:

<ng-template>
  Page 1
</ng-template>
<ng-template>
  Page 2
</ng-template>
<ng-template>
  Page 3
</ng-template>

這時候我們可以替每個 <ng-template> 都加上名稱,接著在 *ngTemplateOutlet 呈現不同的內容,但這樣總是有點不靈活,這時候我們可以建立一個 directive,並掛在每個 <ng-template> 上,之後在程式內就可以使用 @ViewChildren 的方式,拿到這些 <ng-template> 囉!

首先我們先建立一個 directive:

@Directive({
  selector: '[appCarouselPage]'
})
export class CarouselPageDirective {
  constructor(public templateRef: TemplateRef<any>) { }
}

在這裡面我們注入 TemplateRef (從 @angular/core ),代表 directive 所在的宿主元素樣板,並將它設為 public,以便拿到 directive 時同時可以拿到所屬的宿主元素樣板。

接著在畫面上掛上這個 directive:

<ng-template appCarouselPage>
  Page 1
</ng-template>
<ng-template appCarouselPage>
  Page 2
</ng-template>
<ng-template appCarouselPage>
  Page 3
</ng-template>
<div *ngTemplateOutlet="displayPage"></div>

*ngTemplateOutlet 內放置了一個 displayPage 變數,接著我們就要在程式中決定這個變數要放置哪個樣板:

export class AppComponent implements AfterViewInit {
  @ViewChildren(CarouselPageDirective) carouselPages: QueryList<CarouselPageDirective> 
  displayPage: TemplateRef<any>;
  index = 0;

  setDisplayPage(index) {
    this.displayPage = 
      this.carouselPages.find((item, index) => index === this.index).templateRef;
  }

  ngAfterViewInit() {
    this.setDisplayPage(this.index);
  }
}

上面程式我們使用 @ViewChildren 取得樣板上所有掛著 CarouselPageDirective 的 directive,並在 ngAfterViewInit 的生命週期內,將取得的所有 directives 的第一筆,設定給要顯示資料的 displayPage 變數,此時畫面上就能顯示 Page 1 的樣板囉!

如果不知道為什麼要在 ngAfterViewInit 取得這些 directives,可以回去看看之前介紹生命週期的文章

之後我們可以再補上輪播的程式,例如:

  next() {
    this.index = (this.index + 1) % this.carouselPages.length;
    this.setDisplayPage(this.index);
  }

就可以達到不斷輪播每個 <ng-template> 的效果啦!

若要帶入參數,也非常容易,如下:

<ng-template appCarouselPage let-bg="background">
  <span [style.background-color]="bg">Page 1</span>
</ng-template>
<ng-template appCarouselPage let-bg="background">
  Page 2
</ng-template>
<ng-template appCarouselPage let-bg="background">
  Page 3
</ng-template>
<div *ngTemplateOutlet="displayPage; context: {background: backgroundColor}"></div>
<button (click)="next()">Next</button>
<button (click)="setBackground()">Set Blue Background</button>

最終效果參考如下:

https://wellwind.idv.tw/blog/2018/10/27/mastering-angular-12-advanced-ng-template-outlet/01.gif

原始碼位置:

https://stackblitz.com/edit/ironman2019-ngtemplateoutlet?file=src/app/app.component.html

本日小結

今天我們更加徹底的學習了如何使用 <ng-template> 的技巧,我們可以使用 *ngTemplateOutlet 來決定顯示什麼 <ng-template> ,當需要有點變化時,也可以搭配 context 傳入不同的參數!並搭配了自訂 directive 的方法,實際做了個小玩具。有了這些彈性的功能,我們在開發樣板時,就能更加有彈性,架構也會更加漂亮,雖然要額外學一點語法,但絕對是非常值得啊!

相關資源


上一篇
[Angular 大師之路] Day 11 - *ngIf 有 else 可以用嗎?
下一篇
[Angular 大師之路] Day 13 - 認識 ng-container
系列文
Angular 大師之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
atimhome
iT邦新手 5 級 ‧ 2018-11-27 18:14:05

https://wellwind.idv.tw/blog/2018/10/26/mastering-angular-12-advanced-ng-template-outlet/01.gif
大大的圖片好像掛了,大大的天梯網站目前也只看到11/14的文章,沒有之後的文章。

感謝提醒,已經更新圖片連結囉

由於完賽了,加上最近比較忙,所以之後的內容會慢慢的再更新
/images/emoticon/emoticon02.gif

atimhome iT邦新手 5 級 ‧ 2018-12-04 15:02:44 檢舉

/images/emoticon/emoticon41.gif

0
Ho.Chun
iT邦新手 5 級 ‧ 2020-03-28 18:02:30

不好意思,想請問一下
因為看到文中,這邊直接注入了 TemplateRef
https://ithelp.ithome.com.tw/upload/images/20200328/20106955K7uxBSdDCB.png

所以可以這麼說嗎 ?

想在 directive 內取得 宿主元素
就要先知道 宿主元素 是什麼類型

另外,如果寫了一個 directive
同時掛在 <div> & <ng-template>
這樣的話還能像文中一樣,直接注入 TemplateRef 嗎 ?
/images/emoticon/emoticon16.gif

需要知道宿主元素是什麼類型正確沒有錯,因此建議規定好宿主類型

如要求所有使用這個 directive 的人只能掛在 <ng-template> 上,不能掛在 <div>

當然這樣使用上比較麻煩,若希望做到更好,也可以同時注入 TemplateRef 和 ElementRef,

注入 ElementRef 時,掛在 div 上就會拿到 div,而掛在 ng-template 上只會拿到一段註解元素
注入 TemplateRef 時,掛在 div 上會拿不到(因為 ng-template 不存在),掛在 ng-template 上就會拿到宿主

另外要注意的是,掛在 div 上時單純注入 ng-template 會出現找不到東西可以注入的問題,此時需要用 @Optional() 來處理

以下範例:

  constructor(
    @Optional() private elementRef: ElementRef<any>,
    @Optional() private templateRef: TemplateRef<any>) { 

    if(templateRef){
      // 針對 template 處理
    } else {
      // 針對 element 處理
    }
  }
Ho.Chun iT邦新手 5 級 ‧ 2020-03-31 18:42:22 檢舉

太感謝了!! 這部分我想我通了 /images/emoticon/emoticon41.gif

我要留言

立即登入留言