文章目录
  1. 1. 指令概览
  2. 2. 指令在Angular中的意义
  3. 3. 属性型指令
    1. 3.1. 基本用法
    2. 3.2. 改造
    3. 3.3. 绑定到第二个属性
  4. 4. 结构性指令
    1. 4.1. *ngIf剖析
    2. 4.2. 星号“*”前缀
    3. 4.3. 仿*ngIf的实现
    4. 4.4. 仿延迟加载的实现
  5. 5. 利用指令实现表单校验器
    1. 5.1. 实现校验器
    2. 5.2. 使用校验器

指令概览

注:点此获取本章所有源码

在Angular中有三种类型的指令

  • 组件 — 拥有模板的指令
  • 结构型指令 — 通过添加和移除 DOM 元素改变 DOM 布局的指令
  • 属性型指令 — 改变元素、组件或其它指令的外观和行为的指令。

组件(Component)是指令(Directive) 的子接口,是一种特殊的指令,组件可以带有 HTML 模板,指令 不能有模板。组件是这三种指令中最常用的,在本系列之前的文章有详细讲过,再次不再赘述。

属性型指令可以改变一个元素的外观或行为,但是不会改变 DOM 结构,Angular 内置指令里面典型的属性型指令有 ngClass、ngStyle。

结构型指令:可以修改 DOM 结构,内置的常用结构型指令有 ngFor、ngIf 和 NgSwitch。由于结构型指令会修改 DOM 结构,所以同一个 HTML 标签上面不能同时使用多个结构型指令,否则大家都来改DOM结构,到底听谁的呢?

如果要在同一个 HTML元素上面使用多个结构性指令,可以考虑加一层空的元素来嵌套,比如在外面套一层空的<ng-container></ng-container>,或者套一层空的 <div>

指令在Angular中的意义

市面上有很多UI框架,例如SwingExtJSReact等。虽然它们的基类都和Angular一样,都是从组件开始的,但却没有指令的概念。

那么问题来了:为什么 Angular 里面一定要引入“指令”这个概念呢?

根本原因是:我们需要用指令来增强标签的功能,包括HTML原生标签和你自己自定义的标签。

举例来说,<div>是一个常用的原生HTML标签,但是请不要小看它,它上面实际上有非常多的属性,这些属性都是 W3C 规范规定好的:
div标签自带属性

还能支持以下事件属性:
div标签支持的时间属性

但是,这些内置属性还不够用,你想给原生的HTML标签再扩展一些属性。比方说:你想给 <div>标签增加一个自定义的属性叫做my-high-light,当鼠标进入 div 内部时,div 的背景就会高亮显示,可以这样使用: <div my-high-light>

这时候,没有指令机制就无法实现了。

属性型指令

基本用法

首先,命令行输入ng g d attribute-directives/highlight,会在项目的attribute-directives目录下生成如下两个文件,并自动更新app.module.ts 文件。
生成的文件
指令文件的基本内容
app.module.ts文件更新的内容

这是最基本的用法,在指令类中声明拥有该属性指令的元素的背景色:

1
2
3
4
5
6
7
8
9
// highlight.directive.ts
import { Directive, ElementRef, Input } from '@angular/core';

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

在标签中添加这个属性:

1
<p testHightlight>我的背景色是yellow</p>

在上述代码中,ElementRef是该DOM元素, nativeElement可以访问原生的方法。于是这个p标签的背景色就成了yellow。

但这显然不满足我们的需求。想让鼠标移入才高亮,我们需要引入 HostListener

1
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

然后使用它:

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

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

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

改造

在实际使用中,我们应该让指令的使用者可以在模板中通过绑定来设置颜色,就像这样:

1
<p [testHighlight]="yellow">模板中定义颜色</p>

我们需要把testHighlight的值传入该指令:

1
2
3
// 获取属性值
@Input('testHighlight')
highlightColor: string;

然后修改onMouseEnter方法:

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

这样就大功告成了。

绑定到第二个属性

目前,默认颜色(它在用户选取了高亮颜色之前一直有效)被硬编码为红色。我们还可以让模板的开发者也可以设置默认颜色。

把第二个名叫defaultColor的输入属性添加到HighlightDirective中:

1
2
3
4
5
@Input('testHighlight')
highlightColor: string;
// 获取第二个属性
@Input()
defaultColor: string;

然后修改onMouseEnter方法:

1
2
3
4
@HostListener('mouseenter') onMouseEnter() {
// 默认颜色优先级:组件设置的颜色 > 模板定义的默认颜色 > 红色
this.highlight(this.highlightColor || this.defaultColor || 'red');
}

在模板里,我们定义defaultColor的值,来设定初始颜色

1
<p [testHighlight]="color" defaultColor="#d87093">模板中定义defaultColor</p>

Angular之所以知道defaultColor绑定属于HighlightDirective,是因为我们已经通过@Input装饰器把它设置成了公共属性。

最后的效果图:
三个选项为测试程序

结构性指令

*ngIf剖析

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

1
2
3
4
5
6
<p *ngIf="true">
表达式为真,此段落在DOM中
</p>
<p *ngIf="false">
表达式为false,此段落不在DOM中
</p>

当条件为假时,NgIf会从DOM中移除它的宿主元素,取消它监听过的那些DOM事件,从Angular变更检测中移除该组件,并销毁它。 这些组件和DOM节点可以被当做垃圾收集起来,并且释放它们占用的内存。

星号“*”前缀

