Angular学习笔记(五)自定义指令详解及应用
指令概览
在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框架,例如Swing、ExtJS、React等。虽然它们的基类都和Angular一样,都是从组件开始的,但却没有指令的概念。
那么问题来了:为什么 Angular 里面一定要引入“指令”这个概念呢?
根本原因是:我们需要用指令来增强标签的功能,包括HTML原生标签和你自己自定义的标签。
举例来说,<div>是一个常用的原生HTML标签,但是请不要小看它,它上面实际上有非常多的属性,这些属性都是 W3C 规范规定好的:
还能支持以下事件属性:
但是,这些内置属性还不够用,你想给原生的HTML标签再扩展一些属性。比方说:你想给 <div>标签增加一个自定义的属性叫做my-high-light,当鼠标进入 div 内部时,div 的背景就会高亮显示,可以这样使用: <div my-high-light>。
这时候,没有指令机制就无法实现了。
属性型指令
基本用法
首先,命令行输入ng g d attribute-directives/highlight,会在项目的attribute-directives目录下生成如下两个文件,并自动更新app.module.ts 文件。


这是最基本的用法,在指令类中声明拥有该属性指令的元素的背景色:
1 | // highlight.directive.ts |
在标签中添加这个属性:
1 | <p testHightlight>我的背景色是yellow</p> |
在上述代码中,ElementRef是该DOM元素, nativeElement可以访问原生的方法。于是这个p标签的背景色就成了yellow。
但这显然不满足我们的需求。想让鼠标移入才高亮,我们需要引入 HostListener:
1 | import { Directive, ElementRef, HostListener, Input } from '@angular/core'; |
然后使用它:
1 | @HostListener('mouseenter') onMouseEnter() { |
改造
在实际使用中,我们应该让指令的使用者可以在模板中通过绑定来设置颜色,就像这样:
1 | <p [testHighlight]="yellow">模板中定义颜色</p> |
我们需要把testHighlight的值传入该指令:
1 | // 获取属性值 |
然后修改onMouseEnter方法:
1 | @HostListener('mouseenter') onMouseEnter() { |
这样就大功告成了。
绑定到第二个属性
目前,默认颜色(它在用户选取了高亮颜色之前一直有效)被硬编码为红色。我们还可以让模板的开发者也可以设置默认颜色。
把第二个名叫defaultColor的输入属性添加到HighlightDirective中:
1 | @Input('testHighlight') |
然后修改onMouseEnter方法:
1 | @HostListener('mouseenter') onMouseEnter() { |
在模板里,我们定义defaultColor的值,来设定初始颜色
1 | <p [testHighlight]="color" defaultColor="#d87093">模板中定义defaultColor</p> |
Angular之所以知道defaultColor绑定属于HighlightDirective,是因为我们已经通过@Input装饰器把它设置成了公共属性。
最后的效果图:
结构性指令
*ngIf剖析
ngIf是一个很好的结构型指令案例:它接受一个布尔值,并据此让一整块DOM树出现或消失。
1 | <p *ngIf="true"> |

当条件为假时,NgIf会从DOM中移除它的宿主元素,取消它监听过的那些DOM事件,从Angular变更检测中移除该组件,并销毁它。 这些组件和DOM节点可以被当做垃圾收集起来,并且释放它们占用的内存。
星号“*”前缀
星号是一个用来简化更复杂语法的“语法糖”。 从内部实现来说,Angular把*ngIf 属性 翻译成一个<ng-template> 元素 并用它来包裹宿主元素,代码如下:
1 | <div *ngIf="hero" >原模板</div> |
<ng-template>是一个 Angular 元素,用来渲染HTML。 它永远不会直接显示出来。 在渲染视图之前,Angular 会把<ng-template>及其内容替换为一个注释。
如果没有使用结构型指令,而仅仅把一些别的元素包装进<ng-template>中,那些元素就是不可见的。
仿*ngIf的实现
以下将实现一个叫UnlessDirective的结构型指令,它是NgIf的反义词。 NgIf在条件为true的时候显示模板内容,而UnlessDirective则会在条件为false时显示模板内容。
首先,命令行ng g d structural-directives/unless生成指令文件;然后编写指令内容:
1 | import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; |
指令会从 Angular 生成的<ng-template>元素中创建一个内嵌的视图,并把这个视图插入到一个视图容器中,紧挨着本指令原来的宿主元素。
我们使用TemplateRef取得<ng-template>的内容,并通过ViewContainerRef来访问这个视图容器。
我们还须为它定义一个设置器(setter),见代码第11行。
- 如果条件为假,并且以前尚未创建过该视图,就告诉视图容器(ViewContainer)根据模板创建一个内嵌视图。
- 如果条件为真,并且视图已经显示出来了,就会清除该容器,并销毁该视图。
在模板中使用该指令:
1 | <div> |
效果如图:
仿延迟加载的实现
这个例子会动态创建3个div,每个延迟2秒。
指令代码如下:
1 | import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; |
模板需这样引用:
1 | <div *ngFor="let item of [1,2,3]"> |
解析:第一个div延迟2s创建,第二个4s,第三个6s。效果演示:
利用指令实现表单校验器
指令的另一种常见用法是表单校验器。接下来我们将简单实现一个手机号码的校验器。
实现校验器
在实现表单校验时,需要引入一个服务:
1 | import { Validator, AbstractControl, NG_VALIDATORS } from "@angular/forms"; |
如上,其中provide: NG_VALIDATORS这个东西是固定的:指令把自己注册到了NG_VALIDATORS提供商中,该提供商拥有一组可扩展的验证器。
useExisting的值是下面的类名,这个类实现了一个Validator接口,我们把校验方法写进这个接口。
multi可以设置多值。
校验器全部代码如下:
1 | import { Directive, Input } from '@angular/core'; |
使用校验器
模板中这样使用:
1 | <label>手机号:</label> |
每当输入框的值发生改变,都会触发这个mobile校验器。当校验失败时,下方会给出错误信息;校验成功则会清除信息。
注意:别忘了在app.module.ts文件中引入FormsModule模块,放入imports列表中

