填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫Home

結構型指令

Structural directives

本章將看看 Angular 如何用結構型指令操縱 DOM 樹,以及你該如何寫自己的結構型指令來完成同樣的任務。

This guide looks at how Angular manipulates the DOM with structural directives and how you can write your own structural directives to do the same thing.

試試現場演練 / 下載範例

Try the現場演練 / 下載範例.

什麼是結構型指令?

What are structural directives?

結構型指令的職責是 HTML 佈局。 它們塑造或重塑 DOM 的結構,比如新增、移除或維護這些元素。

Structural directives are responsible for HTML layout. They shape or reshape the DOM's structure, typically by adding, removing, or manipulating elements.

像其它指令一樣,你可以把結構型指令應用到一個宿主元素上。 然後它就可以對宿主元素及其子元素做點什麼。

As with other directives, you apply a structural directive to a host element. The directive then does whatever it's supposed to do with that host element and its descendants.

結構型指令非常容易識別。 在這個例子中,星號(*)被放在指令的屬性名之前。

Structural directives are easy to recognize. An asterisk (*) precedes the directive attribute name as in this example.

<div *ngIf="hero" class="name">{{hero.name}}</div>
src/app/app.component.html (ngif)
      
      <div *ngIf="hero" class="name">{{hero.name}}</div>
    

沒有方括號,沒有圓括號,只是把 *ngIf 設定為一個字串。

No brackets. No parentheses. Just *ngIf set to a string.

在這個例子中,你將學到星號(*)這個簡寫方法,而這個字串是一個微語法,而不是通常的範本表示式。 Angular 會解開這個語法糖,變成一個 <ng-template> 標記,包裹著宿主元素及其子元素。 每個結構型指令都可以用這個範本做點不同的事情。

You'll learn in this guide that the asterisk (*) is a convenience notation and the string is a microsyntax rather than the usual template expression. Angular desugars this notation into a marked-up <ng-template> that surrounds the host element and its descendants. Each structural directive does something different with that template.

三個常用的內建結構型指令 —— NgIfNgForNgSwitch...。 你在範本語法一章中學過它,並且在 Angular 文件的例子中到處都在用它。下面是範本中的例子:

Three of the common, built-in structural directives—NgIf, NgFor, and NgSwitch...—are described in the Built-in directives guide and seen in samples throughout the Angular documentation. Here's an example of them in a template:

<div *ngIf="hero" class="name">{{hero.name}}</div> <ul> <li *ngFor="let hero of heroes">{{hero.name}}</li> </ul> <div [ngSwitch]="hero?.emotion"> <app-happy-hero *ngSwitchCase="'happy'" [hero]="hero"></app-happy-hero> <app-sad-hero *ngSwitchCase="'sad'" [hero]="hero"></app-sad-hero> <app-confused-hero *ngSwitchCase="'confused'" [hero]="hero"></app-confused-hero> <app-unknown-hero *ngSwitchDefault [hero]="hero"></app-unknown-hero> </div>
src/app/app.component.html (built-in)
      
      <div *ngIf="hero" class="name">{{hero.name}}</div>

<ul>
  <li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>

<div [ngSwitch]="hero?.emotion">
  <app-happy-hero    *ngSwitchCase="'happy'"    [hero]="hero"></app-happy-hero>
  <app-sad-hero      *ngSwitchCase="'sad'"      [hero]="hero"></app-sad-hero>
  <app-confused-hero *ngSwitchCase="'confused'" [hero]="hero"></app-confused-hero>
  <app-unknown-hero  *ngSwitchDefault           [hero]="hero"></app-unknown-hero>
</div>
    

本章不會重複講如何使用它們,而是解釋它們的工作原理以及如何寫自己的結構型指令

This guide won't repeat how to use them. But it does explain how they work and how to write your own structural directive.

指令的拼寫形式
Directive spelling

在本章中,你將看到指令同時具有兩種拼寫形式大駝峰 UpperCamelCase 和小駝峰 lowerCamelCase,比如你已經看過的 NgIfngIf。 這裡的原因在於,NgIf 參考的是指令的類別名稱,而 ngIf 參考的是指令的屬性名*。

Throughout this guide, you'll see a directive spelled in both UpperCamelCase and lowerCamelCase. Already you've seen NgIf and ngIf. There's a reason. NgIf refers to the directive class; ngIf refers to the directive's attribute name.

指令的類別名稱拼寫成大駝峰形式NgIf),而它的屬性名則拼寫成小駝峰形式ngIf)。 本章會在談論指令的屬性和工作原理時參考指令的類別名稱,在描述如何在 HTML 範本中把該指令應用到元素時,參考指令的屬性名

A directive class is spelled in UpperCamelCase (NgIf). A directive's attribute name is spelled in lowerCamelCase (ngIf). The guide refers to the directive class when talking about its properties and what the directive does. The guide refers to the attribute name when describing how you apply the directive to an element in the HTML template.

還有另外兩種 Angular 指令,在本開發指南的其它地方有講解:(1) 元件 (2) 屬性型指令。

There are two other kinds of Angular directives, described extensively elsewhere: (1) components and (2) attribute directives.

元件可以在原生 HTML 元素中管理一小片區域的 HTML。從技術角度說,它就是一個帶範本的指令。

A component manages a region of HTML in the manner of a native HTML element. Technically it's a directive with a template.

屬性型指令會改變某個元素、元件或其它指令的外觀或行為。 比如,內建的NgStyle指令可以同時修改元素的多個樣式。

An attribute directive changes the appearance or behavior of an element, component, or another directive. For example, the built-in NgStyledirective changes several element styles at the same time.

你可以在一個宿主元素上應用多個屬性型指令,但只能應用一個結構型指令。

You can apply many attribute directives to one host element. You can only apply one structural directive to a host element.

NgIf 案例分析

NgIf case study

NgIf 是一個很好的結構型指令案例:它接受一個布林值,並據此讓一整塊 DOM 樹出現或消失。

NgIf is the simplest structural directive and the easiest to understand. It takes a boolean expression and makes an entire chunk of the DOM appear or disappear.

<p *ngIf="true"> Expression is true and ngIf is true. This paragraph is in the DOM. </p> <p *ngIf="false"> Expression is false and ngIf is false. This paragraph is not in the DOM. </p>
src/app/app.component.html (ngif-true)
      
      <p *ngIf="true">
  Expression is true and ngIf is true.
  This paragraph is in the DOM.
</p>
<p *ngIf="false">
  Expression is false and ngIf is false.
  This paragraph is not in the DOM.
</p>
    

ngIf 指令並不是使用 CSS 來隱藏元素的。它會把這些元素從 DOM 中物理刪除。 使用瀏覽器的開發者工具就可以確認這一點。

The ngIf directive doesn't hide elements with CSS. It adds and removes them physically from the DOM. Confirm that fact using browser developer tools to inspect the DOM.

可以看到第一段文字出現在了 DOM 中,而第二段則沒有,在第二段的位置上是一個關於“繫結”的註釋(稍後有更多講解)。

The top paragraph is in the DOM. The bottom, disused paragraph is not; in its place is a comment about "bindings" (more about that later).

當條件為假時,NgIf 會從 DOM 中移除它的宿主元素,取消它監聽過的那些 DOM 事件,從 Angular 變更檢測中移除該元件,並銷燬它。 這些元件和 DOM 節點可以被當做垃圾收集起來,並且釋放它們佔用的記憶體。

