# 结构型指令

本指南是关于结构指令的,并提供了有关此类指令的工作方式、Angular 如何解释它们的速记语法以及如何添加模板保护属性以捕获模板类型错误的概念信息。

结构指令是通过添加和删除 DOM 元素来更改 DOM 布局的指令。

Angular 提供了一组内置的结构指令(例如 NgIfNgForOfNgSwitch等),在所有 Angular 项目中通用。有关更多信息,请参阅内置指令。

TIP

对于本页面介绍的示例应用程序,请参阅现场演练/ 下载范例 .

# 结构型指令简写形式

应用结构指令时,它们通常以星号 *为前缀,例如 *ngIf。本约定是 Angular 解释并转换为更长形式的速记。Angular 会将结构指令前面的星号转换为围绕宿主元素及其后代的 <ng-template>

例如,让我们采取以下代码,如果 hero存在,则使用 *ngIf来显示英雄的名字:

src/app/app.component.html (asterisk)

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

Angular 创建一个 &lt;ng-template&gt;元素,并将 *ngIf指令应用于它,在那里它成为方括号中的属性绑定 [ngIf]。然后,&lt;div&gt;的其余部分(包括其 class 属性)会在 &lt;ng-template&gt;中移动:

src/app/app.component.html (ngif-template)

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

请注意,Angular 实际上并没有创建真正的 &lt;ng-template&gt;元素,而是仅渲染 &lt;div&gt;元素。


<div _ngcontent-c0>Mr. Nice</div>

*ngFor中的星号的简写形式与非简写的 &lt;ng-template&gt;形式进行比较:

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结构指令相关的所有内容都被移动到 &lt;ng-template&gt;。元素上的所有其他绑定和属性都适用于 &lt;ng-template&gt;中的 &lt;div&gt;元素。当元素在 &lt;ng-template&gt;中移动时,宿主元素上的其他修饰符(除了 ngFor字符串)会保持在原位。在此示例中,[class.odd]="odd"保留在 &lt;div&gt;上。

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

解析器将 PascalCase 应用于所有指令,并以指令的属性名称为前缀,例如 ngFor。例如,ngFor输入属性 oftrackBy映射到 ngForOfngForTrackBy

NgFor指令遍历列表时,它会设置和重置其自己的上下文对象的属性。这些属性可以包括但不限于 indexodd和名为 $implicit的特殊属性。

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

有关更多信息,请参见 NgFor APINgForOf API 文档。

TIP

请注意,Angular 的 &lt;ng-template&gt;元素定义了一个默认不渲染任何内容的模板,如果你只是在 &lt;ng-template&gt;中包装元素而不应用结构指令,则不会渲染这些元素。

有关更多信息,请参阅ng-template API文档。

# 每个元素一个结构指令

重复一个 HTML 块是一个很常见的用例,但前提是在特定条件为真时。一种直观的方法是将 *ngFor*ngIf放在同一个元素上。但是,由于 *ngFor*ngIf都是结构指令,因此编译器会将其视为错误。你只能将一个 结构 指令应用于一个元素。

原因是简单。结构指令可以用宿主元素及其后代做复杂的事情。

当两个指令都声称使用了同一个宿主元素时,哪一个应该优先?

哪个应该先走,NgIfNgForNgIf可以取消 NgFor的效果吗?如果是这样(看起来应该是这样),Angular 应该如何概括其他结构指令的取消能力?

这些问题没有简单的答案。禁止多个结构指令使它们没有实际意义。这个用例有一个简单的解决方案:将 *ngIf放在包装 *ngFor元素的容器元素上。一个或两个元素可以是 &lt;ng-container&gt;,以便不会生成额外的 DOM 元素。

# 创建结构型指令

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

以下是应用于 p 元素的 UnlessDirective选择器 appUnlessconditionfalse,浏览器将显示该句子。

src/app/app.component.html (appUnless-1)

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

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

  • 导入 InputTemplateRefViewContainerRef

src/app/unless.directive.ts (skeleton)

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

@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
}
  • 在指令的构造函数中将 TemplateRefViewContainerRef注入成私有变量。

src/app/unless.directive.ts (ctor)

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

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

TemplateRef 可帮助你获取 &lt;ng-template&gt;的内容,而 ViewContainerRef 可以访问视图容器。

  • 添加一个带 setter 的 @Input()属性 appUnless

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 以前尚未创建视图,则此 setter 会导致视图容器从模板创建出嵌入式视图。

  • 如果条件为真值,并且当前正显示着视图,则此 setter 会清除容器,这会导致销毁该视图。

完整的指令如下:

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

# 测试指令

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

  • 添加一个 condition设置为 falseAppComponent

src/app/app.component.ts (excerpt)

condition = false;
  • 更新模板以使用指令。这里,*appUnless位于两个具有相反 condition&lt;p&gt;标记上,一个为 true,一个为 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 出现。

  • 要在浏览器中更改并显示 condition的值,请添加一段标记代码以显示状态和按钮。

