Angular 結(jié)構(gòu)型指令

2022-07-01 11:36 更新

編寫結(jié)構(gòu)型指令

本主題演示如何創(chuàng)建結(jié)構(gòu)型指令,并提供有關(guān)指令如何工作、Angular 如何解釋簡寫形式以及如何添加模板守衛(wèi)屬性以捕獲模板類型錯誤的概念性信息。

有關(guān)此頁面描述的示例應(yīng)用程序,請參見現(xiàn)場演練 / 下載范例 。

創(chuàng)建結(jié)構(gòu)型指令

本節(jié)將指導(dǎo)你創(chuàng)建 ?UnlessDirective ?以及如何設(shè)置 ?condition ?值。 ?UnlessDirective ?與 ?NgIf ?相反,并且 ?condition ?值可以設(shè)置為 ?true ?或 ?false ?。 ?NgIf ?為 ?true ?時顯示模板內(nèi)容;而 ?UnlessDirective ?在這個條件為 ?false ?時顯示內(nèi)容。

以下是應(yīng)用于 p 元素的 ?UnlessDirective ?選擇器 ?appUnless ?當(dāng) ?condition ?為 ?false ?,瀏覽器將顯示該句子。

<p *appUnless="condition">Show this sentence unless the condition is true.</p>
  1. 使用 Angular CLI,運(yùn)行以下命令,其中 ?unless ?是偽指令的名稱:
  2. ng generate directive unless

    Angular 會創(chuàng)建指令類,并指定 CSS 選擇器 ?appUnless?,它會在模板中標(biāo)識指令。

  3. 導(dǎo)入 ?Input?、?TemplateRef ?和 ?ViewContainerRef?。
  4. import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
    
    @Directive({ selector: '[appUnless]'})
    export class UnlessDirective {
    }
  5. 在指令的構(gòu)造函數(shù)中將 ?TemplateRef ?和 ?ViewContainerRef ?注入成私有變量。
  6. constructor(
      private templateRef: TemplateRef<any>,
      private viewContainer: ViewContainerRef) { }

    ?UnlessDirective ?會通過 Angular 生成的 ?<ng-template>? 創(chuàng)建一個嵌入的視圖,然后將該視圖插入到該指令的原始 ?<p>? 宿主元素緊后面的視圖容器中。

    ?TemplateRef?可幫助你獲取 ?<ng-template>? 的內(nèi)容,而 ?ViewContainerRef ?可以訪問視圖容器。

  7. 添加一個帶 setter 的 ?@Input()? 屬性 ?appUnless?。
  8. @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;
      }
    }

    每當(dāng)條件的值更改時,Angular 都會設(shè)置 ?appUnless ?屬性。

    • 如果條件是假值,并且 Angular 以前尚未創(chuàng)建視圖,則此 setter 會導(dǎo)致視圖容器從模板創(chuàng)建出嵌入式視圖。
    • 如果條件為真值,并且當(dāng)前正顯示著視圖,則此 setter 會清除容器,這會導(dǎo)致銷毀該視圖。

完整的指令如下:

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

測試指令

在本節(jié)中,你將更新你的應(yīng)用程序,以測試 ?UnlessDirective ?。

  1. 添加一個 ?condition ?設(shè)置為 ?false ?的 ?AppComponent ?。
  2. condition = false;
  3. 更新模板以使用指令。這里,?*appUnless? 位于兩個具有相反 ?condition ?的 ?<p>? 標(biāo)記上,一個為 ?true ?,一個為 ?false ?。
  4. <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 ?標(biāo)記為結(jié)構(gòu)型指令的簡寫形式。如果 ?condition ?是假值,則會讓頂部段落 A,而底部段落 B 消失。當(dāng) ?condition ?為真時,頂部段落 A 消失,而底部段落 B 出現(xiàn)。

  5. 要在瀏覽器中更改并顯示 ?condition ?的值,請?zhí)砑右欢螛?biāo)記代碼以顯示狀態(tài)和按鈕。
  6. <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 ?的值。


結(jié)構(gòu)型指令簡寫形式

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

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

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

?*ngIf? 指令移到了 ?<ng-template>? 上,在這里它成為綁定在方括號 ?[ngIf]? 中的屬性。 ?<div>? 的其余部分(包括其 class 屬性)移到了 ?<ng-template>? 內(nèi)部。

<ng-template [ngIf]="hero">
  <div class="name">{{hero.name}}</div>
</ng-template>

Angular 不會創(chuàng)建真正的 ?<ng-template>? 元素,只會將 ?<div>? 和注釋節(jié)點(diǎn)占位符渲染到 DOM 中。

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

?*ngFor? 中的星號的簡寫形式與非簡寫的 ?<ng-template>? 形式進(jìn)行比較:

<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 ?結(jié)構(gòu)型指令相關(guān)的所有內(nèi)容都應(yīng)用到了 ?<ng-template>? 中。而元素上的所有其他綁定和屬性應(yīng)用到了 ?<ng-template>? 中的 ?<div>? 元素上。除了 ?ngFor ?字符串外,宿主元素上的其他修飾都會保留在 ?<ng-template>? 中。在這個例子中,?[class.odd]="odd"? 就留在了 ?<div>? 中。

?let ?關(guān)鍵字會聲明一個模板輸入變量,你可以在模板中引用該變量。在這個例子中,是 ?hero?、?i? 和 ?odd?。解析器將 ?let hero?、?let i? 和 ?let odd? 轉(zhuǎn)換為名為 ?let-hero?、?let-i? 和 ?let-odd? 的變量。 ?let-i? 和 ?let-odd? 變量變?yōu)?nbsp;?let i=index? 和 ?let odd=odd? 。 Angular 會將 ?i? 和 ?odd ?設(shè)置為上下文中 ?index ?和 ?odd ?屬性的當(dāng)前值。

