阅读(1025) (0)

Angular 结构型指令

2022-06-28 10:45:52 更新

编写结构型指令

本主题演示如何创建结构型指令,并提供有关指令如何工作、Angular 如何解释简写形式以及如何添加模板守卫属性以捕获模板类型错误的概念性信息。

有关此页面描述的示例应用程序,请参见现场演练 / 下载范例 。

创建结构型指令

本节将指导你创建 ​UnlessDirective ​以及如何设置 ​condition ​值。 ​UnlessDirective ​与 ​NgIf ​相反,并且 ​condition ​值可以设置为 ​true ​或 ​false ​。 ​NgIf ​为 ​true ​时显示模板内容;而 ​UnlessDirective ​在这个条件为 ​false ​时显示内容。

以下是应用于 p 元素的 ​UnlessDirective ​选择器 ​appUnless ​当 ​condition ​为 ​false ​,浏览器将显示该句子。

<p *appUnless="condition">Show this sentence unless the condition is true.</p>
  1. 使用 Angular CLI,运行以下命令,其中 ​unless ​是伪指令的名称:
  2. ng generate directive unless

    Angular 会创建指令类,并指定 CSS 选择器 ​appUnless​,它会在模板中标识指令。

  3. 导入 ​Input​、​TemplateRef ​和 ​ViewContainerRef​。
  4. import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
    
    @Directive({ selector: '[appUnless]'})
    export class UnlessDirective {
    }
  5. 在指令的构造函数中将 ​TemplateRef ​和 ​ViewContainerRef ​注入成私有变量。
  6. constructor(
      private templateRef: TemplateRef<any>,
      private viewContainer: ViewContainerRef) { }

    UnlessDirective ​会通过 Angular 生成的 ​<ng-template>​ 创建一个嵌入的视图,然后将该视图插入到该指令的原始 ​<p>​ 宿主元素紧后面的视图容器中。

    TemplateRef​可帮助你获取 ​<ng-template>​ 的内容,而 ​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;
      }
    }

    每当条件的值更改时,Angular 都会设置 ​appUnless ​属性。

    • 如果条件是假值,并且 Angular 以前尚未创建视图,则此 setter 会导致视图容器从模板创建出嵌入式视图。
    • 如果条件为真值,并且当前正显示着视图,则此 setter 会清除容器,这会导致销毁该视图。

完整的指令如下:

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

测试指令

在本节中,你将更新你的应用程序,以测试 ​UnlessDirective ​。

  1. 添加一个 ​condition ​设置为 ​false ​的 ​AppComponent ​。
  2. condition = false;
  3. 更新模板以使用指令。这里,​*appUnless​ 位于两个具有相反 ​condition ​的 ​<p>​ 标记上,一个为 ​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 ​标记为结构型指令的简写形式。如果 ​condition ​是假值,则会让顶部段落 A,而底部段落 B 消失。当 ​condition ​为真时,顶部段落 A 消失,而底部段落 B 出现。

  5. 要在浏览器中更改并显示 ​condition ​的值,请添加一段标记代码以显示状态和按钮。
  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 ​的值。


结构型指令简写形式

结构型指令(例如 ​*ngIf​)上的星号 ​​语法是 Angular 解释为较长形式的简写形式。 Angular 将结构型指令前面的星号转换为围绕宿主元素及其后代的 ​<ng-template>​。

下面是一个 ​*ngIf​ 的示例,如果 ​hero ​存在,则显示英雄的名称:

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

*ngIf​ 指令移到了 ​<ng-template>​ 上,在这里它成为绑定在方括号 ​[ngIf]​ 中的属性。 ​<div>​ 的其余部分(包括其 class 属性)移到了 ​<ng-template>​ 内部。

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

Angular 不会创建真正的 ​<ng-template>​ 元素,只会将 ​<div>​ 和注释节点占位符渲染到 DOM 中。

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

*ngFor​ 中的星号的简写形式与非简写的 ​<ng-template>​ 形式进行比较:

<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>​ 中。

let ​关键字会声明一个模板输入变量,你可以在模板中引用该变量。在这个例子中,是 ​hero​、​i​ 和 ​odd​。解析器将 ​let hero​、​let i​ 和 ​let odd​ 转换为名为 ​let-hero​、​let-i​ 和 ​let-odd​ 的变量。 ​let-i​ 和 ​let-odd​ 变量变为 ​let i=index​ 和 ​let odd=odd​ 。 Angular 会将 ​i​ 和 ​odd ​设置为上下文中 ​index ​和 ​odd ​属性的当前值。