src/app/app.component.html

<p>
  The condition is currently
  <span [ngClass]="{ 'a': !condition, 'b': condition, 'unless': true }">{{condition}}</span>.
  <button
    type="button"
    (click)="condition = !condition"
    [ngClass] = "{ 'a': condition, 'b': !condition }" >
    Toggle condition to {{condition ? 'false' : 'true'}}
  </button>
</p>

要验证指令是否有效,请单击按钮以更改 condition的值。

UnlessDirective in action

# 结构型指令语法参考

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

*:prefix="( :let | :expression ) (';' | ',')? ( :let | :as | :keyExp )*"下表描述了结构型指令语法的每个部分:

as


as = :export "as" :local ";"?

keyExp


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

let


let = "let" :local "=" :export ";"?
关键字 详情
prefix HTML 属性的键名HTML
key HTML 属性的键名HTML
local 在模板中使用的局部变量名
export 该指令以特定名称导出的值
expression 标准 Angular 表达式

# Angular 如何翻译简写形式

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

简写形式 翻译结果
prefix 和裸 expression [prefix]="expression"
keyExp [prefixKey] "expression" (let-prefixKey="export") 注意: prefix 被加到了 key 上 [prefixKey] "expression" (let-prefixKey="export")
keyExp [prefixKey] "expression" (let-prefixKey="export")
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"

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

src/app/if-loaded.directive.ts

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

import { Loaded, LoadingState } from './loading-state';

@Directive({ selector: '[appIfLoaded]' })
export class IfLoadedDirective<T> {
  private isViewCreated = false;

  @Input('appIfLoaded') set state(state: LoadingState<T>) {
    if (!this.isViewCreated && state.type === 'loaded') {
      this.viewContainerRef.createEmbeddedView(this.templateRef);
      this.isViewCreated = true;
    } else if (this.isViewCreated && state.type !== 'loaded') {
      this.viewContainerRef.clear();
      this.isViewCreated = false;
    }
  }

  constructor(
    private readonly viewContainerRef: ViewContainerRef,
    private readonly templateRef: TemplateRef<unknown>
  ) {}

  static ngTemplateGuard_appIfLoaded<T>(
    dir: IfLoadedDirective<T>,
    state: LoadingState<T>
  ): state is Loaded<T> {
    return true;
  }
}

src/app/loading-state.ts

export type Loaded<T> = { type: 'loaded', data: T };

export type Loading = { type: 'loading' };

export type LoadingState<T> = Loaded<T> | Loading;

src/app/hero.component.ts

import { Component } from '@angular/core';

import { LoadingState } from './loading-state';
import { Hero, heroes } from './hero';

@Component({
  selector: 'app-hero',
  template: `
    <button (click)="onLoadHero()">Load Hero</button>
    <p *appIfLoaded="heroLoadingState">{{ heroLoadingState.data | json }}</p>
  `,
})
export class HeroComponent {
  heroLoadingState: LoadingState<Hero> = { type: 'loading' };

  onLoadHero(): void {
    this.heroLoadingState = { type: 'loaded', data: heroes[0] };
  }
}

在这个例子中,LoadingState<T&gt;类型允许两个状态之一,Loaded<T&gt;Loading。用作指令的 state输入(别名为 appIfLoaded)的表达式是宽泛的伞形类型 LoadingState,因为还不知道此时的加载状态是什么。

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

# 为指令的上下文指定类型

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

src/app/trigonometry.directive.ts

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

@Directive({ selector: '[appTrigonometry]' })
export class TrigonometryDirective {
  private isViewCreated = false;
  private readonly context = new TrigonometryContext();

  @Input('appTrigonometry') set angle(angleInDegrees: number) {
    const angleInRadians = toRadians(angleInDegrees);
    this.context.sin = Math.sin(angleInRadians);
    this.context.cos = Math.cos(angleInRadians);
    this.context.tan = Math.tan(angleInRadians);

    if (!this.isViewCreated) {
      this.viewContainerRef.createEmbeddedView(this.templateRef, this.context);
      this.isViewCreated = true;
    }
  }

  constructor(
    private readonly viewContainerRef: ViewContainerRef,
    private readonly templateRef: TemplateRef<TrigonometryContext>
  ) {}

  // Make sure the template checker knows the type of the context with which the
  // template of this directive will be rendered
  static ngTemplateContextGuard(
    directive: TrigonometryDirective,
    context: unknown
  ): context is TrigonometryContext {
    return true;
  }
}

class TrigonometryContext {
  sin = 0;
  cos = 0;
  tan = 0;
}

function toRadians(degrees: number): number {
  return degrees * (Math.PI / 180);
}

src/app/app.component.html (appTrigonometry)

<ul *appTrigonometry="30; sin as s; cos as c; tan as t">
  <li>sin(30°): {{ s }}</li>
  <li>cos(30°): {{ c }}</li>
  <li>tan(30°): {{ t }}</li>
</ul>
Last Updated: 5/13/2023, 10:57:08 AM