When the condition is false, NgIf removes its host element from the DOM, detaches it from DOM events (the attachments that it made), detaches the component from Angular change detection, and destroys it. The component and DOM nodes can be garbage-collected and free up memory.

為什麼移除而不是隱藏

Why remove rather than hide?

指令也可以透過把它的 display 風格設定為 none 而隱藏不需要的段落。

A directive could hide the unwanted paragraph instead by setting its display style to none.

<p [style.display]="'block'"> Expression sets display to "block". This paragraph is visible. </p> <p [style.display]="'none'"> Expression sets display to "none". This paragraph is hidden but still in the DOM. </p>
src/app/app.component.html (display-none)
      
      <p [style.display]="'block'">
  Expression sets display to "block".
  This paragraph is visible.
</p>
<p [style.display]="'none'">
  Expression sets display to "none".
  This paragraph is hidden but still in the DOM.
</p>
    

當不可見時,這個元素仍然留在 DOM 中。

While invisible, the element remains in the DOM.

對於簡單的段落,隱藏和移除之間的差異影響不大,但對於資源佔用較多的元件是不一樣的。 當隱藏掉一個元素時,元件的行為還在繼續 —— 它仍然附加在它所屬的 DOM 元素上, 它也仍在監聽事件。Angular 會繼續檢查哪些能影響資料繫結的變更。 元件原本要做的那些事情仍在繼續。

The difference between hiding and removing doesn't matter for a simple paragraph. It does matter when the host element is attached to a resource intensive component. Such a component's behavior continues even when hidden. The component stays attached to its DOM element. It keeps listening to events. Angular keeps checking for changes that could affect data bindings. Whatever the component was doing, it keeps doing.

雖然不可見,元件及其各級子元件仍然佔用著資源,而這些資源如果分配給別人可能會更有用。 在效能和記憶體方面的負擔相當可觀,響應度會降低,而使用者卻可能無法從中受益。

Although invisible, the component—and all of its descendant components—tie up resources. The performance and memory burden can be substantial, responsiveness can degrade, and the user sees nothing.

當然,從積極的一面看,重新顯示這個元素會非常快。 元件以前的狀態被保留著,並隨時可以顯示。 元件不用重新初始化 —— 該操作可能會比較昂貴。 這時候隱藏和顯示就成了正確的選擇。

On the positive side, showing the element again is quick. The component's previous state is preserved and ready to display. The component doesn't re-initialize—an operation that could be expensive. So hiding and showing is sometimes the right thing to do.

但是,除非有非常強烈的理由來保留它們,否則你會更傾向於移除使用者看不見的那些 DOM 元素,並且使用 NgIf 這樣的結構型指令來收回用不到的資源。

But in the absence of a compelling reason to keep them around, your preference should be to remove DOM elements that the user can't see and recover the unused resources with a structural directive like NgIf .

同樣的考量也適用於每一個結構型指令,無論是內建的還是自訂的。 你應該提醒自己慎重考慮新增元素、移除元素以及建立和銷燬元件的後果。

These same considerations apply to every structural directive, whether built-in or custom. Before applying a structural directive, you might want to pause for a moment to consider the consequences of adding and removing elements and of creating and destroying components.

星號(*)字首

The asterisk (*) prefix

你可能注意到了指令名的星號(*)字首,並且困惑於為什麼需要它以及它是做什麼的。

Surely you noticed the asterisk (*) prefix to the directive name and wondered why it is necessary and what it does.

這裡的 *ngIf 會在 hero 存在時顯示英雄的名字。

Here is *ngIf displaying the hero's name if hero exists.

<div *ngIf="hero" class="name">{{hero.name}}</div>
src/app/app.component.html (asterisk)
      
      <div *ngIf="hero" class="name">{{hero.name}}</div>
    

星號是一個用來簡化更復雜語法的“語法糖”。 從內部實現來說,Angular 把 *ngIf 屬性 翻譯成一個 <ng-template> 元素 並用它來包裹宿主元素,程式碼如下:

The asterisk is "syntactic sugar" for something a bit more complicated. Internally, Angular translates the *ngIf attribute into a <ng-template> element, wrapped around the host element, like this.

<ng-template [ngIf]="hero"> <div class="name">{{hero.name}}</div> </ng-template>
src/app/app.component.html (ngif-template)
      
      <ng-template [ngIf]="hero">
  <div class="name">{{hero.name}}</div>
</ng-template>
    
  • *ngIf 指令被移到了 <ng-template> 元素上。在那裡它變成了一個屬性繫結 [ngIf]

    The *ngIf directive moved to the <ng-template> element where it became a property binding,[ngIf].

  • <div> 上的其餘部分,包括它的 class 屬性在內,移到了內部的 <ng-template> 元素上。

    The rest of the <div>, including its class attribute, moved inside the <ng-template> element.

第一種形態永遠不會真的渲染出來。 只有最終產出的結果才會出現在 DOM 中。

The first form is not actually rendered, only the finished product ends up in the DOM.

Angular 會在真正渲染的時候填充 <ng-template> 的內容,並且把 <ng-template> 替換為一個供診斷用的註釋。

Angular consumed the <ng-template> content during its actual rendering and replaced the <ng-template> with a diagnostic comment.

NgForNgSwitch...指令也都遵循同樣的模式。

The NgForand NgSwitch...directives follow the same pattern.

*ngFor 內幕

Inside *ngFor

Angular 會把 *ngFor 用同樣的方式把星號(*)語法的 template屬性轉換成 <ng-template>元素

Angular transforms the *ngFor in similar fashion from asterisk (*) syntax to <ng-template> element.

這裡有一個 NgFor 的全特性應用,同時用了這兩種寫法:

Here's a full-featured application of NgFor, written both ways:

<div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd"> ({{i}}) {{hero.name}} </div> <ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById"> <div [class.odd]="odd">({{i}}) {{hero.name}}</div> </ng-template>
src/app/app.component.html (inside-ngfor)
      
      <div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd">
  ({{i}}) {{hero.name}}
</div>

<ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById">
  <div [class.odd]="odd">({{i}}) {{hero.name}}</div>
</ng-template>
    

它明顯比 ngIf 複雜得多,確實如此。 NgFor 指令比本章展示過的 NgIf 具有更多的必選特性和可選特性。 至少 NgFor 會需要一個迴圈變數(let hero)和一個列表(heroes)。

This is manifestly more complicated than ngIf and rightly so. The NgFor directive has more features, both required and optional, than the NgIf shown in this guide. At minimum NgFor needs a looping variable (let hero) and a list (heroes).

你可以透過把一個字串賦值給 ngFor 來啟用這些特性,這個字串使用 Angular 的微語法

You enable these features in the string assigned to ngFor, which you write in Angular's microsyntax.

ngFor 字串之外的每一樣東西都會留在宿主元素(<div>)上,也就是說它移到了 <ng-template> 內部。 在這個例子中,[class.odd]="odd" 留在了 <div> 上。

Everything outside the ngFor string stays with the host element (the <div>) as it moves inside the <ng-template>. In this example, the [class.odd]="odd" stays on the <div>.

微語法

Microsyntax

Angular 微語法能讓你透過簡短的、友好的字串來配置一個指令。 微語法解析器把這個字串翻譯成 <ng-template> 上的屬性:

