结构型指令

One of the defining features of a single page application is its manipulation of the DOM tree. Instead of serving a whole new page every time a user navigates, whole sections of the DOM appear and disappear according to the application state. In this chapter we'll look at how Angular manipulates the DOM and how we can do it ourselves in our own directives.

单页面应用的基本特性之一,就是它要操纵DOM树。不同于以前那种用户每次浏览都重新从服务器取得整个页面的方式, 单页面应用中,DOM中的各个区域会根据应用程序的状态而出现或消失。 在本章中,我们将看看Angular如何操纵DOM树,以及我们该如何在自己的指令中这么做。

In this chapter we will

在本章中,我们将:

Try the .

试试在线例子

What are structural directives?

什么是结构型指令?

There are three kinds of Angular directives:

Angular指令可分为三种:

  1. Components

    组件

  2. Attribute directives

    属性型指令

  3. Structural directives

    结构型指令

The Component is really a directive with a template. It's the most common of the three directives and we write lots of them as we build our application.

组件其实就是一个带模板的指令。 它是这三种指令中最常用的,我们会写大量的组件来构建应用程序。

The Attribute directive changes the appearance or behavior of an element. The built-in NgStyle directive, for example, can change several element styles at the same time. We can use it to render text bold, italic, and lime green by binding to a component property that requests such a sickening result.

属性型指令会修改元素的外观或行为。 比如,内置指令NgStyle就能同时修改元素的好几个样式。 通过绑定到组件的属性,我们可以把文本渲染成加粗、斜体、灰绿色这种肉麻的效果。

A Structural directive changes the DOM layout by adding and removing DOM elements. We've seen three of the built-in structural directives in other chapters: ngIf, ngSwitch and ngFor.

结构型指令通过添加和删除DOM元素来改变DOM的布局。 我们会在其它章节看到三个内置的结构型指令:ngIfngSwitch以及ngFor

<div *ngIf="hero">{{hero}}</div> <div *ngFor="let hero of heroes">{{hero}}</div> <div [ngSwitch]="status"> <template [ngSwitchCase]="'in-mission'">In Mission</template> <template [ngSwitchCase]="'ready'">Ready</template> <template ngSwitchDefault>Unknown</template> </div>

NgIf Case Study

NgIf案例分析

Let's focus on ngIf. It's a great example of a structural directive: it takes a boolean and makes an entire chunk of DOM appear or disappear.

我们重点看下ngIf。它是一个很好的结构型指令案例:它接受一个布尔值,并据此让一整块DOM树出现或消失。

<p *ngIf="condition"> condition is true and ngIf is true. </p> <p *ngIf="!condition"> condition is false and ngIf is false. </p>

The ngIf directive does not hide the element. Using browser developer tools we can see that, when the condition is true, the top paragraph is in the DOM and the bottom disused paragraph is completely absent from the DOM! In its place are empty <script> tags.

ngIf指令并不会隐藏元素。 使用浏览器的开发者工具就会看到:当condition为真的时候,只剩下了DOM顶部的段落,而底部无用的段落完全从DOM中消失了! 在它的位置上是空白的<script>标签

element not in dom

Why remove rather than hide?

为什么移除而不是隐藏

We could hide the unwanted paragraph by setting its css display style to none. The element would remain in the DOM while invisible. Instead we removed it with ngIf.

其实也可以通过把CSS样式display设置为none来隐藏掉那个不想要的段落。 该元素仍然留在DOM中,只是看不到了。但我们却通过ngIf移除了它。

The difference matters. When we hide an element, the component's behavior continues. It remains attached to its DOM element. It continues to listen to events. Angular keeps checking for changes that could affect data bindings. Whatever the component was doing, it keeps doing it.

不同之处在于:当我们隐藏掉一个元素时,组件的行为还在继续 —— 它仍然附加在它所属的DOM元素上, 它也仍在监听事件。Angular会继续检查哪些能影响数据绑定的变更。 组件原本要做的那些事情仍在继续。

