在本章中,我们将看看Angular如何用结构型指令操纵DOM树,以及我们该如何写自己的结构型指令来完成同样的任务。
目录
Table of contents
什么是结构型指令?
What are structural directives?
结构型指令的职责是HTML布局。 它们塑造或重塑DOM的结构,比如添加、移除或维护这些元素。
像其它指令一样,你可以把结构型指令应用到一个宿主元素上。 然后它就可以对宿主元素及其子元素做点什么。
结构型指令非常容易识别。 在这个例子中,星号(*)被放在指令的属性名之前。
<div *ngIf="hero" >{{hero.name}}</div>
没有方括号,没有圆括号,只是把*ngIf
设置为一个字符串。
在这个例子中,我们将学到星号(*)这个简写方法,而这个字符串是一个微语法,而不是通常的模板表达式。
Angular会解开这个语法糖,变成一个<ng-template>
标记,包裹着宿主元素及其子元素。
每个结构型指令都可以用这个模板做点不同的事情。
三个常用的内置结构型指令 —— NgIf、NgFor和NgSwitch...。 我们在模板语法一章中讲过它,并且在Angular文档的例子中到处都在用它。下面是模板中的例子:
<div *ngIf="hero" >{{hero.name}}</div>
<ul>
<li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>
<div [ngSwitch]="hero?.emotion">
<happy-hero *ngSwitchCase="'happy'" [hero]="hero"></happy-hero>
<sad-hero *ngSwitchCase="'sad'" [hero]="hero"></sad-hero>
<confused-hero *ngSwitchCase="'confused'" [hero]="hero"></confused-hero>
<unknown-hero *ngSwitchDefault [hero]="hero"></unknown-hero>
</div>
本章不会重复讲如何使用它们,而是解释它们的工作原理以及如何写自己的结构型指令。
在本章中,我们将看到指令同时具有两种拼写形式大驼峰UpperCamelCase
和小驼峰lowerCamelCase
,比如我们已经看过的NgIf
和ngIf
。
这里的原因在于,NgIf
引用的是指令的类名,而ngIf
引用的是指令的属性名*。
指令的类名拼写成大驼峰形式(NgIf
),而它的属性名则拼写成小驼峰形式(ngIf
)。
本章会在谈论指令的属性和工作原理时引用指令的类名,在描述如何在HTML模板中把该指令应用到元素时,引用指令的属性名。
还有另外两种Angular指令,在本开发指南的其它地方有讲解:(1) 组件 (2) 属性型指令。
组件可以在原生HTML元素中管理一小片区域的HTML。从技术角度说,它就是一个带模板的指令。
属性型指令会改变某个元素、组件或其它指令的外观或行为。
比如,内置的NgStyle
指令可以同时修改元素的多个样式。
我们可以在一个宿主元素上应用多个属性型指令,但只能应用一个结构型指令。
NgIf案例分析
NgIf case study
我们重点看下ngIf
。它是一个很好的结构型指令案例:它接受一个布尔值,并据此让一整块DOM树出现或消失。
<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中物理删除。
使用浏览器的开发者工具就可以确认这一点。

可以看到第一段文字出现在了DOM中,而第二段则没有,在第二段的位置上是一个关于“绑定”的注释(稍后有更多讲解)。
当条件为假时,NgIf
会从DOM中移除它的宿主元素,取消它监听过的那些DOM事件,从Angular变更检测中移除该组件,并销毁它。
这些组件和DOM节点可以被当做垃圾收集起来,并且释放它们占用的内存。
为什么移除而不是隐藏?
Why remove rather than hide?
指令也可以通过把它的display
风格设置为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中。

