我的新書AI 職場超神助手:ChatGPT 與生成式 AI 一鍵搞定工作難題的教材投影片已製作完成
歡迎各位有需要的教師和博碩文化索取教材

[功能介紹-7] Structural Directives

什麼是結構指令

結構指令負責HTML佈局。他們通常通過添加,刪除或操縱元素來塑造或重塑DOM的結構。結構指令很容易識別。通常會有星號(*)在指令屬性名稱前面。

以下是三種常件的結構指令的範例

<div *ngIf="hero" >{{hero.name}}</div>

<ul>
  <li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>
<!--hero?.emotion指的是hero.emotion,?代表如果hero為空時不會因此引發null reference exception-->
<div &#91;ngSwitch&#93;="hero?.emotion">
  <app-happy-hero    *ngSwitchCase="'happy'"    &#91;hero&#93;="hero"></app-happy-hero>
  <app-sad-hero      *ngSwitchCase="'sad'"      &#91;hero&#93;="hero"></app-sad-hero>
  <app-confused-hero *ngSwitchCase="'app-confused'" &#91;hero&#93;="hero"></app-confused-hero>
  <app-unknown-hero  *ngSwitchDefault           &#91;hero&#93;="hero"></app-unknown-hero>
</div>

一個attribute directive改變了元素的外觀或行為。例如,內置的NgStyle指令同時改變幾個元素樣式。
而Structural Directives則是會自動將裡面的內容儲存成一個ng-template並且操縱它,這也是為什麼這些指令前面會有一個星號(*)。
其實這個星號會悄悄的讓這個directive成為structure的directive
例如

<div *ngIf="hero" >{{hero.name}}</div>

其實等同於

<ng-template &#91;ngIf&#93;="hero">
  <div>{{hero.name}}</div>
</ng-template>

我們可以觀察到

  • *號將該ngIf改為一個屬性綁定的元素,並綁定一個ng-template。*ngIf <ng-template> [ngIf]
  • 剩下的部分<div>,包括它的class屬性,移到了<ng-template>元素之下。

我們可以將許多屬性指令寫在同一個host element上,但同一個host element只能夠有一個結構指令。

ng-template

從上面*所做的事情,我們可以知道,結構型指令是建立在ng-template之上的。
一個ng-template並不會一開始就顯示在畫面上,而是通過directive去操作裡面的dom並將要顯示的template添加在dom之中。

因此,*ngIf隱藏掉的物件,和我們使用css去show、hide在意義上是完全不同的。
因為它已經不在dom之上,是沒辦法被操作的。

不過由於若是網頁內的資料量大,angular有足夠的理由這樣做。
因為這可以避免過多的dom元素拖累網頁效能,若單純使用css去hide、show元素,所有的監聽器、物件依舊會在背景執行,這會讓效能變得不佳。
如果我們需要在show、hide物件的同時執行一些特殊的指令,可以用Lifecycle Hooks來撰寫此時要做的事情。

如何套用多個結構指令在元件裡

上面有提到,我們可以將許多屬性指令寫在同一個host element上,但同一個host element只能夠有一個結構指令。
所以在一般的狀態下,如果需要兩個標籤,我們會將HTML利用一些不會影響結構的標籤來做多層的Structural Directives控制

<div *ngIf="hero"><span *ngFor="hero of heroes">{{hero.name}} </span></div>

但有時候該狀況不允許任何多餘的標籤在裡面,例如下拉選單select。
若有一個區域選單select裡面的option內容要由*ngFor來產生,但是當city未選擇時,select又希望不要有任何下拉選單,
這時後我們在option上的確同時會需要放許多個結構指令,但ANGULAR不允許同一個標籤上放兩個結構指令。

如果我們多包一層span去包裡面的內容,會發現因為select內不允許span標籤,會造成讀不到option下拉選單,即便已經選擇了city

<div>
  Pick your favorite hero
  (<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
</div>
<select &#91;(ngModel)&#93;="hero">
  <span *ngFor="let h of heroes">
    <span *ngIf="showSad || h.emotion !== 'sad'">
      <option &#91;ngValue&#93;="h">{{h.name}} ({{h.emotion}})</option>
    </span>
  </span>
</select>


這時候就可以改用<ng-container>

<select &#91;(ngModel)&#93;="hero">
  <ng-container *ngFor="let h of heroes">
    <ng-container *ngIf="showSad || h.emotion !== 'sad'">
      <option &#91;ngValue&#93;="h">{{h.name}} ({{h.emotion}})</option>
    </ng-container>
  </ng-container>
</select>

就可以正常顯示了

自製一個結構Directive

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

/**
 * Add the template content to the DOM unless the condition is true.
 */
@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
  private hasView = false;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef) { }

  @Input() set appUnless(condition: boolean) {
    if (!condition && !this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (condition && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}

最重要的地方是在@Input那段,展示了如何去操作dom
使用的地方如下:

<p *appUnless="condition" class="unless a">
  (A) This paragraph is displayed because the condition is false.
</p>

完整範例請見:live example / download example


17年資歷女工程師,專精於動畫、影像辨識以及即時串流程式開發。經常組織活動,邀請優秀的女性分享她們的技術專長,並在眾多場合分享自己的技術知識,也活躍於非營利組織,辦理活動來支持特殊兒及其家庭。期待用技術改變世界。

如果你認同我或想支持我的努力,歡迎請我喝一杯咖啡!讓我更有動力分享知識!