The Angular microsyntax lets you configure a directive in a compact, friendly string. The microsyntax parser translates that string into attributes on the <ng-template>:

  • let 關鍵字宣告一個範本輸入變數,你會在範本中參考它。本例子中,這個輸入變數就是 heroiodd。 解析器會把 let herolet ilet odd 翻譯成命名變數 let-herolet-ilet-odd

    The let keyword declares a template input variable that you reference within the template. The input variables in this example are hero, i, and odd. The parser translates let hero, let i, and let odd into variables named let-hero, let-i, and let-odd.

  • 微語法解析器接收 oftrackby,把它們首字母大寫(of -> Of, trackBy -> TrackBy), 並且給它們加上指令的屬性名(ngFor)字首,最終產生的名字是 ngForOfngForTrackBy。 這兩個最終產生的名字是 NgFor輸入屬性,指令據此瞭解到列表是 heroes,而 track-by 函式是 trackById

    The microsyntax parser title-cases all directives and prefixes them with the directive's attribute name, such as ngFor. For example, the ngFor input properties, of and trackBy, become ngForOf and ngForTrackBy, respectively. That's how the directive learns that the list is heroes and the track-by function is trackById.

  • NgFor 指令在列表上迴圈,每個迴圈中都會設定和重置它自己的上下文物件上的屬性。 這些屬性包括但不限於 indexodd 以及一個特殊的屬性名 $implicit(隱式變數)。

    As the NgFor directive loops through the list, it sets and resets properties of its own context object. These properties can include, but aren't limited to, index, odd, and a special property named $implicit.

  • let-ilet-odd 變數是透過 let i=indexlet odd=odd 來定義的。 Angular 把它們設定為上下文物件中的 indexodd 屬性的當前值。

    The let-i and let-odd variables were defined as let i=index and let odd=odd. Angular sets them to the current value of the context's index and odd properties.

  • 這裡並沒有指定 let-hero 的上下文屬性。它的來源是隱式的。 Angular 將 let-hero 設定為此上下文中 $implicit 屬性的值, 它是由 NgFor 用當前迭代中的英雄初始化的。

    The context property for let-hero wasn't specified. Its intended source is implicit. Angular sets let-hero to the value of the context's $implicit property, which NgFor has initialized with the hero for the current iteration.

  • API 參考手冊中描述了 NgFor 指令的其它屬性和上下文屬性。

    The NgFor API guide describes additional NgFor directive properties and context properties.

  • NgForOf 指令實現了 NgFor。請到 NgForOf API 參考手冊中瞭解 NgForOf 指令的更多屬性及其上下文屬性。

    The NgForOf directive implements NgFor. Read more about additional NgForOf directive properties and context properties in the NgForOf API reference.

編寫你自己的結構型指令

Writing your own structural directives

當你編寫自己的結構型指令時,也可以利用這些微語法機制。 例如,Angular 中的微語法允許你寫成 <div *ngFor="let item of items">{{item}}</div> 而不是 <ng-template ngFor let-item [ngForOf]="items"><div>{{item}}</div></ng-template>。 以下各節提供了有關約束、語法和微語法翻譯方式的詳細資訊。

These microsyntax mechanisms are also available to you when you write your own structural directives. For example, microsyntax in Angular allows you to write <div *ngFor="let item of items">{{item}}</div> instead of <ng-template ngFor let-item [ngForOf]="items"><div>{{item}}</div></ng-template>. The following sections provide detailed information on constraints, grammar, and translation of microsyntax.

約束

Constraints

微語法必須滿足以下要求:

Microsyntax must meet the following requirements:

  • 它必須可被預先了解,以便 IDE 可以解析它而無需知道指令的底層語義或已存在哪些指令。

    It must be known ahead of time so that IDEs can parse it without knowing the underlying semantics of the directive or what directives are present.

  • 它必須轉換為 DOM 中的“鍵-值”屬性。

    It must translate to key-value attributes in the DOM.

語法

Grammar

當你編寫自己的結構型指令時,請使用以下語法:

When you write your own structural directives, use the following grammar:

*:prefix="( :let | :expression ) (';' | ',')? ( :let | :as | :keyExp )*"
      
      *:prefix="( :let | :expression ) (';' | ',')? ( :let | :as | :keyExp )*"
    

下表描述了微語法的每個組成部分。

The following tables describe each portion of the microsyntax grammar.

prefix

HTML 屬性鍵(attribute key)

HTML attribute key

key

HTML 屬性鍵(attribute key)

HTML attribute key

local

範本中使用的區域性變數名

local variable name used in the template

export

指令使用指定名稱匯出的值

value exported by the directive under a given name

expression

標準 Angular 表示式

standard Angular expression

keyExp = :key ":"? :expression ("as" :local)? ";"?
let = "let" :local "=" :export ";"?
as = :export "as" :local ";"?

翻譯

Translation

將微語法轉換為常規的繫結語法,如下所示:

A microsyntax is translated to the normal binding syntax as follows:

微語法

Microsyntax

翻譯結果

Translation

prefix 和裸表示式

prefix and naked expression

[prefix]="expression"
keyExp

[prefixKey] "表示式" (let-prefixKey="export")
注意 prefix 已經加到了 key 的前面

[prefixKey] "expression" (let-prefixKey="export")
Notice that the prefix is added to the key

letlet-local="export"

微語法樣例

Microsyntax examples

下表說明了 Angular 會如何解開微語法。

The following table demonstrates how Angular desugars microsyntax.

微語法

Microsyntax

解語法糖後

Desugared

*ngFor="let item of [1,2,3]"<ng-template ngFor let-item [ngForOf]="[1,2,3]">
*ngFor="let item of [1,2,3] as items; trackBy: myTrack; index as i"<ng-template ngFor let-item [ngForOf]="[1,2,3]" let-items="ngForOf" [ngForTrackBy]="myTrack" let-i="index">
*ngIf="exp"<ng-template [ngIf]="exp">
*ngIf="exp as value"<ng-template [ngIf]="exp" let-value="ngIf">

這些微語法機制在你寫自己的結構型指令時也同樣有效,參考 NgIf 的原始碼NgFor 的原始碼 可以學到更多。

Studying the source code for NgIfand NgForOfis a great way to learn more.

範本輸入變數

Template input variable

範本輸入變數是這樣一種變數,你可以在單個實例的範本中參考它的值。 這個例子中有好幾個範本輸入變數:heroiodd。 它們都是用 let 作為前導關鍵字。

A template input variable is a variable whose value you can reference within a single instance of the template. There are several such variables in this example: hero, i, and odd. All are preceded by the keyword let.

範本輸入變數範本參考變數不同的,無論是在語義上還是語法上。

A template input variable is not the same as a template reference variable, neither semantically nor syntactically.

你使用 let 關鍵字(如 let hero)在範本中宣告一個範本輸入變數。 這個變數的範圍被限制在所重複範本的單一實例上。 事實上,你可以在其它結構型指令中使用同樣的變數名。

You declare a template input variable using the let keyword (let hero). The variable's scope is limited to a single instance of the repeated template. You can use the same variable name again in the definition of other structural directives.

而宣告範本參考變數使用的是給變數名加 # 字首的方式(#var)。 一個參考變數參考的是它所附著到的元素、元件或指令。它可以在整個範本任意位置訪問。

You declare a template reference variable by prefixing the variable name with # (#var). A reference variable refers to its attached element, component or directive. It can be accessed anywhere in the entire template.

