属性指令为了改变DOM元素的外观或行为。

一、概述

Angular有三种指令分别为:

  1. 组件
  2. 结构指令
  3. 属性指令

组件实际是一个带有模板的指令,应用程序绝大部分都是使用这种类指令来构建。

结构指令:通过添加或移除来改变DOM布局,NgForNgIf 就是两个常见例子。

属性指令:改变DOM元素的外观或行为,NgStyle 内置指令就是一个例子,它能同一时刻改变元素的CSS样式。

本章只讨论属性指令,接下来我们通过自定义一个属性指令来设置当用户鼠标移动到元素时设置元素高亮示例。

二、创建属性指令

属性指令至少需要一个控制类和 Directive 装饰器,装饰器是需要指定一个CSS选择标识符与指令进行关联(其实这一点所有指令都是一样的,因为对于Angular而言,一个指令想要必须与DOM有一个直接关联关系),而控制类实现指令的行为能力。

小示例

我们依然以官网的快速入门项目为种子项目,在app目录里创建 highlight.directive.ts 文件:

import {Directive, ElementRef, Input} from 'angular2/core';

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

export class HighlightDirective {
    constructor(el: ElementRef) {
        el.nativeElement.style.backgroundColor = 'yellow';
    }
}

首先从加载的模块来说:Directive 对应 @Directive 装饰符;还需要 ElementRef 注入构造函数,为了能访问DOM元素;最后还还一个 Input 用于输入属性后面会用到。

其次 @Directive 这里指定了一个 [myHighlight] CSS选择器,意味者Angular会查找所有带有 myHighlight 属性的元素。

命名为什么不使用”highlight”?

highlight 肯定比 myHighlight 更好,从技术讲两者并没有什么区别。不过Angular建议,加上前缀可以确保不会与HTML标准的属性冲突;而且最好不要使用 ng 开头,因为她是Angular的,小心和Angular内置指令什么的又冲突。

综合上述就加了一个 my

@Directive 装饰器之后,就是一个控件类,用于控制属性指令的行为能力。每次匹配到某个元素时,Angular会重新实例一个新控制类,并把注入一下 ElementRef 对象。

ElementRef 对象目前只有一个 nativeElement (any 类型)属性,光名称上看就是指原生DOM元素对象,我们可以透过他来控制元素。

三、应用属性指令

修改 app.component.ts 文件:

import { Component }          from 'angular2/core';
import {HighlightDirective}   from './highlight.directive';

@Component({
    selector: 'my-app',
    template: `
        <h1>My First Attribute Directive</h1>
        <span myHighlight>Highlight me!</span>
    `,
    directives: [ HighlightDirective ]
})
export class AppComponent { }

首先引入属性指令模块,并且设置 directives 元数据属性,这样才算真正把属性注入到当前模块中。加载模块、注册注入,这一过程就是DI所必须的操作。

在模板中, <span> 有我们创建属性指令时所定的CSS选择标识符,刚才说属性指令与DOM之间惺惺相惜的桥梁就是它。同时当Angular发现时,会创建一个新的 HighlightDirective 实例,并把属性指令所在的元素(也叫属性指令的宿主)作为构建参数传递给控制类。

四、响应用户操作

上面只是改了元素的背景色,如果我们要增加当用户鼠标移至元素上时才改变背景色,那么需要两个步骤:

  1. 检测用户鼠标移入和移出。
  2. 通过设置或清除颜色来响应用户操作。

从事件检测开始,在指令元数据里添加 host 属性,并配置两个事件来响应用户行为:

host: {
    '(mouseenter)': 'onMouseEnter()',
    '(mouseleave)': 'onMouseLeave()'
}

host 属性引用的就是指令所在的DOM元素,本示例就是指 <span>

当然,我们也可以使用JavaScript自行对 ElementRef.nativeElement 绑定事件,只不过需要做更多的事,比如:指令销毁时要清除事件等等。

接着,实现两个鼠标事件处理程序:

onMouseEnter() { this._highlight('yellow'); }
onMouseLeave() { this._highlight(null); }

private _highlight(color: string) {
    this._el.style.backgroundColor = color;
}

