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

編寫結構型指令

Writing structural directives

本主題示範如何建立結構型指令,並提供有關指令如何工作、Angular 如何解釋簡寫形式以及如何新增範本守衛屬性以捕獲範本型別錯誤的概念性資訊。

This topic demonstrates how to create a structural directive and provides conceptual information on how directives work, how Angular interprets shorthand, and how to add template guard properties to catch template type errors.

有關此頁面描述的示例應用程式,請參見現場演練 / 下載範例

For the example app that this page describes, see the現場演練 / 下載範例.

有關 Angular 的內建結構型指令(如 NgIfNgForNgSwitch)的更多資訊,請參見內建指令

For more information on Angular's built-in structural directives, such as NgIf, NgFor, and NgSwitch, see Built-in directives.

建立結構型指令

Creating a structural directive

本節將指導你建立 UnlessDirective 以及如何設定 condition 值。 UnlessDirectiveNgIf 相反,並且 condition 值可以設定為 truefalseNgIftrue 時顯示範本內容;而 UnlessDirective 在這個條件為 false 時顯示內容。

This section guides you through creating an UnlessDirective and how to set condition values. The UnlessDirective does the opposite of NgIf, and condition values can be set to true or false. NgIf displays the template content when the condition is true. UnlessDirective displays the content when the condition is false.

以下是應用於 p 元素的 UnlessDirective 選擇器 appUnlessconditionfalse ,瀏覽器將顯示該句子。

Following is the UnlessDirective selector, appUnless, applied to the paragraph element. When condition is false, the browser displays the sentence.

src/app/app.component.html (appUnless-1)
      
      <p *appUnless="condition">Show this sentence unless the condition is true.</p>
    
  1. 使用 Angular CLI,執行以下命令,其中 unless 是偽指令的名稱:

    Using the Angular CLI, run the following command, where unless is the name of the directive:

          
          ng generate directive unless
        

    Angular 會建立指令類別,並指定 CSS 選擇器 appUnless,它會在範本中標識指令。

    Angular creates the directive class and specifies the CSS selector, appUnless, that identifies the directive in a template.

  2. 匯入 InputTemplateRefViewContainerRef

    Import Input, TemplateRef, and ViewContainerRef.

    src/app/unless.directive.ts (skeleton)
          
          import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
    
    @Directive({ selector: '[appUnless]'})
    export class UnlessDirective {
    }
        
  3. 在指令的建構函式中將 TemplateRefViewContainerRef 注入成私有變數。

    Inject TemplateRef and ViewContainerRef in the directive constructor as private variables.

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

    UnlessDirective 會透過 Angular 產生的 <ng-template> 建立一個嵌入的檢視,然後將該檢視插入到該指令的原始 <p> 宿主元素緊後面的檢視容器中。

    The UnlessDirective 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可以訪問檢視容器。

    TemplateRefhelps you get to the <ng-template> contents and ViewContainerRefaccesses the view container.

  4. 新增一個帶 setter 的 @Input() 屬性 appUnless

    Add an appUnless @Input() property with a setter.

    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 屬性。

    Angular sets the appUnless property whenever the value of the condition changes.

    • 如果條件是假值,並且 Angular 以前尚未建立檢視,則此 setter 會導致檢視容器從範本創建出嵌入式檢視。

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

    • 如果條件為真值,並且當前正顯示著檢視,則此 setter 會清除容器,這會導致銷燬該檢視。

      If the condition is truthy and the view is currently displayed, the setter clears the container, which disposes of the view.

完整的指令如下:

The complete directive is as follows:

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;
    }
  }
}
    

測試指令

Testing the directive

在本節中,你將更新你的應用程式,以測試 UnlessDirective

In this section, you'll update your application to test the UnlessDirective.

  1. 新增一個 condition 設定為 falseAppComponent

    Add a condition set to false in the AppComponent.

    src/app/app.component.ts (excerpt)
          
          condition = false;
        
  2. 更新範本以使用指令。這裡,*appUnless 位於兩個具有相反 condition<p> 標記上,一個為 true ,一個為 false

    Update the template to use the directive. Here, *appUnless is on two <p> tags with opposite condition values, one true and one false.

    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>
        

    星號是將 appUnless 標記為結構型指令的簡寫形式。如果 condition 是假值,則會讓頂部段落 A,而底部段落 B 消失。當 condition 為真時,頂部段落 A 消失,而底部段落 B 出現。

    The asterisk is shorthand that marks appUnless as a structural directive. 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 disappears and the bottom (B) paragraph appears.

  3. 要在瀏覽器中更改並顯示 condition 的值,請新增一段標記程式碼以顯示狀態和按鈕。

    To change and display the value of condition in the browser, add markup that displays the status and a button.

    src/app/app.component.html
          
          <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>
        

要驗證指令是否有效,請單擊按鈕以更改 condition 的值。

To verify that the directive works, click the button to change the value of condition.

結構型指令簡寫形式

Structural directive shorthand

結構型指令(例如 *ngIf)上的星號 * 語法是 Angular 解釋為較長形式的簡寫形式。 Angular 將結構型指令前面的星號轉換為圍繞宿主元素及其後代的 <ng-template>

The asterisk, *, syntax on a structural directive, such as *ngIf, is shorthand that Angular interprets into a longer form. Angular transforms the asterisk in front of a structural directive into an <ng-template> that surrounds the host element and its descendants.

下面是一個 *ngIf 的示例,如果 hero 存在,則顯示英雄的名稱:

The following is an example of *ngIf that displays the hero's name if hero exists:

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

