结构型指令
Structural directives
本章将看看 Angular 如何用结构型指令操纵 DOM 树,以及你该如何写自己的结构型指令来完成同样的任务。
This guide looks at how Angular manipulates the DOM with structural directives and how you can write your own structural directives to do the same thing.
什么是结构型指令?
What are structural directives?
结构型指令的职责是 HTML 布局。 它们塑造或重塑 DOM 的结构,比如添加、移除或维护这些元素。
Structural directives are responsible for HTML layout. They shape or reshape the DOM's structure, typically by adding, removing, or manipulating elements.
像其它指令一样,你可以把结构型指令应用到一个宿主元素上。 然后它就可以对宿主元素及其子元素做点什么。
As with other directives, you apply a structural directive to a host element. The directive then does whatever it's supposed to do with that host element and its descendants.
结构型指令非常容易识别。 在这个例子中,星号(*)被放在指令的属性名之前。
Structural directives are easy to recognize. An asterisk (*) precedes the directive attribute name as in this example.
<div *ngIf="hero" class="name">{{hero.name}}</div>
没有方括号,没有圆括号,只是把 *ngIf
设置为一个字符串。
No brackets. No parentheses. Just *ngIf
set to a string.
在这个例子中,你将学到星号(*)这个简写方法,而这个字符串是一个微语法,而不是通常的模板表达式。 Angular 会解开这个语法糖,变成一个 <ng-template>
标记,包裹着宿主元素及其子元素。 每个结构型指令都可以用这个模板做点不同的事情。
You'll learn in this guide that the asterisk (*) is a convenience notation and the string is a microsyntax rather than the usual template expression. Angular desugars this notation into a marked-up <ng-template>
that surrounds the host element and its descendents. Each structural directive does something different with that template.
三个常用的内置结构型指令 —— NgIf、NgFor和NgSwitch...。 你在模板语法一章中学过它,并且在 Angular 文档的例子中到处都在用它。下面是模板中的例子:
Three of the common, built-in structural directives—NgIf, NgFor, and NgSwitch...—are described in the Template Syntax guide and seen in samples throughout the Angular documentation. Here's an example of them in a template:
<div *ngIf="hero" class="name">{{hero.name}}</div>
<ul>
<li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>
<div [ngSwitch]="hero?.emotion">
<app-happy-hero *ngSwitchCase="'happy'" [hero]="hero"></app-happy-hero>
<app-sad-hero *ngSwitchCase="'sad'" [hero]="hero"></app-sad-hero>
<app-confused-hero *ngSwitchCase="'confused'" [hero]="hero"></app-confused-hero>
<app-unknown-hero *ngSwitchDefault [hero]="hero"></app-unknown-hero>
</div>
本章不会重复讲如何使用它们,而是解释它们的工作原理以及如何写自己的结构型指令。
This guide won't repeat how to use them. But it does explain how they work and how to write your own structural directive.
在本章中,你将看到指令同时具有两种拼写形式大驼峰 UpperCamelCase
和小驼峰 lowerCamelCase
,比如你已经看过的 NgIf
和 ngIf
。 这里的原因在于,NgIf
引用的是指令的类名,而 ngIf
引用的是指令的属性名*。
Throughout this guide, you'll see a directive spelled in both UpperCamelCase and lowerCamelCase. Already you've seen NgIf
and ngIf
. There's a reason. NgIf
refers to the directive class; ngIf
refers to the directive's attribute name.
指令的类名拼写成大驼峰形式(NgIf
),而它的属性名则拼写成小驼峰形式(ngIf
)。 本章会在谈论指令的属性和工作原理时引用指令的类名,在描述如何在 HTML 模板中把该指令应用到元素时,引用指令的属性名。
A directive class is spelled in UpperCamelCase (NgIf
). A directive's attribute name is spelled in lowerCamelCase (ngIf
). The guide refers to the directive class when talking about its properties and what the directive does. The guide refers to the attribute name when describing how you apply the directive to an element in the HTML template.
还有另外两种 Angular 指令,在本开发指南的其它地方有讲解:(1) 组件 (2) 属性型指令。
There are two other kinds of Angular directives, described extensively elsewhere: (1) components and (2) attribute directives.
组件可以在原生 HTML 元素中管理一小片区域的 HTML。从技术角度说,它就是一个带模板的指令。
A component manages a region of HTML in the manner of a native HTML element. Technically it's a directive with a template.
属性型指令会改变某个元素、组件或其它指令的外观或行为。 比如,内置的NgStyle
指令可以同时修改元素的多个样式。
An attribute directive changes the appearance or behavior of an element, component, or another directive. For example, the built-in NgStyle
directive changes several element styles at the same time.
你可以在一个宿主元素上应用多个属性型指令,但只能应用一个结构型指令。
You can apply many attribute directives to one host element. You can only apply one structural directive to a host element.
NgIf 案例分析
NgIf case study
NgIf
是一个很好的结构型指令案例:它接受一个布尔值,并据此让一整块 DOM 树出现或消失。
NgIf
is the simplest structural directive and the easiest to understand. It takes a boolean expression and makes an entire chunk of the DOM appear or disappear.
<p *ngIf="true">
Expression is true and ngIf is true.
This paragraph is in the DOM.
</p>
<p *ngIf="false">
Expression is false and ngIf is false.
This paragraph is not in the DOM.
</p>
ngIf
指令并不是使用 CSS 来隐藏元素的。它会把这些元素从 DOM 中物理删除。 使用浏览器的开发者工具就可以确认这一点。
The ngIf
directive doesn't hide elements with CSS. It adds and removes them physically from the DOM. Confirm that fact using browser developer tools to inspect the DOM.
可以看到第一段文字出现在了 DOM 中,而第二段则没有,在第二段的位置上是一个关于“绑定”的注释(稍后有更多讲解)。
The top paragraph is in the DOM. The bottom, disused paragraph is not; in its place is a comment about "bindings" (more about that later).
当条件为假时,NgIf
会从 DOM 中移除它的宿主元素,取消它监听过的那些 DOM 事件,从 Angular 变更检测中移除该组件,并销毁它。 这些组件和 DOM 节点可以被当做垃圾收集起来,并且释放它们占用的内存。
When the condition is false, NgIf
removes its host element from the DOM, detaches it from DOM events (the attachments that it made), detaches the component from Angular change detection, and destroys it. The component and DOM nodes can be garbage-collected and free up memory.
为什么移除而不是隐藏?
Why remove rather than hide?
指令也可以通过把它的 display
风格设置为 none
而隐藏不需要的段落。
A directive could hide the unwanted paragraph instead by setting its display
style to none
.
<p [style.display]="'block'">
Expression sets display to "block".
This paragraph is visible.
</p>
<p [style.display]="'none'">
Expression sets display to "none".
This paragraph is hidden but still in the DOM.
</p>
当不可见时,这个元素仍然留在 DOM 中。
While invisible, the element remains in the DOM.
对于简单的段落,隐藏和移除之间的差异影响不大,但对于资源占用较多的组件是不一样的。 当隐藏掉一个元素时,组件的行为还在继续 —— 它仍然附加在它所属的 DOM 元素上, 它也仍在监听事件。Angular 会继续检查哪些能影响数据绑定的变更。 组件原本要做的那些事情仍在继续。
The difference between hiding and removing doesn't matter for a simple paragraph. It does matter when the host element is attached to a resource intensive component. Such a component's behavior continues even when hidden. The component stays attached to its DOM element. It keeps listening to events. Angular keeps checking for changes that could affect data bindings. Whatever the component was doing, it keeps doing.
虽然不可见,组件及其各级子组件仍然占用着资源,而这些资源如果分配给别人可能会更有用。 在性能和内存方面的负担相当可观,响应度会降低,而用户却可能无法从中受益。
Although invisible, the component—and all of its descendant components—tie up resources. The performance and memory burden can be substantial, responsiveness can degrade, and the user sees nothing.
当然,从积极的一面看,重新显示这个元素会非常快。 组件以前的状态被保留着,并随时可以显示。 组件不用重新初始化 —— 该操作可能会比较昂贵。 这时候隐藏和显示就成了正确的选择。
On the positive side, showing the element again is quick. The component's previous state is preserved and ready to display. The component doesn't re-initialize—an operation that could be expensive. So hiding and showing is sometimes the right thing to do.
但是,除非有非常强烈的理由来保留它们,否则你会更倾向于移除用户看不见的那些 DOM 元素,并且使用 NgIf
这样的结构型指令来收回用不到的资源。
But in the absence of a compelling reason to keep them around, your preference should be to remove DOM elements that the user can't see and recover the unused resources with a structural directive like NgIf
.
同样的考量也适用于每一个结构型指令,无论是内置的还是自定义的。 你应该提醒自己慎重考虑添加元素、移除元素以及创建和销毁组件的后果。
These same considerations apply to every structural directive, whether built-in or custom. Before applying a structural directive, you might want to pause for a moment to consider the consequences of adding and removing elements and of creating and destroying components.
星号(*)前缀
The asterisk (*) prefix
你可能注意到了指令名的星号(*)前缀,并且困惑于为什么需要它以及它是做什么的。
Surely you noticed the asterisk (*) prefix to the directive name and wondered why it is necessary and what it does.
这里的 *ngIf
会在 hero
存在时显示英雄的名字。
Here is *ngIf
displaying the hero's name if hero
exists.
<div *ngIf="hero" class="name">{{hero.name}}</div>
星号是一个用来简化更复杂语法的“语法糖”。 从内部实现来说,Angular 把 *ngIf
属性 翻译成一个 <ng-template>
元素 并用它来包裹宿主元素,代码如下:
The asterisk is "syntactic sugar" for something a bit more complicated. Internally, Angular translates the *ngIf
attribute into a <ng-template>
element, wrapped around the host element, like this.
<ng-template [ngIf]="hero">
<div class="name">{{hero.name}}</div>
</ng-template>
*ngIf
指令被移到了<ng-template>
元素上。在那里它变成了一个属性绑定[ngIf]
。The
*ngIf
directive moved to the<ng-template>
element where it became a property binding,[ngIf]
.<div>
上的其余部分,包括它的class
属性在内,移到了内部的<ng-template>
元素上。The rest of the
<div>
, including its class attribute, moved inside the<ng-template>
element.
第一种形态永远不会真的渲染出来。 只有最终产出的结果才会出现在 DOM 中。
The first form is not actually rendered, only the finished product ends up in the DOM.
Angular 会在真正渲染的时候填充 <ng-template>
的内容,并且把 <ng-template>
替换为一个供诊断用的注释。
Angular consumed the <ng-template>
content during its actual rendering and replaced the <ng-template>
with a diagnostic comment.
NgFor
和NgSwitch...
指令也都遵循同样的模式。
The NgFor
and NgSwitch...
directives follow the same pattern.
*ngFor
内幕
Inside *ngFor
Angular 会把 *ngFor
用同样的方式把星号(*
)语法的 template
属性转换成 <ng-template>
元素。
Angular transforms the *ngFor
in similar fashion from asterisk (*) syntax to <ng-template>
element.
这里有一个 NgFor
的全特性应用,同时用了这两种写法:
Here's a full-featured application of NgFor
, written both ways:
<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>
它明显比 ngIf
复杂得多,确实如此。 NgFor
指令比本章展示过的 NgIf
具有更多的必选特性和可选特性。 至少 NgFor
会需要一个循环变量(let hero
)和一个列表(heroes
)。
This is manifestly more complicated than ngIf
and rightly so. The NgFor
directive has more features, both required and optional, than the NgIf
shown in this guide. At minimum NgFor
needs a looping variable (let hero
) and a list (heroes
).
你可以通过把一个字符串赋值给 ngFor
来启用这些特性,这个字符串使用 Angular 的微语法。
You enable these features in the string assigned to ngFor
, which you write in Angular's microsyntax.
ngFor
字符串之外的每一样东西都会留在宿主元素(<div>
)上,也就是说它移到了 <ng-template>
内部。 在这个例子中,[class.odd]="odd"
留在了 <div>
上。
Everything outside the ngFor
string stays with the host element (the <div>
) as it moves inside the <ng-template>
. In this example, the [class.odd]="odd"
stays on the <div>
.
微语法
Microsyntax
Angular 微语法能让你通过简短的、友好的字符串来配置一个指令。 微语法解析器把这个字符串翻译成 <ng-template>
上的属性:
The Angular microsyntax lets you configure a directive in a compact, friendly string. The microsyntax parser translates that string into attributes on the <ng-template>
:
let
关键字声明一个模板输入变量,你会在模板中引用它。本例子中,这个输入变量就是hero
、i
和odd
。 解析器会把let hero
、let i
和let odd
翻译成命名变量let-hero
、let-i
和let-odd
。The
let
keyword declares a template input variable that you reference within the template. The input variables in this example arehero
,i
, andodd
. The parser translateslet hero
,let i
, andlet odd
into variables namedlet-hero
,let-i
, andlet-odd
.微语法解析器接收
of
和trackby
,把它们首字母大写(of
->Of
,trackBy
->TrackBy
), 并且给它们加上指令的属性名(ngFor
)前缀,最终生成的名字是ngForOf
和ngForTrackBy
。 这两个最终生成的名字是NgFor
的输入属性,指令据此了解到列表是heroes
,而 track-by 函数是trackById
。The microsyntax parser title-cases all directives and prefixes them with the directive's attribute name, such as
ngFor
. For example, thengFor
input properties,of
andtrackBy
, becomengForOf
andngForTrackBy
, respectively. That's how the directive learns that the list isheroes
and the track-by function istrackById
.NgFor
指令在列表上循环,每个循环中都会设置和重置它自己的上下文对象上的属性。 这些属性包括但不限于index
和odd
以及一个特殊的属性名$implicit
(隐式变量)。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
.let-i
和let-odd
变量是通过let i=index
和let odd=odd
来定义的。 Angular 把它们设置为上下文对象中的index
和odd
属性的当前值。The
let-i
andlet-odd
variables were defined aslet i=index
andlet odd=odd
. Angular sets them to the current value of the context'sindex
andodd
properties.这里并没有指定
let-hero
的上下文属性。它的来源是隐式的。 Angular 将let-hero
设置为此上下文中$implicit
属性的值, 它是由NgFor
用当前迭代中的英雄初始化的。The context property for
let-hero
wasn't specified. Its intended source is implicit. Angular setslet-hero
to the value of the context's$implicit
property, whichNgFor
has initialized with the hero for the current iteration.API 参考手册中描述了
NgFor
指令的其它属性和上下文属性。The
NgFor
API guide describes additionalNgFor
directive properties and context properties.NgForOf
指令实现了NgFor
。请到 NgForOf API 参考手册中了解NgForOf
指令的更多属性及其上下文属性。The
NgForOf
directive implementsNgFor
. Read more about additionalNgForOf
directive properties and context properties in the NgForOf API reference.
编写你自己的结构型指令
Writing your own structural directives
当你编写自己的结构型指令时,也可以利用这些微语法机制。 例如,Angular 中的微语法允许你写成 <div *ngFor="let item of items">{{item}}</div>
而不是 <ng-template ngFor let-item [ngForOf]="items"><div>{{item}}</div></ng-template>
。 以下各节提供了有关约束、语法和微语法翻译方式的详细信息。
These microsyntax mechanisms are also available to you when you write your own structural directives. For example, microsyntax in Angular allows you to write <div *ngFor="let item of items">{{item}}</div>
instead of <ng-template ngFor let-item [ngForOf]="items"><div>{{item}}</div></ng-template>
. The following sections provide detailed information on constraints, grammar, and translation of microsyntax.
约束
Constraints
微语法必须满足以下要求:
Microsyntax must meet the following requirements:
它必须可被预先了解,以便 IDE 可以解析它而无需知道指令的底层语义或已存在哪些指令。
It must be known ahead of time so that IDEs can parse it without knowing the underlying semantics of the directive or what directives are present.
它必须转换为 DOM 中的“键-值”属性。
It must translate to key-value attributes in the DOM.
语法
Grammar
当你编写自己的结构型指令时,请使用以下语法:
When you write your own structural directives, use the following grammar:
*:prefix="( :let | :expression ) (';' | ',')? ( :let | :as | :keyExp )*"
下表描述了微语法的每个组成部分。
The following tables describe each portion of the microsyntax grammar.
prefix | HTML 属性键(attribute key) HTML attribute key |
key | HTML 属性键(attribute key) 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 ";"? |
翻译
Translation
将微语法转换为常规的绑定语法,如下所示:
A microsyntax is translated to the normal binding syntax as follows:
微语法 Microsyntax | 翻译结果 Translation |
---|---|
| [prefix]="expression" |
keyExp |
|
let | let-local="export" |
微语法样例
Microsyntax examples
下表说明了 Angular 会如何解开微语法。
The following table demonstrates how Angular desugars microsyntax.
微语法 Microsyntax | 解语法糖后 Desugared |
---|---|
*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"> |
这些微语法机制在你写自己的结构型指令时也同样有效,参考 NgIf
的源码 和 NgFor
的源码 可以学到更多。
Studying the source code for NgIf
and NgForOf
is a great way to learn more.
模板输入变量
Template input variable
模板输入变量是这样一种变量,你可以在单个实例的模板中引用它的值。 这个例子中有好几个模板输入变量:hero
、i
和 odd
。 它们都是用 let
作为前导关键字。
A template input variable is a variable whose value you can reference within a single instance of the template. There are several such variables in this example: hero
, i
, and odd
. All are preceded by the keyword let
.
模板输入变量和模板引用变量是不同的,无论是在语义上还是语法上。
A template input variable is not the same as a template reference variable, neither semantically nor syntactically.
你使用 let
关键字(如 let hero
)在模板中声明一个模板输入变量。 这个变量的范围被限制在所重复模板的单一实例上。 事实上,你可以在其它结构型指令中使用同样的变量名。
You declare a template input variable using the let
keyword (let hero
). The variable's scope is limited to a single instance of the repeated template. You can use the same variable name again in the definition of other structural directives.
而声明模板引用变量使用的是给变量名加 #
前缀的方式(#var
)。 一个引用变量引用的是它所附着到的元素、组件或指令。它可以在整个模板的任意位置访问。
You declare a template reference variable by prefixing the variable name with #
(#var
). A reference variable refers to its attached element, component or directive. It can be accessed anywhere in the entire template.
模板输入变量和引用变量具有各自独立的命名空间。let hero
中的 hero
和 #hero
中的 hero
并不是同一个变量。
Template input and reference variable names have their own namespaces. The hero
in let hero
is never the same variable as the hero
declared as #hero
.
每个宿主元素上只能有一个结构型指令
One structural directive per host element
有时你会希望只有当特定的条件为真时才重复渲染一个 HTML 块。 你可能试过把 *ngFor
和 *ngIf
放在同一个宿主元素上,但 Angular 不允许。这是因为你在一个元素上只能放一个结构型指令。
Someday you'll want to repeat a block of HTML but only when a particular condition is true. You'll try to put both an *ngFor
and an *ngIf
on the same host element. Angular won't let you. You may apply only one structural directive to an element.
原因很简单。结构型指令可能会对宿主元素及其子元素做很复杂的事。当两个指令放在同一个元素上时,谁先谁后?NgIf
优先还是 NgFor
优先?NgIf
可以取消 NgFor
的效果吗? 如果要这样做,Angular 应该如何把这种能力泛化,以取消其它结构型指令的效果呢?
The reason is simplicity. Structural directives can do complex things with the host element and its descendents. When two directives lay claim to the same host element, which one takes precedence? Which should go first, the NgIf
or the NgFor
? Can the NgIf
cancel the effect of the NgFor
? If so (and it seems like it should be so), how should Angular generalize the ability to cancel for other structural directives?
对这些问题,没有办法简单回答。而禁止多个结构型指令则可以简单地解决这个问题。 这种情况下有一个简单的解决方案:把 *ngIf
放在一个"容器"元素上,再包装进 *ngFor
元素。 这个元素可以使用ng-container
,以免引入一个新的 HTML 层级。
There are no easy answers to these questions. Prohibiting multiple structural directives makes them moot. There's an easy solution for this use case: put the *ngIf
on a container element that wraps the *ngFor
element. One or both elements can be an ng-container
so you don't have to introduce extra levels of HTML.
NgSwitch
内幕
Inside NgSwitch directives
Angular 的 NgSwitch
实际上是一组相互合作的指令:NgSwitch
、NgSwitchCase
和 NgSwitchDefault
。
The Angular NgSwitch is actually a set of cooperating directives: NgSwitch
, NgSwitchCase
, and NgSwitchDefault
.
例子如下:
Here's an example.
<div [ngSwitch]="hero?.emotion">
<app-happy-hero *ngSwitchCase="'happy'" [hero]="hero"></app-happy-hero>
<app-sad-hero *ngSwitchCase="'sad'" [hero]="hero"></app-sad-hero>
<app-confused-hero *ngSwitchCase="'confused'" [hero]="hero"></app-confused-hero>
<app-unknown-hero *ngSwitchDefault [hero]="hero"></app-unknown-hero>
</div>
一个值(hero.emotion
)被被赋值给了 NgSwitch
,以决定要显示哪一个分支。
The switch value assigned to NgSwitch
(hero.emotion
) determines which (if any) of the switch cases are displayed.
NgSwitch
本身不是结构型指令,而是一个属性型指令,它控制其它两个 switch 指令的行为。 这也就是为什么你要写成 [ngSwitch]
而不是 *ngSwitch
的原因。
NgSwitch
itself is not a structural directive. It's an attribute directive that controls the behavior of the other two switch directives. That's why you write [ngSwitch]
, never *ngSwitch
.
NgSwitchCase
和 NgSwitchDefault
都是结构型指令。 因此你要使用星号(*
)前缀来把它们附着到元素上。 NgSwitchCase
会在它的值匹配上选项值的时候显示它的宿主元素。 NgSwitchDefault
则会当没有兄弟 NgSwitchCase
匹配上时显示它的宿主元素。
NgSwitchCase
and NgSwitchDefault
are structural directives. You attach them to elements using the asterisk (*) prefix notation. An NgSwitchCase
displays its host element when its value matches the switch value. The NgSwitchDefault
displays its host element when no sibling NgSwitchCase
matches the switch value.
指令所在的元素就是它的宿主元素。 <happy-hero>
是 *ngSwitchCase
的宿主元素。 <unknown-hero>
是 *ngSwitchDefault
的宿主元素。
The element to which you apply a directive is its host element. The <happy-hero>
is the host element for the happy *ngSwitchCase
. The <unknown-hero>
is the host element for the *ngSwitchDefault
.
像其它的结构型指令一样,NgSwitchCase
和 NgSwitchDefault
也可以解开语法糖,变成 <ng-template>
的形式。
As with other structural directives, the NgSwitchCase
and NgSwitchDefault
can be desugared into the <ng-template>
element form.
<div [ngSwitch]="hero?.emotion">
<ng-template [ngSwitchCase]="'happy'">
<app-happy-hero [hero]="hero"></app-happy-hero>
</ng-template>
<ng-template [ngSwitchCase]="'sad'">
<app-sad-hero [hero]="hero"></app-sad-hero>
</ng-template>
<ng-template [ngSwitchCase]="'confused'">
<app-confused-hero [hero]="hero"></app-confused-hero>
</ng-template >
<ng-template ngSwitchDefault>
<app-unknown-hero [hero]="hero"></app-unknown-hero>
</ng-template>
</div>
优先使用星号(*
)语法
Prefer the asterisk (*) syntax.
星号(*
)语法比不带语法糖的形式更加清晰。 如果找不到单一的元素来应用该指令,可以使用<ng-container>作为该指令的容器。
The asterisk (*) syntax is more clear than the desugared form. Use <ng-container> when there's no single element to host the directive.
虽然很少有理由在模板中使用结构型指令的属性形式和元素形式,但这些幕后知识仍然是很重要的,即:Angular 会创建 <ng-template>
,还要了解它的工作原理。 当需要写自己的结构型指令时,你就要使用 <ng-template>
。
While there's rarely a good reason to apply a structural directive in template attribute or element form, it's still important to know that Angular creates a <ng-template>
and to understand how it works. You'll refer to the <ng-template>
when you write your own structural directive.
<ng-template>元素
The <ng-template>
<ng-template>是一个 Angular 元素,用来渲染 HTML。 它永远不会直接显示出来。 事实上,在渲染视图之前,Angular 会把 <ng-template>
及其内容替换为一个注释。
The <ng-template> is an Angular element for rendering HTML. It is never displayed directly. In fact, before rendering the view, Angular replaces the <ng-template>
and its contents with a comment.
如果没有使用结构型指令,而仅仅把一些别的元素包装进 <ng-template>
中,那些元素就是不可见的。 在下面的这个短语"Hip! Hip! Hooray!"中,中间的这个 "Hip!"(欢呼声) 就是如此。
If there is no structural directive and you merely wrap some elements in a <ng-template>
, those elements disappear. That's the fate of the middle "Hip!" in the phrase "Hip! Hip! Hooray!".
<p>Hip!</p>
<ng-template>
<p>Hip!</p>
</ng-template>
<p>Hooray!</p>
Angular 抹掉了中间的那个 "Hip!",让欢呼声显得不再那么热烈了。
Angular erases the middle "Hip!", leaving the cheer a bit less enthusiastic.
结构型指令会让 <ng-template>
正常工作,在你写自己的结构型指令时就会看到这一点。
A structural directive puts a <ng-template>
to work as you'll see when you write your own structural directive.
使用<ng-container>把一些兄弟元素归为一组
Group sibling elements with <ng-container>
通常都需要一个根元素作为结构型指令的宿主。 列表元素(<li>
)就是一个典型的供 NgFor
使用的宿主元素。
There's often a root element that can and should host the structural directive. The list element (<li>
) is a typical host element of an NgFor
repeater.
<li *ngFor="let hero of heroes">{{hero.name}}</li>
当没有这样一个单一的宿主元素时,你就可以把这些内容包裹在一个原生的 HTML 容器元素中,比如 <div>
,并且把结构型指令附加到这个"包裹"上。
When there isn't a host element, you can usually wrap the content in a native HTML container element, such as a <div>
, and attach the directive to that wrapper.
<div *ngIf="hero" class="name">{{hero.name}}</div>
但引入另一个容器元素(通常是 <span>
或 <div>
)来把一些元素归到一个单一的根元素下,通常也会带来问题。注意,是"通常"而不是"总会"。
Introducing another container element—typically a <span>
or <div>
—to group the elements under a single root is usually harmless. Usually ... but not always.
这种用于分组的元素可能会破坏模板的外观表现,因为 CSS 的样式既不曾期待也不会接受这种新的元素布局。 比如,假设你有下列分段布局。
The grouping element may break the template appearance because CSS styles neither expect nor accommodate the new layout. For example, suppose you have the following paragraph layout.
<p>
I turned the corner
<span *ngIf="hero">
and saw {{hero.name}}. I waved
</span>
and continued on my way.
</p>
而你的 CSS 样式规则是应用于 <p>
元素下的 <span>
的。
You also have a CSS style rule that happens to apply to a <span>
within a <p>
aragraph.
p span { color: red; font-size: 70%; }
这样渲染出来的段落就会非常奇怪。
The constructed paragraph renders strangely.
本来为其它地方准备的 p span
样式,被意外的应用到了这里。
The p span
style, intended for use elsewhere, was inadvertently applied here.
另一个问题是:有些 HTML 元素需要所有的直属下级都具有特定的类型。 比如,<select>
元素要求直属下级必须为 <option>
,那就没办法把这些选项包装进 <div>
或 <span>
中。
Another problem: some HTML elements require all immediate children to be of a specific type. For example, the <select>
element requires <option>
children. You can't wrap the options in a conditional <div>
or a <span>
.
如果这样做:
When you try this,
<div>
Pick your favorite hero
(<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
</div>
<select [(ngModel)]="hero">
<span *ngFor="let h of heroes">
<span *ngIf="showSad || h.emotion !== 'sad'">
<option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
</span>
</span>
</select>
下拉列表就是空的。
the drop down is empty.
浏览器不会显示 <span>
中的 <option>
。
The browser won't display an <option>
within a <span>
.
<ng-container> 的救赎
<ng-container> to the rescue
Angular 的 <ng-container>
是一个分组元素,但它不会污染样式或元素布局,因为 Angular 压根不会把它放进 DOM 中。
The Angular <ng-container>
is a grouping element that doesn't interfere with styles or layout because Angular doesn't put it in the DOM.
下面是重新实现的条件化段落,这次使用 <ng-container>
。
Here's the conditional paragraph again, this time using <ng-container>
.
<p>
I turned the corner
<ng-container *ngIf="hero">
and saw {{hero.name}}. I waved
</ng-container>
and continued on my way.
</p>
这次就渲染对了。
It renders properly.
现在用 <ng-container>
来根据条件排除选择框中的某个 <option>
。
Now conditionally exclude a select <option>
with <ng-container>
.
<div>
Pick your favorite hero
(<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
</div>
<select [(ngModel)]="hero">
<ng-container *ngFor="let h of heroes">
<ng-container *ngIf="showSad || h.emotion !== 'sad'">
<option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
</ng-container>
</ng-container>
</select>
下拉框也工作正常。
The drop down works properly.
注意: 记住,ngModel 指令是在 Angular 的 FormsModule 中定义的,你要在想使用它的模块的 imports: [...]
元数据中导入 FormsModule。
Note: Remember that ngModel directive is defined as a part of Angular FormsModule and you need to include FormsModule in the imports: [...] section of the Angular module metadata, in which you want to use it.
<ng-container>
是一个由 Angular 解析器负责识别处理的语法元素。 它不是一个指令、组件、类或接口,更像是 JavaScript 中 if
块中的花括号。
The <ng-container>
is a syntax element recognized by the Angular parser. It's not a directive, component, class, or interface. It's more like the curly braces in a JavaScript if
-block:
if (someCondition) {
statement1;
statement2;
statement3;
}
没有这些花括号,JavaScript 只会执行第一句,而你原本的意图是把其中的所有语句都视为一体来根据条件执行。 而 <ng-container>
满足了 Angular 模板中类似的需求。
Without those braces, JavaScript would only execute the first statement when you intend to conditionally execute all of them as a single block. The <ng-container>
satisfies a similar need in Angular templates.
写一个结构型指令
Write a structural directive
在本节中,你会写一个名叫 UnlessDirective
的结构型指令,它是 NgIf
的反义词。 NgIf
在条件为 true
的时候显示模板内容,而 UnlessDirective
则会在条件为 false
时显示模板内容。
In this section, you write an UnlessDirective
structural directive that does the opposite of NgIf
. NgIf
displays the template content when the condition is true
. UnlessDirective
displays the content when the condition is false.
<p *appUnless="condition">Show this sentence unless the condition is true.</p>
创建指令很像创建组件。
Creating a directive is similar to creating a component.
导入
Directive
装饰器(而不再是Component
)。Import the
Directive
decorator (instead of theComponent
decorator).导入符号
Input
、TemplateRef
和ViewContainerRef
,你在任何结构型指令中都会需要它们。Import the
Input
,TemplateRef
, andViewContainerRef
symbols; you'll need them for any structural directive.给指令类添加装饰器。
Apply the decorator to the directive class.
设置 CSS 属性选择器,以便在模板中标识出这个指令该应用于哪个元素。
Set the CSS attribute selector that identifies the directive when applied to an element in a template.
这里是起点:
Here's how you might begin:
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
}
指令的选择器通常是把指令的属性名括在方括号中,如 [appUnless]
。 这个方括号定义出了一个 CSS 属性选择器。
The directive's selector is typically the directive's attribute name in square brackets, [appUnless]
. The brackets define a CSS attribute selector.
该指令的属性名应该拼写成小驼峰形式,并且带有一个前缀。 但是,这个前缀不能用 ng
,因为它只属于 Angular 本身。 请选择一些简短的,适合你自己或公司的前缀。 在这个例子中,前缀是 app
。
The directive attribute name should be spelled in lowerCamelCase and begin with a prefix. Don't use ng
. That prefix belongs to Angular. Pick something short that fits you or your company. In this example, the prefix is app
.
指令的类名用 Directive
结尾,参见风格指南。 但 Angular 自己的指令例外。
The directive class name ends in Directive
per the style guide. Angular's own directives do not.
TemplateRef 和 ViewContainerRef
TemplateRef and ViewContainerRef
像这个例子一样的简单结构型指令会从 Angular 生成的 <ng-template>
元素中创建一个内嵌的视图,并把这个视图插入到一个视图容器中,紧挨着本指令原来的宿主元素 <p>
(译注:注意不是子节点,而是兄弟节点)。
A simple structural directive like this one 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
来访问这个视图容器。
You'll acquire the <ng-template>
contents with a TemplateRef
and access the view container through a ViewContainerRef
.
你可以把它们都注入到指令的构造函数中,作为该类的私有属性。
You inject both in the directive constructor as private variables of the class.
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef) { }
appUnless 属性
The appUnless property
该指令的使用者会把一个 true/false 条件绑定到 [appUnless]
属性上。 也就是说,该指令需要一个带有 @Input
的 appUnless
属性。
The directive consumer expects to bind a true/false condition to [appUnless]
. That means the directive needs an appUnless
property, decorated with @Input
Read about @Input
in the Template Syntax guide.
@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
属性。因为不能用 appUnless
属性,所以你要为它定义一个设置器(setter)。
Angular sets the appUnless
property whenever the value of the condition changes. Because the appUnless
property does work, it needs a setter.
如果条件为假,并且以前尚未创建过该视图,就告诉视图容器(ViewContainer)根据模板创建一个内嵌视图。
If the condition is falsy and the view hasn't been created previously, tell the view container to create the embedded view from the template.
如果条件为真,并且视图已经显示出来了,就会清除该容器,并销毁该视图。
If the condition is truthy and the view is currently displayed, clear the container which also destroys the view.
没有人会读取 appUnless
属性,因此它不需要定义 getter。
Nobody reads the appUnless
property so it doesn't need a getter.
完整的指令代码如下:
The completed directive code looks like this:
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;
}
}
}
把这个指令添加到 AppModule 的 declarations
数组中。
Add this directive to the declarations
array of the AppModule.
然后创建一些 HTML 来试用一下。
Then create some HTML to try it.
<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>
当 condition
为 false
时,顶部的段落就会显示出来,而底部的段落消失了。 当 condition
为 true
时,顶部的段落被移除了,而底部的段落显示了出来。
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 is removed and the bottom (B) paragraph appears.
改进自定义指令的模板类型检查
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 those mistakes can cause.
使用类型守护属性可以告诉模板类型检查器你所期望的类型,从而改进该模板的编译期类型检查。
Use the type-guard properties to inform the template type checker of an expected type, thus improving compile-time type-checking for that template.
属性
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 example of both kinds of type-guard property.
有关更多信息,请参阅模板类型检查指南。
For more information, see Template type checking guide.
使用模板守护功能可以让模板内的类型需求更具体
Make 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
字段名。该属性的值既可以是针对其返回类型的通用类型窄化函数,也可以是字符串 "binding"
就像 NgIf
一样。
To provide a more specific type for an input expression to a directive within the template, add a ngTemplateGuard_xx
property to the directive, where the suffix to the static property name 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.
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: `{{ state.data }}`,
})
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
结构型指令只有当实际的 state
是 Loaded<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.
@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; };
// …
}
小结
Summary
You can both try and download the source code for this guide in the
本章相关的代码如下:
Here is the source from the src/app/
folder.
import { Component } from '@angular/core';
import { Hero, heroes } from './hero';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
heroes = heroes;
hero = this.heroes[0];
condition = false;
logs: string[] = [];
showSad = true;
status = 'ready';
trackById(index: number, hero: Hero): number { return hero.id; }
}
你学到了
You learned:
结构型指令可以操纵 HTML 的元素布局。
that structural directives manipulate HTML layout.
当没有合适的宿主元素时,可以使用
<ng-container>
对元素进行分组。to use
<ng-container>
as a grouping element when there is no suitable host element.Angular 会把星号(*)语法解开成
<ng-template>
。that the Angular desugars asterisk (*) syntax into a
<ng-template>
.内置指令
NgIf
、NgFor
和NgSwitch
的工作原理。how that works for the
NgIf
,NgFor
andNgSwitch
built-in directives.微语法如何展开成
<ng-template>
。about the microsyntax that expands into a
<ng-template>
.写了一个自定义结构型指令 ——
UnlessDirective
。to write a custom structural directive,
UnlessDirective
.