範本輸入變數和參考變數具有各自獨立的名稱空間。let hero 中的 hero#hero 中的 hero 並不是同一個變數。

Template input and reference variable names have their own namespaces. The hero in let hero is never the same variable as the hero declared as #hero.

每個宿主元素上只能有一個結構型指令

One structural directive per host element

有時你會希望只有當特定的條件為真時才重複渲染一個 HTML 塊。 你可能試過把 *ngFor*ngIf 放在同一個宿主元素上,但 Angular 不允許。這是因為你在一個元素上只能放一個結構型指令。

Someday you'll want to repeat a block of HTML but only when a particular condition is true. You'll try to put both an *ngFor and an *ngIf on the same host element. Angular won't let you. You may apply only one structural directive to an element.

原因很簡單。結構型指令可能會對宿主元素及其子元素做很複雜的事。當兩個指令放在同一個元素上時,誰先誰後?NgIf 優先還是 NgFor 優先?NgIf 可以取消 NgFor 的效果嗎? 如果要這樣做,Angular 應該如何把這種能力泛化,以取消其它結構型指令的效果呢?

The reason is simplicity. Structural directives can do complex things with the host element and its descendents. When two directives lay claim to the same host element, which one takes precedence? Which should go first, the NgIf or the NgFor? Can the NgIf cancel the effect of the NgFor? If so (and it seems like it should be so), how should Angular generalize the ability to cancel for other structural directives?

對這些問題,沒有辦法簡單回答。而禁止多個結構型指令則可以簡單地解決這個問題。 這種情況下有一個簡單的解決方案:把 *ngIf 放在一個"容器"元素上,再包裝進 *ngFor 元素。 這個元素可以使用ng-container,以免引入一個新的 HTML 層級。

There are no easy answers to these questions. Prohibiting multiple structural directives makes them moot. There's an easy solution for this use case: put the *ngIf on a container element that wraps the *ngFor element. One or both elements can be an ng-containerso you don't have to introduce extra levels of HTML.

NgSwitch 內幕

Inside NgSwitch directives

Angular 的 NgSwitch 實際上是一組相互合作的指令:NgSwitchNgSwitchCaseNgSwitchDefault

The Angular NgSwitch is actually a set of cooperating directives: NgSwitch, NgSwitchCase, and NgSwitchDefault.

例子如下:

Here's an example.

<div [ngSwitch]="hero?.emotion"> <app-happy-hero *ngSwitchCase="'happy'" [hero]="hero"></app-happy-hero> <app-sad-hero *ngSwitchCase="'sad'" [hero]="hero"></app-sad-hero> <app-confused-hero *ngSwitchCase="'confused'" [hero]="hero"></app-confused-hero> <app-unknown-hero *ngSwitchDefault [hero]="hero"></app-unknown-hero> </div>
src/app/app.component.html (ngswitch)
      
      <div [ngSwitch]="hero?.emotion">
  <app-happy-hero    *ngSwitchCase="'happy'"    [hero]="hero"></app-happy-hero>
  <app-sad-hero      *ngSwitchCase="'sad'"      [hero]="hero"></app-sad-hero>
  <app-confused-hero *ngSwitchCase="'confused'" [hero]="hero"></app-confused-hero>
  <app-unknown-hero  *ngSwitchDefault           [hero]="hero"></app-unknown-hero>
</div>
    

一個值(hero.emotion)被被賦值給了 NgSwitch,以決定要顯示哪一個分支。

The switch value assigned to NgSwitch (hero.emotion) determines which (if any) of the switch cases are displayed.

NgSwitch 本身不是結構型指令,而是一個屬性型指令,它控制其它兩個 switch 指令的行為。 這也就是為什麼你要寫成 [ngSwitch] 而不是 *ngSwitch 的原因。

NgSwitch itself is not a structural directive. It's an attribute directive that controls the behavior of the other two switch directives. That's why you write [ngSwitch], never *ngSwitch.

NgSwitchCaseNgSwitchDefault 都是結構型指令。 因此你要使用星號(*)字首來把它們附著到元素上。 NgSwitchCase 會在它的值匹配上選項值的時候顯示它的宿主元素。 NgSwitchDefault 則會當沒有兄弟 NgSwitchCase 匹配上時顯示它的宿主元素。

NgSwitchCase and NgSwitchDefault are structural directives. You attach them to elements using the asterisk (*) prefix notation. An NgSwitchCase displays its host element when its value matches the switch value. The NgSwitchDefault displays its host element when no sibling NgSwitchCase matches the switch value.

指令所在的元素就是它的宿主元素。 <happy-hero>*ngSwitchCase 的宿主元素。 <unknown-hero>*ngSwitchDefault 的宿主元素。

The element to which you apply a directive is its host element. The <happy-hero> is the host element for the happy *ngSwitchCase. The <unknown-hero> is the host element for the *ngSwitchDefault.

像其它的結構型指令一樣,NgSwitchCaseNgSwitchDefault 也可以解開語法糖,變成 <ng-template> 的形式。

As with other structural directives, the NgSwitchCase and NgSwitchDefault can be desugared into the <ng-template> element form.

<div [ngSwitch]="hero?.emotion"> <ng-template [ngSwitchCase]="'happy'"> <app-happy-hero [hero]="hero"></app-happy-hero> </ng-template> <ng-template [ngSwitchCase]="'sad'"> <app-sad-hero [hero]="hero"></app-sad-hero> </ng-template> <ng-template [ngSwitchCase]="'confused'"> <app-confused-hero [hero]="hero"></app-confused-hero> </ng-template > <ng-template ngSwitchDefault> <app-unknown-hero [hero]="hero"></app-unknown-hero> </ng-template> </div>
src/app/app.component.html (ngswitch-template)
      
      <div [ngSwitch]="hero?.emotion">
  <ng-template [ngSwitchCase]="'happy'">
    <app-happy-hero [hero]="hero"></app-happy-hero>
  </ng-template>
  <ng-template [ngSwitchCase]="'sad'">
    <app-sad-hero [hero]="hero"></app-sad-hero>
  </ng-template>
  <ng-template [ngSwitchCase]="'confused'">
    <app-confused-hero [hero]="hero"></app-confused-hero>
  </ng-template >
  <ng-template ngSwitchDefault>
    <app-unknown-hero [hero]="hero"></app-unknown-hero>
  </ng-template>
</div>
    

優先使用星號(*)語法

Prefer the asterisk (*) syntax.

星號(*)語法比不帶語法糖的形式更加清晰。 如果找不到單一的元素來應用該指令,可以使用<ng-container>作為該指令的容器。

The asterisk (*) syntax is more clear than the desugared form. Use <ng-container> when there's no single element to host the directive.

雖然很少有理由在範本中使用結構型指令的屬性形式和元素形式,但這些幕後知識仍然是很重要的,即:Angular 會建立 <ng-template>,還要了解它的工作原理。 當需要寫自己的結構型指令時,你就要使用 <ng-template>

While there's rarely a good reason to apply a structural directive in template attribute or element form, it's still important to know that Angular creates a <ng-template> and to understand how it works. You'll refer to the <ng-template> when you write your own structural directive.

<ng-template>元素

The <ng-template>

<ng-template>是一個 Angular 元素,用來渲染 HTML。 它永遠不會直接顯示出來。 事實上,在渲染檢視之前,Angular 會把 <ng-template> 及其內容替換為一個註釋。