对于简单的段落,隐藏和移除之间的差异影响不大,但对于资源占用较多的组件是不一样的。当我们隐藏掉一个元素时,组件的行为还在继续 —— 它仍然附加在它所属的DOM元素上, 它也仍在监听事件。Angular会继续检查哪些能影响数据绑定的变更。 组件原本要做的那些事情仍在继续。
虽然不可见,组件及其各级子组件仍然占用着资源,而这些资源如果分配给别人可能会更有用。 在性能和内存方面的负担相当可观,响应度会降低,而用户却可能无法从中受益。
当然,从积极的一面看,重新显示这个元素会非常快。 组件以前的状态被保留着,并随时可以显示。 组件不用重新初始化 —— 该操作可能会比较昂贵。 这时候隐藏和显示就成了正确的选择。
但是,除非有非常强烈的理由来保留它们,否则我们更倾向于移除用户看不见的那些DOM元素,并且使用NgIf
这样的结构型指令来收回用不到的资源。
同样的考量也适用于每一个结构型指令,无论是内置的还是自定义的。 我们应该提醒自己以及我们指令的使用者,来仔细考虑添加元素、移除元素以及创建和销毁组件的后果。
星号(*)前缀
The asterisk (*) prefix
你可能注意到了指令名的星号(*)前缀,并且困惑于为什么需要它以及它是做什么的。
这里的*ngIf
会在hero
存在时显示英雄的名字。
<div *ngIf="hero" >{{hero.name}}</div>
星号是一个用来简化更复杂语法的“语法糖”。
从内部实现来说,Angular会分两个阶段解开这个语法糖。
首先,它把*ngIf="..."
翻译成一个template
属性 template="ngIf ..."
,代码如下:
<div template="ngIf hero">{{hero.name}}</div>
然后,它把这个template
属性翻译成一个<ng-template>
元素,并用它包裹宿主元素,代码如下:
<ng-template [ngIf]="hero">
<div>{{hero.name}}</div>
</ng-template>
-
*ngIf
指令被移到了<ng-template>
元素上。在那里它变成了一个属性绑定[ngIf]
。 -
<div>
上的其余部分,包括它的class
属性在内,移到了内部的<ng-template>
元素上。
上述形式永远不会真的渲染出来。 只有最终产出的结果才会出现在DOM中。

Angular会在真正渲染的时候填充<ng-template>
的内容,并且把<ng-template>
替换为一个供诊断用的注释。
NgFor
和NgSwitch...
指令也都遵循同样的模式。
*ngFor
内幕
Inside *ngFor
Angular会把*ngFor
用同样的方式把星号()语法的template
属性转换成<ng-template>
元素*。
这里有一个NgFor
的全特性应用,同时用了这三种写法:
<div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd">
({{i}}) {{hero.name}}
</div>
<div template="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
)。
我们可以通过把一个字符串赋值给ngFor
来启用这些特性,这个字符串使用Angular的微语法。
ngFor
字符串之外的每一样东西都会留在宿主元素(<div>
)上,也就是说它移到了<ng-template>
内部。
在这个例子中,[ngClass]="odd"
留在了<div>
上。
微语法
Microsyntax
Angular微语法能让我们通过简短的、友好的字符串来配置一个指令。
微语法解析器把这个字符串翻译成<ng-template>
上的属性:
-
let
关键字声明一个模板输入变量,我们会在模板中引用它。本例子中,这个输入变量就是hero
、i
和odd
。 解析器会把let hero
、let i
和let odd
翻译成命名变量let-hero
、let-i
和let-odd
。 -
微语法解析器接收
of
和trackby
,把它们首字母大写(of
->Of
,trackBy
->TrackBy
), 并且给它们加上指令的属性名(ngFor
)前缀,最终生成的名字是ngForOf
和ngForTrackBy
。 还有两个NgFor
的输入属性,指令据此了解到列表是heroes
,而track-by函数是trackById
。 -
NgFor
指令在列表上循环,每个循环中都会设置和重置它自己的上下文对象上的属性。 这些属性包括index
和odd
以及一个特殊的属性名$implicit
(隐式变量)。 -
let-i
和let-odd
变量是通过let i=index
和let odd=odd
来定义的。 Angular把它们设置为上下文对象中的index
和odd
属性的当前值。 -
上下文中的属性
let-hero
没有指定过,实际上它来自一个隐式变量。 Angular会把let-hero
设置为上下文对象中的$implicit
属性,NgFor
会用当前迭代中的英雄初始化它。 -
API参考手册中描述了
NgFor
指令的其它属性和上下文属性。
这些微语法机制在你写自己的结构型指令时也同样有效,参考NgIf
的源码
和NgFor
的源码 可以学到更多。
模板输入变量
Template input variable
模板输入变量是这样一种变量,你可以在单个实例的模板中引用它的值。
这个例子中有好几个模板输入变量:hero
、i
和odd
。
它们都是用let
作为前导关键字。
A template input variable is not the same as a template reference variable, neither semantically nor syntactically.
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.
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.
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
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.
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?
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.
Inside NgSwitch directives
The Angular NgSwitch is actually a set of cooperating directives: NgSwitch
, NgSwitchCase
, and NgSwitchDefault
.
Here's an example.
<div [ngSwitch]="hero?.emotion">
<happy-hero *ngSwitchCase="'happy'" [hero]="hero"></happy-hero>
<sad-hero *ngSwitchCase="'sad'" [hero]="hero"></sad-hero>
<confused-hero *ngSwitchCase="'confused'" [hero]="hero"></confused-hero>
<unknown-hero *ngSwitchDefault [hero]="hero"></unknown-hero>
</div>
The switch value assigned to NgSwitch
(hero.emotion
) determines which
(if any) of the switch cases are displayed.
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
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.
设计思路:要最小化初始化的成本,并考虑把状态缓存在一个伴生的服务中。
As with other structural directives, the NgSwitchCase
and NgSwitchDefault
can be desugared into the template attribute form.
同样的考量也适用于每一个结构型指令,无论是内置的还是自定义的。 我们应该提醒自己以及我们指令的使用者,来仔细考虑添加元素、移除元素以及创建和销毁组件的后果。
让我们在实践中看看这些变化。为了娱乐,我们设想在甲板上有个叫heavy-loader
(重型起重机)的组件,它会假装在初始化时装载一吨数据。
我们将显示该组件的两个实例。我们使用CSS切换第一个实例的可见性,用ngIf
把第二个实例添加到DOM和将其移除。
出错的文件: ../../../_fragments/structural-directives/ts/src/app/structural-directives.component-message-log.html.md 所在路径: docs,ts,latest,guide,structural-directives 文档路径: ../../../
That, in turn, can be desugared into the <ng-template>
element form.
<div [ngSwitch]="hero?.emotion">
<ng-template [ngSwitchCase]="'happy'">
<happy-hero [hero]="hero"></happy-hero>
</ng-template>
<ng-template [ngSwitchCase]="'sad'">
<sad-hero [hero]="hero"></sad-hero>
</ng-template>
<ng-template [ngSwitchCase]="'confused'">
<confused-hero [hero]="hero"></confused-hero>
</ng-template >
<ng-template ngSwitchDefault>
<unknown-hero [hero]="hero"></unknown-hero>
</ng-template>
</div>
Prefer the asterisk (*) syntax.
The asterisk (*) syntax is more clear than the other desugared forms. Use <ng-container> when there's no single element to host the directive.
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.
The <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.
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>
借助内置的ngOnInit
和ngOnDestroy
生命周期钩子,我们同时记录了组件的创建或销毁过程。
下面是它的操作演示:

