iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 19
0
Modern Web

從巨人的 Tip 看 Angular系列 第 19

[Day 19] ngTemplate 與它的小夥伴們

  • 分享至 

  • xImage
  •  

昨天提了一下 ng-template 的不會出現在 DOM 的小秘密,今天要來分享的是另一個實用的 directive:ngTemplateOutlet。

簡單範例

ngTemplateOutlet 通常會跟 ng-container 一起出現,而且會長的像下面這種樣子:

<ng-container *ngTemplateOutlet="eng; context: myContext"></ng-container>
<br>
<ng-container *ngTemplateOutlet="tw; context: myContext"></ng-container>

再搭配上擁有 template variable 的 ng-template:

<ng-template #eng let-name><span>Hello {{name}}!</span></ng-template>
<ng-template #tw let-person="localTw"><span>您好 {{person}}!</span></ng-template>

將所有程式碼放在一起看的話就會是:

<ng-container *ngTemplateOutlet="eng; context: myContext"></ng-container>
<br>
<ng-container *ngTemplateOutlet="tw; context: myContext"></ng-container>

<ng-template #eng let-name><span>Hello {{name}}!</span></ng-template>
<ng-template #tw let-person="localTw"><span>您好 {{person}}!</span></ng-template>
export class AppComponent {
  title = 'day19';
  myContext = {$implicit: 'World', localTw: '世界'};
}

ng-container 與 ng-template 擁有類似的特性,當 Angular 的編譯器將 HTML template 轉成 JavaScript 的時候,會產生一個 template function 來取代原先的 ng-container tag,所以就能避免開發者為了讓版型可以更加靈活而新增的元素,不至於影響 CSS 樣式的套用。

套用的時需要注意一些事情:

  1. 以 structural directive 的方式來使用 ngTemplate,這會使 Angular 將之視為:

    <ng-container 
    	[ngTemplateOutlet]="eng" 
    	[ngTemplateOutletContext]="myContext"
    ></ng-container>
    

    詳情請看 [Day 14] * 與 microsyntax ?

  2. ngTemplateOutlet 的型別是 TemplateRef<any> | null

  3. ngTemplateOutletContext 的型別是 Object | null

稍稍複雜的例子

以上是比較簡單的應用,接下來的範例(Angular Material Stepper)就有點小複雜:

<mat-horizontal-stepper>
  <mat-step label="Step 1" state="phone">
    <p>Put down your phones.</p>
    <div>
      <button mat-button matStepperNext>Next</button>
    </div>
  </mat-step>
  <mat-step label="Step 2" state="chat">
    <p>Socialize with each other.</p>
    <div>
      <button mat-button matStepperPrevious>Back</button>
      <button mat-button matStepperNext>Next</button>
    </div>
  </mat-step>
  <mat-step label="Step 3">
    <p>You're welcome.</p>
  </mat-step>

  <!-- Icon overrides. -->
  <ng-template matStepperIcon="phone">
    <mat-icon>call_end</mat-icon>
  </ng-template>
  <ng-template matStepperIcon="chat">
    <mat-icon>forum</mat-icon>
  </ng-template>
</mat-horizontal-stepper>

↑ Block 1

Block 1 的例子是從 Angular Material Guide 引用來的,讓我們把焦點放在 matStepperIcon 這個 directive 身上,它本身的設計就是用來放在 ng-template 身上,這點我們可以透過它的 selector 得知:

@Directive({
  selector: 'ng-template[matStepperIcon]',
})
export class MatStepperIcon {
  /** Name of the icon to be overridden. */
  @Input('matStepperIcon') name: StepState;

  constructor(public templateRef: TemplateRef<MatStepperIconContext>) {}
}

↑ Block 2:MatStepperIcon 的原始碼

MatStepperIcon 本身並沒有做什麼事情,只是透過 DI 取得 templateRef 並 public 出去而已,所以其他人只要使用 MatStepperIcon 的 templateRef,就可以取得 Block 1 中被包在 ng-template 內的 mat-icon 元素。

目前還看不出這個 mat-icon 元素最後會被誰使用,所以我們接著在看最外層的 mat-horizontal-stepper 元素,也就是 MatHorizontalStepper component

@Component(
  // ... 略
)
export class MatHorizontalStepper extends MatStepper {
  // ... 略
}

↑ Block 3:MatHorizontalStepper component

它繼承了 MatStepper directive,也就是實際上會將 MatStepperIcon query 出來的物件:

@Directive({
  selector: '[matStepper]', 
  providers: [{provide: CdkStepper, useExisting: MatStepper}]
})
export class MatStepper extends CdkStepper implements AfterContentInit {
  // ... 略

  /** Custom icon overrides passed in by the consumer. */
  @ContentChildren(MatStepperIcon, {descendants: true}) _icons: QueryList<MatStepperIcon>;

  // ... 略  

  /** Consumer-specified template-refs to be used to override the header icons. */
  _iconOverrides: {[key: string]: TemplateRef<MatStepperIconContext>} = {};

  // ... 略

  ngAfterContentInit() {
    super.ngAfterContentInit();
    this._icons.forEach(({name, templateRef}) => 
        this._iconOverrides[name] = templateRef);

    // ... 略
  }
  // ... 略
}

↑ Block 4:MatStepper directive

可以從 Block 4 的 MatStepper directive 看出 query 完 MatStepperIcon 之後,MatStepper 會將 _icons 陣列的內容轉換另一個物件:_iconOverrides,然後在 HTML template 內傳給 MatStepHeader component(Link)。

最後當然就是由 MatStepHeader component 來處理 mat-icon 的 templateRef 囉:

<!-- 略 -->

<div class="mat-step-icon-state-{{state}} mat-step-icon" [class.mat-step-icon-selected]="selected">
  <div class="mat-step-icon-content" [ngSwitch]="!!(iconOverrides && iconOverrides[state])">
    <ng-container
      *ngSwitchCase="true"
      [ngTemplateOutlet]="iconOverrides[state]"
      [ngTemplateOutletContext]="_getIconContext()"></ng-container>
    <!-- 略 -->
  </div>
</div>
<!-- 略 -->

↑ Block 5:MatStepHeader component HTML template(Link


以上就是今天要分享的 Tip,透過定義好 ngTemplate,並在其他的 HTML element 上使用 ngTemplateOutlet、ngTemplateOuteletContext 就可以將 ngTemplate 嵌在該元素內顯示,進而達成更彈性的版面設計!

明天要上班的星期日 ?

以下按照入團順序列出我們團隊夥伴的系列文章!

請自由參閱 ?


上一篇
[Day 18] 寫了 ng-template 卻在渲染後只留下註解!關於 HTML tag 不見的秘密
下一篇
[Day 20] ElementRef、TemplateRef、ViewContainerRef ???
系列文
從巨人的 Tip 看 Angular30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言