The <ng-template> is an Angular element for rendering HTML. It is never displayed directly. In fact, before rendering the view, Angular replaces the <ng-template> and its contents with a comment.

如果沒有使用結構型指令,而僅僅把一些別的元素包裝進 <ng-template> 中,那些元素就是不可見的。 在下面的這個短語"Hip! Hip! Hooray!"中,中間的這個 "Hip!"(歡呼聲) 就是如此。

If there is no structural directive and you merely wrap some elements in a <ng-template>, those elements disappear. That's the fate of the middle "Hip!" in the phrase "Hip! Hip! Hooray!".

<p>Hip!</p> <ng-template> <p>Hip!</p> </ng-template> <p>Hooray!</p>
src/app/app.component.html (template-tag)
      
      <p>Hip!</p>
<ng-template>
  <p>Hip!</p>
</ng-template>
<p>Hooray!</p>
    

Angular 抹掉了中間的那個 "Hip!",讓歡呼聲顯得不再那麼熱烈了。

Angular erases the middle "Hip!", leaving the cheer a bit less enthusiastic.

結構型指令會讓 <ng-template> 正常工作,在你寫自己的結構型指令時就會看到這一點。

A structural directive puts a <ng-template> to work as you'll see when you write your own structural directive.

使用<ng-container>把一些兄弟元素歸為一組

Group sibling elements with <ng-container>

通常都需要一個元素作為結構型指令的宿主。 列表元素(<li>)就是一個典型的供 NgFor 使用的宿主元素。

There's often a root element that can and should host the structural directive. The list element (<li>) is a typical host element of an NgFor repeater.

<li *ngFor="let hero of heroes">{{hero.name}}</li>
src/app/app.component.html (ngfor-li)
      
      <li *ngFor="let hero of heroes">{{hero.name}}</li>
    

當沒有這樣一個單一的宿主元素時,你就可以把這些內容包裹在一個原生的 HTML 容器元素中,比如 <div>,並且把結構型指令附加到這個"包裹"上。

When there isn't a host element, you can usually wrap the content in a native HTML container element, such as a <div>, and attach the directive to that wrapper.

<div *ngIf="hero" class="name">{{hero.name}}</div>
src/app/app.component.html (ngif)
      
      <div *ngIf="hero" class="name">{{hero.name}}</div>
    

但引入另一個容器元素(通常是 <span><div>)來把一些元素歸到一個單一的根元素下,通常也會帶來問題。注意,是"通常"而不是"總會"。

Introducing another container element—typically a <span> or <div>—to group the elements under a single root is usually harmless. Usually ... but not always.

這種用於分組的元素可能會破壞範本的外觀表現,因為 CSS 的樣式既不曾期待也不會接受這種新的元素佈局。 比如,假設你有下列分段佈局。

The grouping element may break the template appearance because CSS styles neither expect nor accommodate the new layout. For example, suppose you have the following paragraph layout.

<p> I turned the corner <span *ngIf="hero"> and saw {{hero.name}}. I waved </span> and continued on my way. </p>
src/app/app.component.html (ngif-span)
      
      <p>
  I turned the corner
  <span *ngIf="hero">
    and saw {{hero.name}}. I waved
  </span>
  and continued on my way.
</p>
    

而你的 CSS 樣式規則是應用於 <p> 元素下的 <span> 的。

You also have a CSS style rule that happens to apply to a <span> within a <p>aragraph.

p span { color: red; font-size: 70%; }
src/app/app.component.css (p-span)
      
      p span { color: red; font-size: 70%; }
    

這樣渲染出來的段落就會非常奇怪。

The constructed paragraph renders strangely.

本來為其它地方準備的 p span 樣式,被意外的應用到了這裡。

The p span style, intended for use elsewhere, was inadvertently applied here.

另一個問題是:有些 HTML 元素需要所有的直屬下級都具有特定的型別。 比如,<select> 元素要求直屬下級必須為 <option>,那就沒辦法把這些選項包裝進 <div><span> 中。

Another problem: some HTML elements require all immediate children to be of a specific type. For example, the <select> element requires <option> children. You can't wrap the options in a conditional <div> or a <span>.

如果這樣做:

When you try this,

<div> Pick your favorite hero (<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>) </div> <select [(ngModel)]="hero"> <span *ngFor="let h of heroes"> <span *ngIf="showSad || h.emotion !== 'sad'"> <option [ngValue]="h">{{h.name}} ({{h.emotion}})</option> </span> </span> </select>
src/app/app.component.html (select-span)
      
      <div>
  Pick your favorite hero
  (<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
</div>
<select [(ngModel)]="hero">
  <span *ngFor="let h of heroes">
    <span *ngIf="showSad || h.emotion !== 'sad'">
      <option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
    </span>
  </span>
</select>
    

下拉列表就是空的。

the drop down is empty.

瀏覽器不會顯示 <span> 中的 <option>

The browser won't display an <option> within a <span>.

<ng-container> 的救贖

<ng-container> to the rescue

Angular 的 <ng-container> 是一個分組元素,但它不會汙染樣式或元素佈局,因為 Angular 壓根不會把它放進 DOM 中。

The Angular <ng-container> is a grouping element that doesn't interfere with styles or layout because Angular doesn't put it in the DOM.

下面是重新實現的條件化段落,這次使用 <ng-container>

Here's the conditional paragraph again, this time using <ng-container>.

<p> I turned the corner <ng-container *ngIf="hero"> and saw {{hero.name}}. I waved </ng-container> and continued on my way. </p>
src/app/app.component.html (ngif-ngcontainer)
      
      <p>
  I turned the corner
  <ng-container *ngIf="hero">
    and saw {{hero.name}}. I waved
  </ng-container>
  and continued on my way.
</p>
    

這次就渲染對了。

It renders properly.

現在用 <ng-container> 來根據條件排除選擇框中的某個 <option>

Now conditionally exclude a select <option> with <ng-container>.

<div> Pick your favorite hero (<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>) </div> <select [(ngModel)]="hero"> <ng-container *ngFor="let h of heroes"> <ng-container *ngIf="showSad || h.emotion !== 'sad'"> <option [ngValue]="h">{{h.name}} ({{h.emotion}})</option> </ng-container> </ng-container> </select>
src/app/app.component.html (select-ngcontainer)
      
      <div>
  Pick your favorite hero
  (<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
</div>
<select [(ngModel)]="hero">
  <ng-container *ngFor="let h of heroes">
    <ng-container *ngIf="showSad || h.emotion !== 'sad'">
      <option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
    </ng-container>
  </ng-container>
</select>
    

下拉框也工作正常。

The drop down works properly.

注意: 記住,ngModel 指令是在 Angular 的 FormsModule 中定義的,你要在想使用它的模組的 imports: [...] 元資料中匯入 FormsModule。

Note: Remember that ngModel directive is defined as a part of Angular FormsModule and you need to include FormsModule in the imports: [...] section of the Angular module metadata, in which you want to use it.

<ng-container> 是一個由 Angular 解析器負責識別處理的語法元素。 它不是一個指令、元件、類別或介面,更像是 JavaScript 中 if 塊中的花括號。

The <ng-container> is a syntax element recognized by the Angular parser. It's not a directive, component, class, or interface. It's more like the curly braces in a JavaScript if-block:

if (someCondition) { statement1; statement2; statement3; }
      
      if (someCondition) {
  statement1;
  statement2;
  statement3;
}
    

沒有這些花括號,JavaScript 只會執行第一句,而你原本的意圖是把其中的所有語句都視為一體來根據條件執行。 而 <ng-container> 滿足了 Angular 範本中類似的需求。

Without those braces, JavaScript would only execute the first statement when you intend to conditionally execute all of them as a single block. The <ng-container> satisfies a similar need in Angular templates.

寫一個結構型指令

Write a structural directive

在本節中,你會寫一個名叫 UnlessDirective 的結構型指令,它是 NgIf 的反義詞。 NgIf 在條件為 true 的時候顯示範本內容,而 UnlessDirective 則會在條件為 false 時顯示範本內容。

In this section, you write an UnlessDirective structural directive that does the opposite of NgIf. NgIf displays the template content when the condition is true. UnlessDirective displays the content when the condition is false.

<p *appUnless="condition">Show this sentence unless the condition is true.</p>
src/app/app.component.html (appUnless-1)
      
      <p *appUnless="condition">Show this sentence unless the condition is true.</p>
    

建立指令很像建立元件。

Creating a directive is similar to creating a component.

  • 匯入 Directive 裝飾器(而不再是 Component)。

    Import the Directive decorator (instead of the Component decorator).

  • 匯入符號 InputTemplateRefViewContainerRef,你在任何結構型指令中都會需要它們。

    Import the Input, TemplateRef, and ViewContainerRef symbols; you'll need them for any structural directive.

  • 給指令類別新增裝飾器。

    Apply the decorator to the directive class.

  • 設定 CSS 屬性選擇器,以便在範本中標識出這個指令該應用於哪個元素。

    Set the CSS attribute selector that identifies the directive when applied to an element in a template.

這裡是起點:

Here's how you might begin:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[appUnless]'}) export class UnlessDirective { }
src/app/unless.directive.ts (skeleton)
      
      import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
}
    