解析器會將 PascalCase 應(yīng)用于所有指令,并為它們加上指令的屬性名稱(例如 ngFor)。比如,?ngFor ?的輸入特性 ?of ?和 ?trackBy ?,會映射為 ?ngForOf ?和 ?ngForTrackBy ?。當(dāng) ?NgFor ?指令遍歷列表時,它會設(shè)置和重置它自己的上下文對象的屬性。這些屬性可以包括但不限于 ?index?、?odd ?和一個名為 ?$implicit? 的特殊屬性。

Angular 會將 ?let-hero? 設(shè)置為上下文的 ?$implicit? 屬性的值, ?NgFor ?已經(jīng)將其初始化為當(dāng)前正在迭代的英雄。

用 <ng-template> 創(chuàng)建模板片段

Angular 的 ?<ng-template>? 元素定義了一個默認(rèn)情況下不渲染任何內(nèi)容的模板。使用 ?<ng-template>? ,你可以手動渲染內(nèi)容,以完全控制內(nèi)容的顯示方式。

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

<p>Hip!</p>
<ng-template>
  <p>Hip!</p>
</ng-template>
<p>Hooray!</p>


結(jié)構(gòu)型指令語法參考

當(dāng)你編寫自己的結(jié)構(gòu)型指令時,請使用以下語法:

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

下表描述了結(jié)構(gòu)型指令語法的每個部分:

prefix

HTML 屬性的鍵名

key

HTML 屬性的鍵名

local

在模板中使用的局部變量名

export

該指令以特定名稱導(dǎo)出的值

expression

標(biāo)準(zhǔn) Angular 表達(dá)式

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

Angular 如何翻譯簡寫形式

Angular 會將結(jié)構(gòu)型指令的簡寫形式轉(zhuǎn)換為普通的綁定語法,如下所示:

簡寫形式

翻譯結(jié)果

prefix 和裸 expression

[prefix]="expression"
keyExp

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

let let-local="export"

簡寫形式示例

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

簡寫形式

Angular 如何解釋此語法

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

改進(jìn)自定義指令的模板類型檢查

你可以通過將模板守衛(wèi)屬性添加到指令定義中來改進(jìn)自定義指令的模板類型檢查。這些屬性可幫助 Angular 的模板類型檢查器在編譯時發(fā)現(xiàn)模板中的錯誤,從而避免運(yùn)行時錯誤。這些屬性如下:

  • ?ngTemplateGuard_(someInputProperty)? 屬性使你可以為模板中的輸入表達(dá)式指定更準(zhǔn)確的類型。
  • 靜態(tài)屬性 ?ngTemplateContextGuard ?聲明了模板上下文的類型。

使用模板守衛(wèi)使模板中的類型要求更具體

模板中的結(jié)構(gòu)型指令會根據(jù)輸入表達(dá)式來控制是否要在運(yùn)行時渲染該模板。為了幫助編譯器捕獲模板類型中的錯誤,你應(yīng)該盡可能詳細(xì)地指定模板內(nèi)指令的輸入表達(dá)式所期待的類型。

類型保護(hù)函數(shù)會將輸入表達(dá)式的預(yù)期類型縮小為可能在運(yùn)行時傳遞給模板內(nèi)指令的類型的子集。你可以提供這樣的功能來幫助類型檢查器在編譯時為表達(dá)式推斷正確的類型。

例如,?NgIf ?的實(shí)現(xiàn)使用類型窄化來確保只有當(dāng) ?*ngIf? 的輸入表達(dá)式為真時,模板才會被實(shí)例化。為了提供具體的類型要求,?NgIf ?指令定義了一個靜態(tài)屬性 ?ngTemplateGuard_ngIf: 'binding'?。這里的 ?binding ?值是一種常見的類型窄化的例子,它會對輸入表達(dá)式進(jìn)行求值,以滿足類型要求。

要為模板中指令的輸入表達(dá)式提供更具體的類型,請在指令中添加 ?ngTemplateGuard_xx ?屬性,其中靜態(tài)屬性名稱 ?xx ?就是 ?@Input()? 字段的名字。該屬性的值可以是基于其返回類型的常規(guī)類型窄化函數(shù),也可以是字符串,例如 ?NgIf ?中的 ?"binding"?。

例如,考慮以下結(jié)構(gòu)型指令,該指令以模板表達(dá)式的結(jié)果作為輸入:

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>? 類型允許兩個狀態(tài)之一, ?Loaded<T>? 或 ?Loading ?。用作指令的 ?state ?輸入的表達(dá)式是寬泛的傘形類型 ?LoadingState?,因為還不知道此時的加載狀態(tài)是什么。

?IfLoadedDirective ?定義聲明了靜態(tài)字段 ?ngTemplateGuard_state?,以表示其窄化行為。在 ?AppComponent ?模板中,?*ifLoaded? 結(jié)構(gòu)型指令只有當(dāng)實(shí)際的 ?state ?是 ?Loaded<Person>? 類型時,才會渲染該模板。類型守護(hù)允許類型檢查器推斷出模板中可接受的 ?state ?類型是 ?Loaded<T>?,并進(jìn)一步推斷出 ?T? 必須是一個 ?Person ?的實(shí)例。

為指令的上下文指定類型

如果你的結(jié)構(gòu)型指令要為實(shí)例化的模板提供一個上下文,可以通過提供靜態(tài)的 ?ngTemplateContextGuard ?函數(shù)在模板中給它提供合適的類型。下面的代碼片段展示了該函數(shù)的一個例子。

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

    // …
}


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號