注意,这里更改颜色是委托私有方法 _highlight 来完成,而方法内又是使用 _el 属性来表示DOM元素对象,所以还需要在构造函数把 ElementRef.nativeElement 赋给 _el

private _el: HTMLElement;
constructor(el: ElementRef) {
    this._el = el.nativeElement;
}

五、绑定属性配置信息

上面示例我们中颜色是硬编码,显然很不灵活,现在把颜色变成由外部来提供,比如:

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

只需要给指令控制类添加一个可输入属性 highlightColor,这样消费者可以通过属性传递高亮颜色值,最终代码如下:

import {Directive, ElementRef, Input} from 'angular2/core';

@Directive({
    selector: '[myHighlight]',
    host: {
        '(mouseenter)': 'onMouseEnter()',
        '(mouseleave)': 'onMouseLeave()'
    }
})

export class HighlightDirective {

    // 默认颜色
    private _defaultColor = 'red';
    private _el: HTMLElement;

    @Input('myHighlight') highlightColor: string;

    constructor(el: ElementRef) { this._el = el.nativeElement; }

    onMouseEnter() { this._highlight(this.highlightColor || this._defaultColor); }
    onMouseLeave() { this._highlight(null); }

    private _highlight(color: string) {
        this._el.style.backgroundColor = color;
    }

}

highlightColor 属性就是输入属性,因为数据流是从绑定表达式到我们的指令,我们在运用时是这样的 <p [myHighlight]="'yellow'">Highlight me!</p> ,如果你了解Angular2模板语法文章已经描述很清楚。

另一个属性指令应用示例

我们先更新 AppComponent 模板内容:

<h1>My First Attribute Directive</h1>
<h4>Pick a highlight color</h4>
<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 [myHighlight]="color">Highlight me!</p>

重点看 [myHighlight]="color" 和 上面示例 [myHighlight]="'yellow'" 二者是不一样的,前者 color 是一个变量,后者 'yellow' 是一个字符串变量;这是因为对于属性绑定而言右边的内容是一个表达式,所以当想传递一个字符串值时必须用一对单引号包裹着。

绑定多个输入参数

上面示例消费者只有指定高亮颜色,如果我们希望还可以指定默认高亮颜色,怎么做?

首先在 HighlightDirective 新增一个名为 defaultColor 的输入属性:

@Input() set defaultColor(colorName:string){
  this._defaultColor = colorName || this._defaultColor;
}

defaultColor 方法为了覆盖默认颜色,而且是一个只有设置能力,无法获取。

那么如果应用呢?记住组件也是指令,既然可以在一组件里添加多个属性绑定,比如:

<my-component [a]="'a'" [b]="'b'" [c]="'c'"><my-component>

那么照葫芦画瓢:

<p [myHighlight]="color" [defaultColor]="'violet'">
  Highlight me too!
</p>

关于输入参数别名

此时,我们再回过头来看 @Input(alias) 输入属性的别名问题。最初我们使用 @Input('myHighlight') highlightColor: string; 一个叫 myHighlight 的别名来传递高亮颜色值。

那我们可不可以把这个别名去掉呢?答案是可以的,但去掉别名后,如果你依然使用 highlightColor 属性名,那我们应用时,只能这样:

<p myHighlight [highlightColor]="color">
  Highlight me too!
</p>

那么另一个问题,假如我们把 highlightColor 变量名变更为 myHighlight 呢?结果会怎么样?

<p [myHighlight]="color">
  Highlight me too!
</p>

我们又能用起初的调用方式,即 [myHighlight]="color"

然后细心的你可能还会发现,上面示例出现过两种应用属性指令的写法:myHighlight [highlightColor]="color"[myHighlight]="color"

这里重点是没有没 [] 问题,这个问题我们依然需要回到模板语法层面上来讲:

首先:对于Angular在查找属性指令时,是依元素是否带有 myHighlight,这才是命中的标准。

其次:对于 [myHighlight] 它除了会被命中外,还带有数据绑定功能。这一点是因为Angular2绑定系统中,[] 表示属性绑定目标,而且属性绑定数据源的数据流方向是由组件类至元素,而对于我们的自定义属性指令在接收而言数据流方向是由表达式至控制类。

所以其实 [myHighlight] 有二个层面的意思,分别是属性指令选择标识符和数据绑定功能。