解析器会将 PascalCase 应用于所有指令,并为它们加上指令的属性名称(例如 ngFor)。比如,​ngFor ​的输入特性 ​of ​和 ​trackBy ​,会映射为 ​ngForOf ​和 ​ngForTrackBy ​。当 ​NgFor ​指令遍历列表时,它会设置和重置它自己的上下文对象的属性。这些属性可以包括但不限于 ​index​、​odd ​和一个名为 ​$implicit​ 的特殊属性。

Angular 会将 ​let-hero​ 设置为上下文的 ​$implicit​ 属性的值, ​NgFor ​已经将其初始化为当前正在迭代的英雄。

用 <ng-template> 创建模板片段

Angular 的 ​<ng-template>​ 元素定义了一个默认情况下不渲染任何内容的模板。使用 ​<ng-template>​ ,你可以手动渲染内容,以完全控制内容的显示方式。

如果没有结构型指令,并且将某些元素包装在 ​<ng-template>​ 中,则这些元素会消失。在下面的示例中,Angular 不会渲染中间的 “Hip!”,因为它被 ​<ng-template>​ 包裹着。

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


结构型指令语法参考

当你编写自己的结构型指令时,请使用以下语法:

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

下表描述了结构型指令语法的每个部分:

prefix

HTML 属性的键名

key

HTML 属性的键名

local

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

export

该指令以特定名称导出的值

expression

标准 Angular 表达式

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

Angular 如何翻译简写形式

Angular 会将结构型指令的简写形式转换为普通的绑定语法,如下所示:

简写形式

翻译结果

prefix 和裸 expression

[prefix]="expression"
keyExp

[prefixKey] "expression" (let-prefixKey="export")
注意,这个 prefix 已经加到了 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">

改进自定义指令的模板类型检查

你可以通过将模板守卫属性添加到指令定义中来改进自定义指令的模板类型检查。这些属性可帮助 Angular 的模板类型检查器在编译时发现模板中的错误,从而避免运行时错误。这些属性如下:

  • ngTemplateGuard_(someInputProperty)​ 属性使你可以为模板中的输入表达式指定更准确的类型。
  • 静态属性 ​ngTemplateContextGuard ​声明了模板上下文的类型。

使用模板守卫使模板中的类型要求更具体

模板中的结构型指令会根据输入表达式来控制是否要在运行时渲染该模板。为了帮助编译器捕获模板类型中的错误,你应该尽可能详细地指定模板内指令的输入表达式所期待的类型。

类型保护函数会将输入表达式的预期类型缩小为可能在运行时传递给模板内指令的类型的子集。你可以提供这样的功能来帮助类型检查器在编译时为表达式推断正确的类型。

例如,​NgIf ​的实现使用类型窄化来确保只有当 ​*ngIf​ 的输入表达式为真时,模板才会被实例化。为了提供具体的类型要求,​NgIf ​指令定义了一个静态属性 ​ngTemplateGuard_ngIf: 'binding'​。这里的 ​binding ​值是一种常见的类型窄化的例子,它会对输入表达式进行求值,以满足类型要求。

要为模板中指令的输入表达式提供更具体的类型,请在指令中添加 ​ngTemplateGuard_xx ​属性,其中静态属性名称 ​xx ​就是 ​@Input()​ 字段的名字。该属性的值可以是基于其返回类型的常规类型窄化函数,也可以是字符串,例如 ​NgIf ​中的 ​"binding"​。

例如,考虑以下结构型指令,该指令以模板表达式的结果作为输入:

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​,因为还不知道此时的加载状态是什么。

IfLoadedDirective ​定义声明了静态字段 ​ngTemplateGuard_state​,以表示其窄化行为。在 ​AppComponent ​模板中,​*ifLoaded​ 结构型指令只有当实际的 ​state ​是 ​Loaded<Person>​ 类型时,才会渲染该模板。类型守护允许类型检查器推断出模板中可接受的 ​state ​类型是 ​Loaded<T>​,并进一步推断出 ​T​ 必须是一个 ​Person ​的实例。

为指令的上下文指定类型

如果你的结构型指令要为实例化的模板提供一个上下文,可以通过提供静态的 ​ngTemplateContextGuard ​函数在模板中给它提供合适的类型。下面的代码片段展示了该函数的一个例子。

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

    // …
}