指令

指令是为 Angular 应用程序中的元素添加额外行为的类。使用 Angular 的内置指令,你可以管理表单、列表、样式以及要让用户看到的任何内容。

Angular 指令的不同类型如下:

指令类型 详情
组件 带有模板的指令。这种指令类型是最常见的指令类型。
属性型指令 更改元素、组件或其他指令的外观或行为的指令。
结构型指令 通过添加和删除 DOM 元素来更改 DOM 布局。

内置属性型指令

属性型指令会监听并修改其它 HTML 元素和组件的行为、Attribute 和 Property。

通用指令 详情
NgClass 添加和删除一组 CSS 类。
NgStyle 添加和删除一组 HTML 样式。
NgModel 将双向数据绑定添加到 HTML 表单元素。

内置结构型指令

结构型指令的职责是 HTML 布局。它们塑造或重塑 DOM 的结构,这通常是通过添加、移除和操纵它们所附加到的宿主元素来实现的。

常见的内置结构型指令 详情
NgIf 有条件地从模板创建或销毁子视图。
NgFor 为列表中的每个条目重复渲染一个节点。
NgSwitch 一组在备用视图之间切换的指令。

属性型指令

使用属性型指令,可以更改 DOM 元素和 Angular 组件的外观或行为。

建立属性型指令

  1. 要创建指令,请使用 CLI 命令 ng generate directive

    CLI 创建 src/app/highlight.directive.ts 以及相应的测试文件 src/app/highlight.directive.spec.ts,并在 AppModule 中声明此指令类。
    CLI 生成默认的 src/app/highlight.directive.ts,如下所示:

1
2
3
4
5
6
7
8
9
 import { Directive } from '@angular/core';

@Directive({
selector: '[appHighlight]'
})

export class HighlightDirective {
constructor() { }
}

@Directive() 装饰器的配置属性会指定指令的 CSS 属性选择器 [appHighlight]。

  1. 从 @angular/core 导入 ElementRef。ElementRef 的 nativeElement 属性会提供对宿主 DOM 元素的直接访问权限。

  2. 在指令的 constructor() 中添加 ElementRef 以注入对宿主 DOM 元素的引用,该元素就是 appHighlight 的作用目标。

  3. 向 HighlightDirective 类中添加逻辑,将背景设置为黄色。

1
2
3
4
5
6
7
8
9
10
import { Directive, ElementRef } from '@angular/core';

@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(private el: ElementRef) {
this.el.nativeElement.style.backgroundColor = 'yellow';
}
}

应用属性型指令

  1. 要使用 HighlightDirective,请将

    元素添加到 HTML 模板中,并以伪指令作为属性。

1
<p appHighlight>Highlight me!</p>

Angular 会创建 HighlightDirective 类的实例,并将 <p> 元素的引用注入到该指令的构造函数中,它会将 <p> 元素的背景样式设置为黄色。

处理用户事件

添加两个事件处理程序,它们会在鼠标进入或离开时做出响应,每个事件处理程序都带有 @HostListener() 装饰器。

1
2
3
4
5
6
7
8
9
10
11
@HostListener('mouseenter') onMouseEnter() {
this.highlight('yellow');
}

@HostListener('mouseleave') onMouseLeave() {
this.highlight('');
}

private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}

要订阅本属性型指令宿主 DOM 元素上的事件(在本例中是 <p>),可以使用 @HostListener() 装饰器。

完整的指令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {

constructor(private el: ElementRef) { }

@HostListener('mouseenter') onMouseEnter() {
this.highlight('yellow');
}

@HostListener('mouseleave') onMouseLeave() {
this.highlight('');
}

private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}

}

当指针悬停在 p 元素上时,背景颜色就会出现;而当指针移出时,背景颜色就会消失。

将值传递给属性型指令

本节将引导你在应用 HighlightDirective 时设置突出显示颜色。

  1. 在 highlight.directive.ts 中,从 @angular/core 导入 Input。
1
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
  1. 添加一个 appHighlight 的 @Input() 属性。

    1
    @Input() appHighlight = '';

    @Input() 装饰器会将元数据添加到此类,以便让该指令的 appHighlight 属性可用于绑定。

  2. 在 app.component.ts,将 color 属性添加到 AppComponent。

1
2
3
export class AppComponent {
color = 'yellow';
}

要同时应用指令和颜色,请通过 appHighlight 指令选择器使用属性绑定,将其设置为 color。

1
<p [appHighlight]="color">Highlight me!</p>

[appHighlight] 属性绑定执行两项任务:

将突出显示指令应用于 <p> 元素

通过属性绑定设置指令的突出显示颜色

通过用户输入来设置值
本节指导你添加单选按钮,将你选择的颜色绑定到 appHighlight 指令。

将标记添加到 app.component.html 以选择颜色,如下所示:

1
2
3
4
5
6
7
8
9
<h1>My First Attribute Directive</h1>

<h2>Pick a highlight color</h2>
<div>
<input type="radio" name="colors" (click)="color='lightgreen'">Green
<input type="radio" name="colors" (click)="color='yellow'">Yellow
<input type="radio" name="colors" (click)="color='cyan'">Cyan
</div>
<p [appHighlight]="color">Highlight me!</p>

修改 AppComponent.color,使其没有初始值。

1
2
3
export class AppComponent {
color = '';
}

在 highlight.directive.ts 中,修改 onMouseEnter 方法,让它首先尝试使用 appHighlight 进行高亮显示,如果 appHighlight 是 undefined,则回退为 red。