A structural directive puts a <ng-template>
to work
as you'll see when you write your own structural directive.
Group sibling elements with <ng-container>
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>
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" >{{hero.name}}</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.
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>
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.

The p span
style, intended for use elsewhere, was inadvertently applied here.
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.

The browser won't display an <option>
within a <span>
.
<ng-container> to the rescue
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.
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.

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.

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;
}
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
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 *myUnless="condition">Show this sentence unless the condition is true.</p>
创建指令很像创建组件。
-
导入
Directive
装饰器(而不再是Component
)。 Import the
Input
,TemplateRef
, andViewContainerRef
symbols; you'll need them for any structural directive .Apply the decorator to the directive class.
Set the CSS attribute selector that identifies the directive when applied to an element in a template.
Here's how you might begin:
src/app/unless.directive.ts (skeleton)
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({ selector: '[myUnless]'})
export class UnlessDirective {
}
The directive's selector is typically the directive's attribute name in square brackets, [myUnless]
.
The brackets define a CSS
attribute selector.
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 my
.
The directive class name ends in Directive
per the style guide.
Angular's own directives do not.
TemplateRef and ViewContainerRef
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.
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) { }
The myUnless property
The directive consumer expects to bind a true/false condition to [myUnless]
.
That means the directive needs a myUnless
property, decorated with @Input
Read about @Input
in the Template Syntax guide.
@Input() set myUnless(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 sets the myUnless
property whenever the value of the condition changes.
Because the myUnless
property does work, it needs a setter.
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.
Nobody reads the myUnless
property so it doesn't need a getter.
The completed directive code looks like this:
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: '[myUnless]'})
export class UnlessDirective {
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef) { }
@Input() set myUnless(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;
}
}
}
Add this directive to the declarations
array of the AppModule.
Then create some HTML to try it.
<p *myUnless="condition" class="unless a">
(A) This paragraph is displayed because the condition is false.
</p>
<p *myUnless="!condition" class="unless b">
(B) Although the condition is true,
this paragraph is displayed because myUnless is set to false.
</p>
当condition
为false
时,顶部的段落就会显示出来,而底部的段落消失了。
当condition
为true
时,顶部的段落被移除了,而底部的段落显示了出来。

Summary
You can both try and download the source code for this guide in the
本章相关的代码如下:
import { Component } from '@angular/core';
import { Hero, heroes } from './hero';
@Component({
selector: 'my-app',
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
- that structural directives manipulate HTML layout.
- to use
<ng-container>
as a grouping element when there is no suitable host element. - that the Angular desugars asterisk (*) syntax into a
<ng-template>
. - how that works for the
NgIf
,NgFor
andNgSwitch
built-in directives. - about the microsyntax that expands into a
<ng-template>
. - to write a custom structural directive,
UnlessDirective
.