Although invisible, the component — and all of its descendant components — tie up resources that might be more useful elsewhere. The performance and memory burden can be substantial and the user may not benefit at all.

虽然不可见,组件及其各级子组件仍然占用着资源,而这些资源如果分配给别人可能会更有用。 在性能和内存方面的负担相当可观,而用户却可能无法从中受益。

On the positive side, showing the element again is very quick. The component's previous state is preserved and ready to display. The component doesn't re-initialize — an operation that could be expensive.

当然,从积极的一面看,重新显示这个元素会非常快。 组件以前的状态被保留着,并随时可以显示。 组件不用重新初始化 —— 该操作可能会比较昂贵。

ngIf is different. Setting ngIf to false does affect the component's resource consumption. Angular removes the element from DOM, stops change detection for the associated component, detaches it from DOM events (the attachments that it made) and destroys the component. The component can be garbage-collected (we hope) and free up memory.

ngIf不同。 把ngIf设置为假将会影响到组件的资源消耗。 Angular会从DOM中移除该元素,停止相关组件的变更检测,把它从DOM事件中摘掉(事件是组件造成的附加项),并销毁组件。 组件会被垃圾回收(希望如此)并释放内存。

Components often have child components which themselves have children. All of them are destroyed when ngIf destroys the common ancestor. This cleanup effort is usually a good thing.

组件通常还有子组件,子组件还有自己的子组件。 当ngIf销毁这个祖先组件时,它们全都会被销毁。 这种清理工作通常会是好事。

Of course it isn't always a good thing. It might be a bad thing if we need that particular component again soon.

当然,它也并不总是好事。 如果我们很快就会再次需要这个组件,它就变成坏事了。

The component's state might be expensive to re-construct. When ngIf becomes true again, Angular recreates the component and its subtree. Angular runs every component's initialization logic again. That could be expensive ... as when a component re-fetches data that had been in memory just moments ago.

重建组件的状态可能是昂贵的。 当ngIf重新变为true的时候,Angular会重新创建该组件及其子树。 Angular会重新运行每个组件的初始化逻辑。那可能会很昂贵……比如当组件需要重新获取刚刚还在内存中的数据时。

Design thought: minimize initialization effort and consider caching state in a companion service.

设计思路:要最小化初始化的成本,并考虑把状态缓存在一个伴生的服务中。

Although there are pros and cons to each approach, in general it is best to use ngIf to remove unwanted components rather than hide them.

虽然每种方法都有各自的优点和缺点,但使用ngIf来移除不需要的组件通常都会比隐藏它们更好一些。

These same considerations apply to every structural directive, whether built-in or custom. We should ask ourselves — and the users of our directives — to think carefully about the consequences of adding and removing elements and of creating and destroying components.

同样的考量也适用于每一个结构型指令,无论是内置的还是自定义的。 我们应该提醒自己以及我们指令的使用者,来仔细考虑添加元素、移除元素以及创建和销毁组件的后果。

Let's see these dynamics at work. For fun, we'll stack the deck against our recommendation and consider a component called heavy-loader that pretends to load a ton of data when initialized.

让我们在实践中看看这些变化。为了娱乐,我们设想在甲板上有个叫heavy-loader(重型起重机)的组件,它会假装在初始化时装载一吨数据。

We'll display two instances of the component. We toggle the visibility of the first one with CSS. We toggle the second into and out of the DOM with ngIf.

我们将显示该组件的两个实例。我们使用CSS切换第一个实例的可见性,用ngIf把第二个实例添加到DOM和将其移除。