1
2
3
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.appHighlight || 'red');
}

启动本应用的开发服务器,以验证用户可以通过单选按钮选择颜色。

绑定到第二个属性

本节将指导你配置应用程序,以便开发人员可以设置默认颜色。

  1. 将第二个 Input() 属性 defaultColor 添加到 HighlightDirective。
1
@Input() defaultColor = '';
  1. 修改指令的 onMouseEnter,使其首先尝试使用 appHighlight 进行突出显示,然后尝试 defaultColor,如果两个属性都 undefined,则变回 red。
    1
    2
    3
    @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.appHighlight || this.defaultColor || 'red');
    }
    若要绑定到 AppComponent.color 并回退为默认颜色“紫罗兰(violet)”,请添加以下 HTML。在这里,defaultColor 绑定没有使用方括号 [],因为它是静态的。
    1
    2
    3
    <p [appHighlight]="color" defaultColor="violet">
    Highlight me too!
    </p>
    与组件一样,你可以将指令的多个属性绑定添加到宿主元素上。

如果没有默认颜色(defaultColor)绑定,则默认为红色。当用户选择一种颜色时,所选的颜色将成为突出显示的颜色。

ElementRef

对视图中某个原生元素的包装器。

ElementRef 的背后是一个可渲染的具体元素。在浏览器中,它通常是一个 DOM 元素。

视图

组件 (component) 类及其关联的模板 (template)定义了一个视图。

视图是可显示元素的最小分组单位,它们会被同时创建和销毁。 Angular 在一个或多个指令 (directive) 的控制下渲染视图。

具体实现上,视图由一个与该组件相关的 ViewRef 实例表示。 直属于某个组件的视图叫做宿主视图。 通常会把视图组织成一些视图树(view hierarchies)。

ViewRef

表示一个 Angular 视图

视图树(View hierarchy)

一棵相关视图的树,它们可以作为一个整体行动。其根视图就是组件的宿主视图。宿主视图可以是内嵌视图树的根,它被收集到了宿主组件上的一个视图容器(ViewContainerRef)中。视图树是 Angular 变更检测的关键部件之一。

视图树和组件树并不是一一对应的。那些嵌入到指定视图树上下文中的视图也可能是其它组件的宿主视图。那些组件可能和宿主组件位于同一个 NgModule 中,也可能属于其它 NgModule。

EmbeddedViewRef

表示视图容器中的 Angular 视图。嵌入视图可以从在模板中定义它的宿主组件之外的组件中引用,也可以由 TemplateRef 进行独立定义。

使用说明

以下模板分为两个单独的 TemplateRef 实例,一个外部实例和一个内部实例。

1
2
3
4
Count: {{items.length}}
<ul>
<li *ngFor="let item of items">{{item}}</li>
</ul>

这是外部 TemplateRef :

1
2
3
4
Count: {{items.length}}
<ul>
<ng-template ngFor let-item [ngForOf]="items"></ng-template>
</ul>

这是内部的 TemplateRef :

1
<li>{{item}}</li>

外部和内部 TemplateRef 实例按如下方式组装到视图中:

1
2
3
4
5
6
7
8
<!-- ViewRef: outer-0 -->
Count: 2
<ul>
<ng-template view-container-ref></ng-template>
<!-- ViewRef: inner-1 --><li>first</li><!-- /ViewRef: inner-1 -->
<!-- ViewRef: inner-2 --><li>second</li><!-- /ViewRef: inner-2 -->
</ul>
<!-- /ViewRef: outer-0 -->

TemplateRef

表示一个内嵌模板,它可用于实例化内嵌的视图。 要想根据模板实例化内嵌的视图,请使用 ViewContainerRef 的 createEmbeddedView() 方法。

通过把一个指令放在 <ng-template> 元素(或一个带 * 前缀的指令)上,可以访问 TemplateRef 的实例。 内嵌视图的 TemplateRef 实例会以 TemplateRef 作为令牌,注入到该指令的构造函数中。

你还可以使用 Query 来找出与某个组件或指令相关的 TemplateRef。

ViewContainerRef

表示可以将一个或多个视图附着到组件中的容器。

可以包含宿主视图(当用 createComponent() 方法实例化组件时创建)和内嵌视图(当用 createEmbeddedView() 方法实例化 TemplateRef 时创建)。

视图容器的实例还可以包含其它视图容器,以创建层次化视图。

可以在元素上放置注入了 ViewContainerRef 的 Directive 来访问元素的 ViewContainerRef。也可以使用 ViewChild 进行查询。

ViewChild

属性装饰器,用于配置一个视图查询。 变更检测器会在视图的 DOM 中查找能匹配上该选择器的第一个元素或指令。 如果视图的 DOM 发生了变化,出现了匹配该选择器的新的子节点,该属性就会被更新。

结构型指令

结构型指令简写形式

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

创建结构型指令

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

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

1
<p *appUnless="condition">Show this sentence unless the condition is true.</p>

使用 Angular CLI,运行以下命令,其中 unless 是伪指令的名称:

1
ng generate directive unless

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

导入 Input、TemplateRef 和 ViewContainerRef。

1
2
3
4
5
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
}

在指令的构造函数中将 TemplateRef 和 ViewContainerRef 注入成私有变量。

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

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

TemplateRef可帮助你获取 <ng-template> 的内容,而 ViewContainerRef 可以访问视图容器。

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

1
2
3
4
5
6
7
8
9
@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 会清除容器,这会导致销毁该视图。

完整的指令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}
}
}