星号是一个用来简化更复杂语法的“语法糖”。 从内部实现来说,Angular把*ngIf 属性 翻译成一个<ng-template> 元素 并用它来包裹宿主元素,代码如下:

1
2
3
4
5
<div *ngIf="hero" >原模板</div>

<ng-template [ngIf]="hero">
<div>实际解析</div>
</ng-template>

<ng-template>是一个 Angular 元素,用来渲染HTML。 它永远不会直接显示出来。 在渲染视图之前,Angular 会把<ng-template>及其内容替换为一个注释。

如果没有使用结构型指令,而仅仅把一些别的元素包装进<ng-template>中,那些元素就是不可见的。

仿*ngIf的实现

以下将实现一个叫UnlessDirective的结构型指令,它是NgIf的反义词。 NgIf在条件为true的时候显示模板内容,而UnlessDirective则会在条件为false时显示模板内容。

首先,命令行ng g d structural-directives/unless生成指令文件;然后编写指令内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

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

private hasView = false;

@Input()
set testUnless(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;
}
}

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

}

指令会从 Angular 生成的<ng-template>元素中创建一个内嵌的视图,并把这个视图插入到一个视图容器中,紧挨着本指令原来的宿主元素。

我们使用TemplateRef取得<ng-template>的内容,并通过ViewContainerRef来访问这个视图容器。

我们还须为它定义一个设置器(setter),见代码第11行。

  • 如果条件为假,并且以前尚未创建过该视图,就告诉视图容器(ViewContainer)根据模板创建一个内嵌视图。
  • 如果条件为真,并且视图已经显示出来了,就会清除该容器,并销毁该视图。

在模板中使用该指令:

1
2
3
4
5
6
7
<div>
<button (click)="condition=!condition">
设置condition为{{condition ? 'false' : 'true'}}
</button>
</div>
<p *testUnless="condition">A段落</p>
<p *testUnless="!condition">B段落</p>

效果如图:

仿延迟加载的实现

这个例子会动态创建3个div,每个延迟2秒。

指令代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
selector: '[testDelay]'
})
export class DelayDirective {

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

@Input()
set testDelay(time: number) {
setTimeout(() => {
// 延时创建元素
this.viewContainerRef.createEmbeddedView(this.templateRef);
}, time);
}

}

模板需这样引用:

1
2
3
4
5
<div *ngFor="let item of [1,2,3]">
<div *testDelay="2000 * item" class="card">
第{{item}}张卡片
</div>
</div>

解析:第一个div延迟2s创建,第二个4s,第三个6s。效果演示:

利用指令实现表单校验器

指令的另一种常见用法是表单校验器。接下来我们将简单实现一个手机号码的校验器。

实现校验器

在实现表单校验时,需要引入一个服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Validator, AbstractControl, NG_VALIDATORS } from "@angular/forms";
@Directive({
selector: '[mobile]',
providers: [
{
provide: NG_VALIDATORS,
useExisting: ChineseMobileValidatorDirective,
multi: true
}
]
})
export class ChineseMobileValidatorDirective implements Validator {
// ...
}

如上,其中provide: NG_VALIDATORS这个东西是固定的:指令把自己注册到了NG_VALIDATORS提供商中,该提供商拥有一组可扩展的验证器。

useExisting的值是下面的类名,这个类实现了一个Validator接口,我们把校验方法写进这个接口。

multi可以设置多值。

校验器全部代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { Directive, Input } from '@angular/core';
import { Validator, AbstractControl, NG_VALIDATORS } from "@angular/forms";

@Directive({
selector: '[mobile]',
providers: [
{
provide: NG_VALIDATORS,
useExisting: ChineseMobileValidatorDirective,
multi: true
}
]
})
export class ChineseMobileValidatorDirective implements Validator {
@Input() ChineseMobileValidator: string;

constructor() { }

validate(control: AbstractControl): { [error: string]: any } {
let val = control.value;// 获取输入框的值
let flag=/^1(3|4|5|7|8)\d{9}$/.test(val);
console.log(flag);
if(flag){
// 清空错误信息
control.setErrors(null);
return null
}else{
// 设置错误信息
control.setErrors({mobileValidator:false});
return {mobileValidator:false};
}
}
}

使用校验器

模板中这样使用:

1
2
3
4
5
6
7
8
9
<label>手机号:</label>
<input #mobile="ngModel" ngModel mobile placeholder="Mobile">
<!-- 检测此项表单是否未通过校验且已被触碰或修改过 -->
<div *ngIf="mobile.invalid && (mobile.dirty || mobile.touched)">
<!-- 检测mobile的错误列表是否有mobileValidator -->
<div *ngIf="!mobile.errors.mobileValidator">
请输入合法的手机号
</div>
</div>

每当输入框的值发生改变,都会触发这个mobile校验器。当校验失败时,下方会给出错误信息;校验成功则会清除信息。

注意:别忘了在app.module.ts文件中引入FormsModule模块,放入imports列表中

文章目录
  1. 1. 指令概览
  2. 2. 指令在Angular中的意义
  3. 3. 属性型指令
    1. 3.1. 基本用法
    2. 3.2. 改造
    3. 3.3. 绑定到第二个属性
  4. 4. 结构性指令
    1. 4.1. *ngIf剖析
    2. 4.2. 星号“*”前缀
    3. 4.3. 仿*ngIf的实现
    4. 4.4. 仿延迟加载的实现
  5. 5. 利用指令实现表单校验器
    1. 5.1. 实现校验器
    2. 5.2. 使用校验器