<div><!-- Visibility --> <button (click)="isVisible = !isVisible">show | hide</button> <heavy-loader [style.display]="isVisible ? 'inline' : 'none'" [logs]="logs"></heavy-loader> </div> <div><!-- NgIf --> <button (click)="condition = !condition">if | !if</button> <heavy-loader *ngIf="condition" [logs]="logs"></heavy-loader> </div> <h4>heavy-loader log:</h4> <div *ngFor="let message of logs">{{message}}</div> import { Component, Input, OnDestroy, OnInit } from '@angular/core'; let nextId = 1; @Component({ selector: 'heavy-loader', template: '<span>heavy loader #{{id}} on duty!</span>' }) export class HeavyLoaderComponent implements OnDestroy, OnInit { id = nextId++; @Input() logs: string[]; ngOnInit() { // Mock todo: get 10,000 rows of data from the server this.log(`heavy-loader ${this.id} initialized, loading 10,000 rows of data from the server`); } ngOnDestroy() { // Mock todo: clean-up this.log(`heavy-loader ${this.id} destroyed, cleaning up`); } private log(msg: string) { this.logs.push(msg); this.tick(); } // Triggers the next round of Angular change detection // after one turn of the browser event loop // ensuring display of msg added in onDestroy private tick() { setTimeout(() => { }, 0); } }

We also log when a component is created or destroyed using the built-in ngOnInit and ngOnDestroy lifecycle hooks. Here it is in action:

借助内置的ngOnInitngOnDestroy生命周期钩子,我们同时记录了组件的创建或销毁过程。 下面是它的操作演示:

heavy loader toggle

Both components are in the DOM at the start. First we toggle the component's visibility repeatedly. The component never leaves the DOM. When visible it's always the same instance and the log is quiet.

开始的时候,两个组件都在DOM中。 首先我们重复切换第一个组件的可见性。组件从未离开过DOM节点。 当可见时,它总是同一个实例,而日志里什么都没有。

Then we toggle the second component with ngIf. We create a new instance every time and the log shows that we're paying a heavy price to create and destroy it.

当我们切换使用ngIf的第二个实例时。 我们每次都会创建新的实例,而日志中显示,我们为了创建和销毁它付出了沉重的代价。

If we really expected to "wink" the component like this, toggling visibility would be the better choice. In most UIs, when we "close" a component we're unlikely to see it again for a long time, if ever. The ngIf would be preferred in that case.

如果我们真的期望像这样让组件“眨眼”,切换可见性就会是更好的选择。 在大多数UI中,当我们“关闭”一个组件时,在相当长时间内都不大可能想再见到它 —— 可能永远也不见。 在这种情况下,我们会更喜欢ngIf

The <template> tag

<template>标签

Structural directives, like ngIf, do their magic by using the HTML 5 template tag.

结构型指令,比如ngIf,使用HTML 5的template标签 完成它们的“魔法”。

Outside of an Angular app, the <template> tag's default CSS display property is none. It's contents are invisible within a hidden document fragment.

在Angular应用之外,<template>标签的默认CSS属性displaynone。 它的内容存在于一个隐藏的文档片段中。

Inside of an app, Angular removes the<template> tags and their children. The contents are gone — but not forgotten as we'll see soon.

而在Angular应用中,Angular会移除<template>标签及其子元素。 这些内容不见了,但是并没有被“忘记”,我们很快就明白了。

We can confirm these effects by wrapping the middle "hip" of the phrase "Hip! Hip! Hooray!" within a <template> tag.

我们可以通过把短语"Hip! Hip! Hooray!"中间的"hip"包在一个<template>标签中来验证下这个效果。

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

The display is a 'Hip! Hooray!', short of perfect enthusiasm. The DOM effects are different when Angular is in control.

这时候显示的内容是'Hip! Hooray!',缺乏完美的热情(译注:因为少了一个词嘛)。在Angular的控制下,DOM的效果是不同的。

template outside angular

Evidently Angular replaces the <template> tag and its contents with empty <script> tags. That's just its default behavior. It can do something different as we saw when applying a variety of ngSwitch directives to <template> tags:

显然,Angular把<template>标签及其内容替换成了一个空白的<script>标签。 这只是它的默认行为。 当把ngSwitch家族的各种指令应用于<template>标签时,我们就会看到有些东西不一样了:

<div [ngSwitch]="status"> <template [ngSwitchCase]="'in-mission'">In Mission</template> <template [ngSwitchCase]="'ready'">Ready</template> <template ngSwitchDefault>Unknown</template> </div>

When one of those ngSwitch conditions is true, Angular inserts the template's content into the DOM.

当这些ngSwitch的条件之一为真的时候,Angular把模板的内容插入到了DOM中。

What does this have to do with ngIf and ngFor? We didn't use a <template> tag with those directives.

这和ngIfngFor有什么关系?很明显,我们在那些指令中并没有用到<template>标签。

The asterisk (*) effect

星号(*)效果

Here are those directives again. See the difference?

下面也是那些指令。看出有什么不同了吗?

<div *ngIf="hero">{{hero}}</div> <div *ngFor="let hero of heroes">{{hero}}</div>

We're prefixing these directive names with an asterisk (*).

我们把那些指令名加上了星号(*)前缀。

The asterisk is "syntactic sugar". It simplifies ngIf and ngFor for both the writer and the reader. Under the hood, Angular replaces the asterisk version with a more verbose <template> form.

这个星号是一种“语法糖”。它简化了ngIfngFor —— 无论是写还是读。

The next two ngIf examples are effectively the same and we may write in either style:

接下来这两个ngIf范例的效果完全相同,只是我们写成了另一种风格:

<!-- Examples (A) and (B) are the same --> <!-- (A) *ngIf paragraph --> <p *ngIf="condition"> Our heroes are true! </p> <!-- (B) [ngIf] with template --> <template [ngIf]="condition"> <p> Our heroes are true! </p> </template>

Most of us would rather write in style (A).

大多数都喜欢用风格(A)来写。

It's worth knowing that Angular expands style (A) into style (B). It moves the paragraph and its contents inside a <template> tag. It moves the directive up to the <template> tag where it becomes a property binding, surrounded in square brackets. The boolean value of the host component's condition property determines whether the templated content is displayed or not.

要知道,Angular会把风格(A)写成风格(B)。 它把段落及其内容移到了<template>标签中。 它把指令移到了<template>标签上,成为该标签的一个属性绑定 —— 包装在方括号中。 宿主组件的condition属性的布尔值决定该模板的内容是否应该被显示。

Angular transforms *ngFor in a similar manner:

Angular把*ngFor转换成一个类似的形式:

<!-- Examples (A) and (B) are the same --> <!-- (A) *ngFor div --> <div *ngFor="let hero of heroes">{{ hero }}</div> <!-- (B) ngFor with template --> <template ngFor let-hero [ngForOf]="heroes"> <div>{{ hero }}</div> </template>

The basic pattern is the same:  create a <template>, relocate the content, and move the directive onto the <template>.

基本的转换模式是一样的:创建一个<template>,将内容重定位,并且把指令移到<template>上。

There are extra nuances stemming from Angular's ngFor micro-syntax which expands into an additional ngForOf property binding (the iterable) and the hero template input variable (the current item in each iteration).

Angular的ngFor微语法里面有一些细微差别, 它被展开成了另一个ngForOf属性绑定(可迭代者)和另一个模板输入变量hero(每次迭代中的当前条目)。

Make a structural directive

制作一个结构型指令

Let's write our own structural directive, an Unless directive, the not-so-evil twin of ngIf.

我们来写自己的结构型指令:Unless,这是ngIf指令不那么邪恶的孪生兄弟。

Unlike ngIf which displays the template content when true, our directive displays the content when the condition is false.

当条件为truengIf才显示模板内容,与之不同的是,我们这个指令只有当条件是false时才显示这些内容。

Creating a directive is similar to creating a component.

创建指令很像创建组件。

Here is how we begin:

下面是最初的样子:

unless.directive.ts (excerpt)

import { Directive, Input } from '@angular/core'; @Directive({ selector: '[myUnless]' }) export class UnlessDirective { }

Selector brackets [ ]

选择器中的括号[ ]

The CSS syntax for selecting an attribute is a name in square brackets. We surround our directive name in square brackets. See Directive configuration on the cheatsheet.

在CSS中,用于选择属性(Attribute)的选择器就是放在方括号中的名字。 于是我们把指令名包裹在方括号中。参见小抄中的指令配置项

Selector name prefixes

选择器名称前缀

We recommend picking a selector name with a prefix to ensure that it cannot conflict with any standard HTML attribute, now or in the future.

我们建议在给选择器起名时加个前缀,以确保它不会和任何标准的HTML属性冲突,无论是现在还是未来。

We do not prefix our unless directive name with ng. That prefix belongs to Angular and we don't want to confuse our directives with their directives.

我们并没有unless指令名加上ng前缀。 那个前缀是属于Angular的,我们肯定不会希望自己的指令和Angular内置的指令冲突。

Our prefix is my.

我们用的前缀是my

We'll need access to the template and something that can render its contents. We access the template with a TemplateRef. The renderer is a ViewContainerRef. We inject both into our constructor as private variables.

我们需要访问模板,并且需要一个渲染器来渲染它的内容。 我们通过TemplateRef来访问模板。渲染器是ViewContainerRef。 我们把它们都作为私有变量注入到构造函数中。

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

The consumer of our directive will bind a boolean value to our directive's myUnless input property. The directive adds or removes the template based on that value.

这个指令的使用者将把一个布尔值绑定到指令的输入属性myUnless上。 该指令会基于这个值添加或移除此模板。

Let's add the myUnless property now as a setter-only property.

我们现在先把myUnless属性定义成一个“只写”属性。

@Input() set myUnless(condition: boolean) { if (!condition) { this.viewContainer.createEmbeddedView(this.templateRef); } else { this.viewContainer.clear(); } }

The @Input() annotation marks this property as an input for the directive.

@Input()装饰器表明这个属性对于指令来说是个输入属性。

Nothing fancy here: if the condition is false, we render the template, otherwise we clear the element content.

这里没什么特别的:如果条件为假,我们就渲染模板,否则就清空元素内容。

The end result should look like this:

最终看起来是这样的:

unless.directive.ts

import { Directive, Input } from '@angular/core'; import { TemplateRef, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[myUnless]' }) export class UnlessDirective { constructor( private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef ) { } @Input() set myUnless(condition: boolean) { if (!condition) { this.viewContainer.createEmbeddedView(this.templateRef); } else { this.viewContainer.clear(); } } }

Now we add it to the declarations array of the AppModule and try it. First we add some test HTML to the template:

现在,我们就来把它加到AppModule的declarations数组中,试一试。 我们首先把一些测试用的HTML添加到模板中:

<p *myUnless="condition"> condition is false and myUnless is true. </p> <p *myUnless="!condition"> condition is true and myUnless is false. </p>

We run it and it behaves as expected, doing the opposite of ngIf. When condition is true, the top paragraph is removed (replaced by <script> tags) and the bottom paragraph appears.

我们运行它,它的行为正如所预期的那样 —— 跟ngIf相反。 当conditiontrue时,顶部的段落被移除了(被替换为<script>标签),并且底部的段落显示了出来。

myUnless is true

Our myUnless directive is dead simple. Surely we left something out. Surely ngIf is more complex?

这个myUnless指令实在太简单了,我们肯定忘了点什么。 那么ngIf会更复杂吗?

Look at the source code. It's well documented and we shouldn't be shy about consulting the source when we want to know how something works.

看下源码。 它有很好的文档,况且,如果我们想了解某些东西的工作原理,也不用羞于“咨询”源码。

ngIf isn't much different! There are a few additional checks to improve performance (don't clear or recreate the view unless necessary) but otherwise it's much the same.

ngIf也没多大不同嘛!它做了更多的检查来提升性能(除非必要,否则它不会清除或重新创建视图),但其它的部分都跟我们写的一样。

Wrap up

总结

Here is the pertinent source for this chapter.

本章相关的代码如下:

import { Directive, Input } from '@angular/core'; import { TemplateRef, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[myUnless]' }) export class UnlessDirective { constructor( private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef ) { } @Input() set myUnless(condition: boolean) { if (!condition) { this.viewContainer.createEmbeddedView(this.templateRef); } else { this.viewContainer.clear(); } } } import { Component, Input, OnDestroy, OnInit } from '@angular/core'; let nextId = 1; @Component({ selector: 'heavy-loader', template: '<span>heavy loader #{{id}} on duty!</span>' }) export class HeavyLoaderComponent implements OnDestroy, OnInit { id = nextId++; @Input() logs: string[]; ngOnInit() { // Mock todo: get 10,000 rows of data from the server this.log(`heavy-loader ${this.id} initialized, loading 10,000 rows of data from the server`); } ngOnDestroy() { // Mock todo: clean-up this.log(`heavy-loader ${this.id} destroyed, cleaning up`); } private log(msg: string) { this.logs.push(msg); this.tick(); } // Triggers the next round of Angular change detection // after one turn of the browser event loop // ensuring display of msg added in onDestroy private tick() { setTimeout(() => { }, 0); } } import { Component } from '@angular/core'; @Component({ moduleId: module.id, selector: 'structural-directives', templateUrl: './structural-directives.component.html', styles: ['button { min-width: 100px; }'] }) export class StructuralDirectivesComponent { heroes = ['Mr. Nice', 'Narco', 'Bombasto']; hero = this.heroes[0]; condition = true; isVisible = true; logs: string[] = []; status = 'ready'; } <h1>Structural Directives</h1> <div *ngIf="hero">{{hero}}</div> <div *ngFor="let hero of heroes">{{hero}}</div> <div [ngSwitch]="status"> <template [ngSwitchCase]="'in-mission'">In Mission</template> <template [ngSwitchCase]="'ready'">Ready</template> <template ngSwitchDefault>Unknown</template> </div> <hr> <button (click)="condition = !condition" [style.background] = "condition ? 'orangered': 'lightgreen'" > Set 'condition' to {{condition ? 'False': 'True'}} </button> <p *ngIf="condition"> condition is true and ngIf is true. </p> <p *ngIf="!condition"> condition is false and ngIf is false. </p> <p *myUnless="condition"> condition is false and myUnless is true. </p> <p *myUnless="!condition"> condition is true and myUnless is false. </p> <hr> <div><!-- Visibility --> <button (click)="isVisible = !isVisible">show | hide</button> <heavy-loader [style.display]="isVisible ? 'inline' : 'none'" [logs]="logs"></heavy-loader> </div> <div><!-- NgIf --> <button (click)="condition = !condition">if | !if</button> <heavy-loader *ngIf="condition" [logs]="logs"></heavy-loader> </div> <h4>heavy-loader log:</h4> <div *ngFor="let message of logs">{{message}}</div> <hr> <p> Hip! </p> <template> <p> Hip! </p> </template> <p> Hooray! </p> <hr> <!-- Examples (A) and (B) are the same --> <!-- (A) *ngIf paragraph --> <p *ngIf="condition"> Our heroes are true! </p> <!-- (B) [ngIf] with template --> <template [ngIf]="condition"> <p> Our heroes are true! </p> </template> <hr> <!-- Examples (A) and (B) are the same --> <!-- (A) *ngFor div --> <div *ngFor="let hero of heroes">{{ hero }}</div> <!-- (B) ngFor with template --> <template ngFor let-hero [ngForOf]="heroes"> <div>{{ hero }}</div> </template>

We learned that we can manipulate our HTML layout with structural directives like ngFor and ngIf and we wrote our own structural directive, myUnless, to do something similar.

我们学会了通过像ngForngIf这样的结构型指令来操纵HTML的布局。我们还写出了我们的第一个结构型指令myUnless来做类似的事情。

Angular offers more sophisticated techniques for managing layout such as structural components that can take external content and incorporate that content within their own templates. Tab and tab pane controls are good examples.

Angular提供了更多成熟的技术来管理布局,比如结构性组件可以接受外部内容,并把这些内容合并到组件自己的模板中。 多页标签及其面板控件就是很好的例子。

We'll learn about structural components in a future chapter.

我们将在未来的章节中还会讲述结构型指令。