*ngIf 指令移到了 <ng-template> 上,在這裡它成為繫結在方括號 [ngIf] 中的屬性。 <div> 的其餘部分(包括其 class 屬性)移到了 <ng-template> 內部。

The *ngIf directive moves to the <ng-template> where it becomes a property binding in square brackets, [ngIf]. The rest of the <div>, including its class attribute, moves inside the <ng-template>.

src/app/app.component.html (ngif-template)
      
      <ng-template [ngIf]="hero">
  <div class="name">{{hero.name}}</div>
</ng-template>
    

Angular 不會建立真正的 <ng-template> 元素,只會將 <div> 和註釋節點佔位符渲染到 DOM 中。

Angular does not create a real <ng-template> element, instead rendering only the <div> and a comment node placeholder to the DOM.

      
      <!--bindings={
  "ng-reflect-ng-if": "[object Object]"
}-->
<div _ngcontent-c0>Mr. Nice</div>
    

*ngFor 中的星號的簡寫形式與非簡寫的 <ng-template> 形式進行比較:

The following example compares the shorthand use of the asterisk in *ngFor with the longhand <ng-template> form:

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>
    

這裡,ngFor 結構型指令相關的所有內容都應用到了 <ng-template> 中。而元素上的所有其他繫結和屬性應用到了 <ng-template> 中的 <div> 元素上。除了 ngFor 字串外,宿主元素上的其他修飾都會保留在 <ng-template> 中。在這個例子中,[class.odd]="odd" 就留在了 <div> 中。

Here, everything related to the ngFor structural directive applies to the <ng-template>. All other bindings and attributes on the element apply to the <div> element within the <ng-template>. Other modifiers on the host element, in addition to the ngFor string, remain in place as the element moves inside the <ng-template>. In this example, the [class.odd]="odd" stays on the <div>.

let 關鍵字會宣告一個範本輸入變數,你可以在範本中參考該變數。在這個例子中,是 heroiodd。解析器將 let herolet ilet odd 轉換為名為 let-herolet-ilet-odd 的變數。 let-ilet-odd 變數變為 let i=indexlet odd=odd 。 Angular 會將 iodd 設定為上下文中 indexodd 屬性的當前值。

The let keyword declares a template input variable that you can 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. The let-i and let-odd variables become let i=index and let odd=odd. Angular sets i and odd to the current value of the context's index and odd properties.

解析器會將 PascalCase 應用於所有指令,並為它們加上指令的屬性名稱(例如 ngFor)。比如,ngFor 的輸入特性 oftrackBy ,會對映為 ngForOfngForTrackBy 。當 NgFor 指令遍歷列表時,它會設定和重置它自己的上下文物件的屬性。這些屬性可以包括但不限於 indexodd 和一個名為 $implicit 的特殊屬性。

The parser applies PascalCase to all directives and prefixes them with the directive's attribute name, such as ngFor. For example, the ngFor input properties, of and trackBy, map to ngForOf and ngForTrackBy. 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.

Angular 會將 let-hero 設定為上下文的 $implicit 屬性的值, NgFor 已經將其初始化為當前正在迭代的英雄。

Angular sets let-hero to the value of the context's $implicit property, which NgFor has initialized with the hero for the current iteration.

有關更多資訊,請參見 NgFor APINgForOf API 文件。

For more information, see the NgFor API and NgForOf API documentation.

<ng-template> 建立範本片段

Creating template fragments with <ng-template>

Angular 的 <ng-template> 元素定義了一個預設情況下不渲染任何內容的範本。使用 <ng-template> ,你可以手動渲染內容,以完全控制內容的顯示方式。

Angular's <ng-template> element defines a template that doesn't render anything by default. With <ng-template>, you can render the content manually for full control over how the content displays.

如果沒有結構型指令,並且將某些元素包裝在 <ng-template> 中,則這些元素會消失。在下面的示例中,Angular 不會渲染中間的 “Hip!”,因為它被 <ng-template> 包裹著。

If there is no structural directive and you wrap some elements in an <ng-template>, those elements disappear. In the following example, Angular does not render the middle "Hip!" in the phrase "Hip! Hip! Hooray!" because of the surrounding <ng-template>.

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

結構型指令語法參考

Structural directive syntax reference

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

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

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

下表描述了結構型指令語法的每個部分:

The following tables describe each portion of the structural directive grammar:

prefix

HTML 屬性的鍵名

HTML attribute key

key

HTML 屬性的鍵名

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 ";"?

Angular 如何翻譯簡寫形式

How Angular translates shorthand

Angular 會將結構型指令的簡寫形式轉換為普通的繫結語法,如下所示:

Angular translates structural directive shorthand into the normal binding syntax as follows:

簡寫形式

Shorthand

翻譯結果

Translation

prefix 和裸 expression

prefix and naked expression

[prefix]="expression"
keyExp

[prefixKey] "expression" (let-prefixKey="export")
注意,這個 prefix 已經加到了 key 上。

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

letlet-local="export"

簡寫形式示例

Shorthand examples

下表提供了一些簡寫形式示例:

The following table provides shorthand examples:

簡寫形式

Shorthand

Angular 如何解釋此語法

How Angular interprets the syntax

*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">

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

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. These properties are as follows:

  • 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 examples of both kinds of type-guard property. For more information, see Template type checking.

使用範本守衛使範本中的型別要求更具體

Making 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() 欄位的名字。該屬性的值可以是基於其返回型別的常規型別窄化函式,也可以是字串,例如 NgIf 中的 "binding"

To provide a more specific type for an input expression to a directive within the template, add an ngTemplateGuard_xx property to the directive, where the suffix to the static property name, xx, 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:

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.

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; };

    // …
}