指令的選擇器通常是把指令的屬性名括在方括號中,如 [appUnless]。 這個方括號定義出了一個 CSS 屬性選擇器

The directive's selector is typically the directive's attribute name in square brackets, [appUnless]. The brackets define a CSS attribute selector.

該指令的屬性名應該拼寫成小駝峰形式,並且帶有一個字首。 但是,這個字首不能用 ng,因為它只屬於 Angular 本身。 請選擇一些簡短的,適合你自己或公司的字首。 在這個例子中,字首是 app

The directive attribute name should be spelled in lowerCamelCase and begin with a prefix. Don't use ng. That prefix belongs to Angular. Pick something short that fits you or your company. In this example, the prefix is app.

指令的類別名稱Directive 結尾,參閱風格指南。 但 Angular 自己的指令例外。

The directive class name ends in Directive per the style guide. Angular's own directives do not.

TemplateRefViewContainerRef

TemplateRef and ViewContainerRef

像這個例子一樣的簡單結構型指令會從 Angular 產生的 <ng-template> 元素中建立一個內嵌的檢視,並把這個檢視插入到一個檢視容器中,緊挨著本指令原來的宿主元素 <p>(譯註:注意不是子節點,而是兄弟節點)。

A simple structural directive like this one creates an embedded view from the Angular-generated <ng-template> and inserts that view in a view container adjacent to the directive's original <p> host element.

你可以使用TemplateRef取得 <ng-template> 的內容,並透過ViewContainerRef來訪問這個檢視容器

You'll acquire the <ng-template> contents with a TemplateRefand access the view container through a ViewContainerRef.

你可以把它們都注入到指令的建構函式中,作為該類別的私有屬性。

You inject both in the directive constructor as private variables of the class.

constructor( private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) { }
src/app/unless.directive.ts (ctor)
      
      constructor(
  private templateRef: TemplateRef<any>,
  private viewContainer: ViewContainerRef) { }
    

appUnless 屬性

The appUnless property

該指令的使用者會把一個 true/false 條件繫結到 [appUnless] 屬性上。 也就是說,該指令需要一個帶有 @InputappUnless 屬性。

The directive consumer expects to bind a true/false condition to [appUnless]. That means the directive needs an appUnless property, decorated with @Input

要了解關於 @Input 的更多知識,參閱範本語法一章。

Read about @Input in the @Input() and @Output() properties guide.

@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; } }
src/app/unless.directive.ts (set)
      
      @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;
  }
}
    

一旦該值的條件發生了變化,Angular 就會去設定 appUnless 屬性。因為不能用 appUnless 屬性,所以你要為它定義一個設定器(setter)。

Angular sets the appUnless property whenever the value of the condition changes. Because the appUnless property does work, it needs a setter.

  • 如果條件為假,並且以前尚未建立過該檢視,就告訴檢視容器(ViewContainer)根據範本建立一個內嵌檢視

    If the condition is falsy and the view hasn't been created previously, tell the view container to create the embedded view from the template.

  • 如果條件為真,並且檢視已經顯示出來了,就會清除該容器,並銷燬該檢視。

    If the condition is truthy and the view is currently displayed, clear the container which also destroys the view.

沒有人會讀取 appUnless 屬性,因此它不需要定義 getter。

Nobody reads the appUnless property so it doesn't need a getter.

完整的指令程式碼如下:

The completed directive code looks like this:

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; } } }
src/app/unless.directive.ts (excerpt)
      
      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;
    }
  }
}
    

把這個指令新增到 AppModule 的 declarations 陣列中。

Add this directive to the declarations array of the AppModule.

然後建立一些 HTML 來試用一下。

Then create some HTML to try it.

<p *appUnless="condition" class="unless a"> (A) This paragraph is displayed because the condition is false. </p> <p *appUnless="!condition" class="unless b"> (B) Although the condition is true, this paragraph is displayed because appUnless is set to false. </p>
src/app/app.component.html (appUnless)
      
      <p *appUnless="condition" class="unless a">
  (A) This paragraph is displayed because the condition is false.
</p>

<p *appUnless="!condition" class="unless b">
  (B) Although the condition is true,
  this paragraph is displayed because appUnless is set to false.
</p>
    

conditionfalse 時,頂部的段落就會顯示出來,而底部的段落消失了。 當 conditiontrue 時,頂部的段落被移除了,而底部的段落顯示了出來。

When the condition is falsy, the top (A) paragraph appears and the bottom (B) paragraph disappears. When the condition is truthy, the top (A) paragraph is removed and the bottom (B) paragraph appears.

改進自訂指令的範本型別檢查

Improving template type checking for custom directives

你可以透過在指令定義中新增範本守護屬性來改進自訂指令的範本型別檢查。這些屬性可以幫助 Angular 範本型別檢查器在編譯期間發現範本中的錯誤,避免這些失誤導致執行期錯誤。

You can improve template type checking for custom directives by adding template guard properties to your directive definition. These properties help the Angular template type checker find mistakes in the template at compile time, which can avoid runtime errors those mistakes can cause.

使用型別守護屬性可以告訴範本型別檢查器你所期望的型別,從而改進該範本的編譯期型別檢查。

Use the type-guard properties to inform the template type checker of an expected type, thus improving compile-time type-checking for that template.

  • 屬性 ngTemplateGuard_(someInputProperty) 允許你為範本中的輸入表示式指定一個更準確的型別。

    A property ngTemplateGuard_(someInputProperty) lets you specify a more accurate type for an input expression within the template.

  • ngTemplateContextGuard 靜態屬性聲明瞭範本上下文的型別。

    The ngTemplateContextGuard static property declares the type of the template context.

