Angular 结构型指令
编写结构型指令
本主题演示如何创建结构型指令,并提供有关指令如何工作、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>
- 使用 Angular CLI,运行以下命令,其中
unless
是伪指令的名称: - 导入
Input
、TemplateRef
和 ViewContainerRef
。 - 在指令的构造函数中将
TemplateRef
和 ViewContainerRef
注入成私有变量。 - 添加一个带 setter 的
@Input()
属性 appUnless
。 - 如果条件是假值,并且 Angular 以前尚未创建视图,则此 setter 会导致视图容器从模板创建出嵌入式视图。
- 如果条件为真值,并且当前正显示着视图,则此 setter 会清除容器,这会导致销毁该视图。
ng generate directive unless
Angular 会创建指令类,并指定 CSS 选择器 appUnless
,它会在模板中标识指令。
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
}
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef) { }
UnlessDirective
会通过 Angular 生成的 <ng-template>
创建一个嵌入的视图,然后将该视图插入到该指令的原始 <p>
宿主元素紧后面的视图容器中。
TemplateRef
可帮助你获取 <ng-template>
的内容,而 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;
}
}
每当条件的值更改时,Angular 都会设置 appUnless
属性。
完整的指令如下:
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
设置为 false
的 AppComponent
。 - 更新模板以使用指令。这里,
*appUnless
位于两个具有相反 condition
的 <p>
标记上,一个为 true
,一个为 false
。 - 要在浏览器中更改并显示
condition
的值,请添加一段标记代码以显示状态和按钮。
condition = false;
<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 出现。
<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"
|
keyExp
|
|
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; };
// …
}