本節提供了這兩種型別守護屬性的例子。

This section provides example of both kinds of type-guard property.

欲知詳情,請參閱範本型別檢查指南

For more information, see Template type checking guide.

使用範本守護功能可以讓範本內的型別需求更具體

Make in-template type requirements more specific with template guards

範本中的結構型指令會根據輸入表示式來控制是否要在執行時渲染該範本。為了幫助編譯器捕獲範本型別中的錯誤,你應該儘可能詳細地指定範本內指令的輸入表示式所期待的型別。

A structural directive in a template controls whether that template is rendered at run time, based on its input expression. To help the compiler catch template type errors, you should specify as closely as possible the required type of a directive's input expression when it occurs inside the template.

型別守護函式會把輸入表示式所期待的型別窄化為在執行時可能傳給指令的子型別。你可以提供這樣一個函式來幫助型別檢查器在編譯期間推斷出該表示式的正確型別。

A type guard function narrows the expected type of an input expression to a subset of types that might be passed to the directive within the template at run time. You can provide such a function to help the type-checker infer the proper type for the expression at compile time.

例如,NgIf 的實現使用型別窄化來確保只有當 *ngIf 的輸入表示式為真時,範本才會被實例化。為了提供具體的型別要求,NgIf 指令定義了一個靜態屬性 ngTemplateGuard_ngIf: 'binding'binding 值是一種常見的型別窄化的例子,它會對輸入表示式進行求值,以滿足型別要求。

For example, the NgIf implementation uses type-narrowing to ensure that the template is only instantiated if the input expression to *ngIf is truthy. To provide the specific type requirement, the NgIf directive defines a static property ngTemplateGuard_ngIf: 'binding'. The binding value is a special case for a common kind of type-narrowing where the input expression is evaluated in order to satisfy the type requirement.

要為範本中的指令提供一個更具體的輸入表示式型別,就要把 ngTemplateGuard_xx 屬性新增到該指令中,其靜態屬性名的字尾(xx)是 @Input 欄位名。該屬性的值既可以是針對其返回型別的通用型別窄化函式,也可以是字串 "binding" 就像 NgIf 一樣。

To provide a more specific type for an input expression to a directive within the template, add a ngTemplateGuard_xx property to the directive, where the suffix to the static property name is the @Input field name. The value of the property can be either a general type-narrowing function based on its return type, or the string "binding" as in the case of NgIf.

例如,考慮以下結構型指令,它以範本表示式的結果作為輸入。

For example, consider the following structural directive that takes the result of a template expression as an input.

export type Loaded= { type: 'loaded', data: T }; export type Loading = { type: 'loading' }; export type LoadingState= Loaded| Loading; export class IfLoadedDirective{ @Input('ifLoaded') set state(state: LoadingState) {} static ngTemplateGuard_state(dir: IfLoadedDirective, expr: LoadingState): expr is Loaded{ return true; }; } export interface Person { name: string; } @Component({ template: `<div *ifLoaded="state">{{ state.data }}</div>`, }) export class AppComponent { state: LoadingState; }
IfLoadedDirective
      
      export type Loaded = { type: 'loaded', data: T };
export type Loading = { type: 'loading' };
export type LoadingState = Loaded | Loading;
export class IfLoadedDirective {
    @Input('ifLoaded') set state(state: LoadingState) {}
    static ngTemplateGuard_state(dir: IfLoadedDirective, expr: LoadingState): expr is Loaded { return true; };
}

export interface Person {
  name: string;
}

@Component({
  template: `<div *ifLoaded="state">{{ state.data }}</div>`,
})
export class AppComponent {
  state: LoadingState;
}
    

在這個例子中,LoadingState<T> 型別允許兩種狀態之一,Loaded<T>Loading。此表示式用作該指令的 state 輸入是一個總括型別 LoadingState,因為此處的載入狀態是未知的。

In this example, the LoadingState<T> type permits either of two states, Loaded<T> or Loading. The expression used as the directive’s state input is of the umbrella type LoadingState, as it’s unknown what the loading state is at that point.

IfLoadedDirective 定義聲明瞭靜態欄位 ngTemplateGuard_state,表示其窄化行為。在 AppComponent 範本中,*ifLoaded 結構型指令只有當實際的 stateLoaded<Person> 型別時,才會渲染該範本。型別守護允許型別檢查器推斷出範本中可接受的 state 型別是 Loaded<T>,並進一步推斷出 T 必須是 Person 一個實例。

The IfLoadedDirective definition declares the static field ngTemplateGuard_state, which expresses the narrowing behavior. Within the AppComponent template, the *ifLoaded structural directive should render this template only when state is actually Loaded<Person>. The type guard allows the type checker to infer that the acceptable type of state within the template is a Loaded<T>, and further infer that T must be an instance of Person.

為指令上下文指定型別

Typing the directive's context

如果你的結構型指令要為實例化的範本提供一個上下文,可以透過提供靜態的 ngTemplateContextGuard 函式在範本中給它提供合適的型別。下面的程式碼片段展示了該函式的一個例子。

If your structural directive provides a context to the instantiated template, you can properly type it inside the template by providing a static ngTemplateContextGuard function. The following snippet shows an example of such a function.

@Directive({…}) export class ExampleDirective { // Make sure the template checker knows the type of the context with which the // template of this directive will be rendered static ngTemplateContextGuard(dir: ExampleDirective, ctx: unknown): ctx is ExampleContext { return true; }; // … }
myDirective.ts
      
      @Directive({…})
export class ExampleDirective {
    // Make sure the template checker knows the type of the context with which the
    // template of this directive will be rendered
    static ngTemplateContextGuard(dir: ExampleDirective, ctx: unknown): ctx is ExampleContext { return true; };

    // …
}
    

小結

Summary

你可以去現場演練 / 下載範例中下載本章的原始碼。

You can both try and download the source code for this guide in the現場演練 / 下載範例.

本章相關的程式碼如下:

Here is the source from the src/app/ folder.

import { Component } from '@angular/core'; import { Hero, heroes } from './hero'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: [ './app.component.css' ] }) export class AppComponent { heroes = heroes; hero = this.heroes[0]; condition = false; logs: string[] = []; showSad = true; status = 'ready'; trackById(index: number, hero: Hero): number { return hero.id; } }<h1>Structural Directives</h1> <p>Conditional display of hero</p> <blockquote> <div *ngIf="hero" class="name">{{hero.name}}</div> </blockquote> <p>List of heroes</p> <ul> <li *ngFor="let hero of heroes">{{hero.name}}</li> </ul> <hr> <h2 id="ngIf">NgIf</h2> <p *ngIf="true"> Expression is true and ngIf is true. This paragraph is in the DOM. </p> <p *ngIf="false"> Expression is false and ngIf is false. This paragraph is not in the DOM. </p> <p [style.display]="'block'"> Expression sets display to "block". This paragraph is visible. </p> <p [style.display]="'none'"> Expression sets display to "none". This paragraph is hidden but still in the DOM. </p> <h4>NgIf with template</h4> <p>&lt;ng-template&gt; element</p> <ng-template [ngIf]="hero"> <div class="name">{{hero.name}}</div> </ng-template> <hr> <h2 id="ng-container">&lt;ng-container&gt;</h2> <h4>*ngIf with a &lt;ng-container&gt;</h4> <button (click)="hero = hero ? null : heroes[0]">Toggle hero</button> <p> I turned the corner <ng-container *ngIf="hero"> and saw {{hero.name}}. I waved </ng-container> and continued on my way. </p> <p> I turned the corner <span *ngIf="hero"> and saw {{hero.name}}. I waved </span> and continued on my way. </p> <p><i>&lt;select&gt; with &lt;span&gt;</i></p> <div> Pick your favorite hero (<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>) </div> <select [(ngModel)]="hero"> <span *ngFor="let h of heroes"> <span *ngIf="showSad || h.emotion !== 'sad'"> <option [ngValue]="h">{{h.name}} ({{h.emotion}})</option> </span> </span> </select> <p><i>&lt;select&gt; with &lt;ng-container&gt;</i></p> <div> Pick your favorite hero (<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>) </div> <select [(ngModel)]="hero"> <ng-container *ngFor="let h of heroes"> <ng-container *ngIf="showSad || h.emotion !== 'sad'"> <option [ngValue]="h">{{h.name}} ({{h.emotion}})</option> </ng-container> </ng-container> </select> <br><br> <hr> <h2 id="ngFor">NgFor</h2> <div class="box"> <p class="code">&lt;div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd"&gt;</p> <div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd"> ({{i}}) {{hero.name}} </div> <p class="code">&lt;ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById"/&gt;</p> <ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById"> <div [class.odd]="odd">({{i}}) {{hero.name}}</div> </ng-template> </div> <hr> <h2 id="ngSwitch">NgSwitch</h2> <div>Pick your favorite hero</div> <p> <label *ngFor="let h of heroes"> <input type="radio" name="heroes" [(ngModel)]="hero" [value]="h">{{h.name}} </label> <label><input type="radio" name="heroes" (click)="hero = null">None of the above</label> </p> <h4>NgSwitch</h4> <div [ngSwitch]="hero?.emotion"> <app-happy-hero *ngSwitchCase="'happy'" [hero]="hero"></app-happy-hero> <app-sad-hero *ngSwitchCase="'sad'" [hero]="hero"></app-sad-hero> <app-confused-hero *ngSwitchCase="'confused'" [hero]="hero"></app-confused-hero> <app-unknown-hero *ngSwitchDefault [hero]="hero"></app-unknown-hero> </div> <h4>NgSwitch with &lt;ng-template&gt;</h4> <div [ngSwitch]="hero?.emotion"> <ng-template [ngSwitchCase]="'happy'"> <app-happy-hero [hero]="hero"></app-happy-hero> </ng-template> <ng-template [ngSwitchCase]="'sad'"> <app-sad-hero [hero]="hero"></app-sad-hero> </ng-template> <ng-template [ngSwitchCase]="'confused'"> <app-confused-hero [hero]="hero"></app-confused-hero> </ng-template > <ng-template ngSwitchDefault> <app-unknown-hero [hero]="hero"></app-unknown-hero> </ng-template> </div> <hr> <h2>&lt;ng-template&gt;</h2> <p>Hip!</p> <ng-template> <p>Hip!</p> </ng-template> <p>Hooray!</p> <hr> <h2 id="appUnless">UnlessDirective</h2> <p> The condition is currently <span [ngClass]="{ 'a': !condition, 'b': condition, 'unless': true }">{{condition}}</span>. <button (click)="condition = !condition" [ngClass] = "{ 'a': condition, 'b': !condition }" > Toggle condition to {{condition ? 'false' : 'true'}} </button> </p> <p *appUnless="condition" class="unless a"> (A) This paragraph is displayed because the condition is false. </p> <p *appUnless="!condition" class="unless b"> (B) Although the condition is true, this paragraph is displayed because appUnless is set to false. </p> <h4>UnlessDirective with template</h4> <p *appUnless="condition">Show this sentence unless the condition is true.</p> <p *appUnless="condition" class="code unless"> (A) &lt;p *appUnless="condition" class="code unless"&gt; </p> <ng-template [appUnless]="condition"> <p class="code unless"> (A) &lt;ng-template [appUnless]="condition"&gt; </p> </ng-template>button { min-width: 100px; font-size: 100%; } .box { border: 1px solid gray; max-width: 600px; padding: 4px; } .choices { font-style: italic; } code, .code { background-color: #eee; color: black; font-family: Courier, sans-serif; font-size: 85%; } div.code { width: 400px; } .heroic { font-size: 150%; font-weight: bold; } hr { margin: 40px 0 } .odd { background-color: palegoldenrod; } td, th { text-align: left; vertical-align: top; } p span { color: red; font-size: 70%; } .unless { border: 2px solid; padding: 6px; } p.unless { width: 500px; } button.a, span.a, .unless.a { color: red; border-color: gold; background-color: yellow; font-size: 100%; } button.b, span.b, .unless.b { color: black; border-color: green; background-color: lightgreen; font-size: 100%; }import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { heroSwitchComponents } from './hero-switch.components'; import { UnlessDirective } from './unless.directive'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent, heroSwitchComponents, UnlessDirective ], bootstrap: [ AppComponent ] }) export class AppModule { }export interface Hero { id: number; name: string; emotion?: string; } export const heroes: Hero[] = [ { id: 1, name: 'Dr Nice', emotion: 'happy'}, { id: 2, name: 'Narco', emotion: 'sad' }, { id: 3, name: 'Windstorm', emotion: 'confused' }, { id: 4, name: 'Magneta'} ];import { Component, Input } from '@angular/core'; import { Hero } from './hero'; @Component({ selector: 'app-happy-hero', template: `Wow. You like {{hero.name}}. What a happy hero ... just like you.` }) export class HappyHeroComponent { @Input() hero: Hero; } @Component({ selector: 'app-sad-hero', template: `You like {{hero.name}}? Such a sad hero. Are you sad too?` }) export class SadHeroComponent { @Input() hero: Hero; } @Component({ selector: 'app-confused-hero', template: `Are you as confused as {{hero.name}}?` }) export class ConfusedHeroComponent { @Input() hero: Hero; } @Component({ selector: 'app-unknown-hero', template: `{{message}}` }) export class UnknownHeroComponent { @Input() hero: Hero; get message() { return this.hero && this.hero.name ? `${this.hero.name} is strange and mysterious.` : 'Are you feeling indecisive?'; } } export const heroSwitchComponents = [ HappyHeroComponent, SadHeroComponent, ConfusedHeroComponent, UnknownHeroComponent ];import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; /** * Add the template content to the DOM unless the condition is true. * * If the expression assigned to `appUnless` evaluates to a truthy value * then the templated elements are removed removed from the DOM, * the templated elements are (re)inserted into the DOM. * * <div *appUnless="errorCount" class="success"> * Congrats! Everything is great! * </div> * * ### Syntax * * - `<div *appUnless="condition">...</div>` * - `<ng-template [appUnless]="condition"><div>...</div></ng-template>` * */ @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; } } }
      
      import { Component } from '@angular/core';

import { Hero, heroes } from './hero';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent {
  heroes = heroes;
  hero = this.heroes[0];

  condition = false;
  logs: string[] = [];
  showSad = true;
  status = 'ready';

  trackById(index: number, hero: Hero): number { return hero.id; }
}
    

你學到了

You learned: