AngularJS自定义指令(2):isolate指令域和parent域绑定

上一部分讨论到了angular的指令可以创建一个isolate的域,现在就继续这个讨论。

Binding between isolated and parent scope properties

很多时候,将一个指令的scope进行isolate是很有好处的,尤其是你如果想要操作scope里的很多属性时。但是大部分时候,你还是需要从父级scope里获取一些属性值的。好消息是angular提供了这样的方法,可以把指令isolate的域和父级scope联通起来。第一部分最后的那个例子,如下:

app.directive('helloWorld', function() {
return {
scope: {},
restrict: 'AE',
replace: true,
template: '<p style="background-color:{{color}}">Hello World</p>',
link: function(scope, elem, attrs) {
elem.bind('click', function() {
elem.css('background-color','white');
scope.$apply(function() {
scope.color = "white";
});
});
elem.bind('mouseover', function() {
elem.css('cursor', 'pointer');
});
}
};
});

通过scope: {}的设置,指令创建了一个isolate的scope。

<body ng-controller="MainCtrl">
<input type="text" ng-model="color" placeholder="Enter a color"/>
<hello-world/>
</body>

但是hin遗憾的是,预期中的功能不能work了。原因是template里面的{{color}}是指令域的color了,而不是parent域的color。

AngularJS框架提供的方法是: 在指令元素上添加一些属性(attribute),然后构建指令的scope对象,把数据联通起来。这个过程中需要使用prefix标识符, angularjs一共提供了三种标识符:

Option 1: Use @ for one way text binding

app.directive('helloWorld', function() {
return {
scope: {
color: '@colorAttr'
},
....
// the rest of the configurations
};
});

<body ng-controller="MainCtrl">
<input type="text" ng-model="color" placeholder="Enter a color"/>
<hello-world color-attr="{{color}}"/>
</body>

在上面这个指令的定义中,color: ‘@colorAttr’, 这设置的意义是,在指令的isolate域内创建一个color属性,并把它绑定到属性colorAttr上,而这个属性被声明到了指令元素上。此处的关键地方是,colorAttr是通过{{color}}的方式进行赋值的,这是@ prefix的特殊要求,必须通过{{}}这种angular的表达式来进行赋值。这样当表达式{{color}}的值发生了变化的时候,colorAttr也变化了,最终导致指令的isolate域中的属性color也跟着发生变化。

这种方法叫做单向的绑定,当父级scope中的属性值发生变化的时候,指令的isolate域内相应的属性值也会跟着变话,你甚至可以监听这个变化来触发一些行为,这都可以。但是反方向是行不通的,在isolate的指令域内对属性进行操作,不会传导到父级里。

另外,如果指令标签上的attribute名和isolated域的property名字一样的时候,可以省略@ prefix后面的部分,框架会默认去找同名的attribute, 比如下面这个样子:

app.directive('helloWorld', function() {
return {
scope: {
color: '@'
},
....
// the rest of the configurations
};
});

<hello-world color="{{color}}"/>

Option 2: Use = for two way binding

把代码改成下面这个样子:

app.directive('helloWorld', function() {
return {
scope: {
color: '='
},
....
// the rest of the configurations
};
});

<body ng-controller="MainCtrl">
<input type="text" ng-model="color" placeholder="Enter a color"/>
<hello-world color="color"/>
</body>

跟第一种方式比,有两个变化,首先使用的prefix变成了”=”,另外指令标签上的属性声明,变成了color=”color”。这种设置还会构成双向绑定。

Option 3: Use & to execute functions in the parent scope

一些时候我们希望在isolated的指令域中调用parent里的函数,这种场合应该使用prefix &. 比如俺们想在指令中调用parent controller里的sayHello(),可以把指令的定义改成下面这样:

app.directive('sayHello', function() {
return {
scope: {
sayHelloIsolated: '&;'
},
....
// the rest of the configurations
};
});

<body ng-controller="MainCtrl">
<input type="text" ng-model="color" placeholder="Enter a color"/>
<say-hello sayHelloIsolated="sayHello()"/>
</body>

另外这里有一个很好的demo case, http://plnkr.co/edit/k4scWKwtGBJw7lfKGqVJ?p=preview

AngularJS自定义指令(1):link, compile和scope

是时候好好研究下directive了。directive对于angularjs来说,可以说是最重要的组件了。而其中有难度的部分就是自定义指令了。

Building Custom Directives

在angular里面注册directive的方法,跟注册controller是一样的,不同的是directive会返回一个对象,俗称DDO(directive definition object),定义一个指令就是要构建DDO的各个属性,比如最简单的下面这个例子:

var app = angular.module('myapp', []);
app.directive('helloWorld', function() {
return {
restrict: 'AE',
replace: 'true',
template: '<h3>Hello World!!</h3>'
};
});

上这个例子中DDO里出现了三个属性:

  • restrict:限定指令以什么形式出现在html的标签里,一共有四种模式。A和E分别代表attribute和element
  • replace:  定义template是否要替代,指令元素
  • template: 定义html代码,当这个指令被compile和link之后,返回的相应元素。

Link function and Scope

directive提供的模板只有在正确的scope中进行编译才能有意义。默认情况下,一个directive不会生成新的子scope,而是继承它的parent scope,也就是说,如果指令出现在某个controller之内,就会使用这个controller的scope。

为了使用这个scope,俺们需要利用指令系统的link函数,具体的方法是为指令的定义对象添加link属性。

举了例子,这里修改下俺们的helloworld指令,实现这样一个效果:从一个文本框中获取用户的输入的某种颜色,并将Hello World的背景改成相应的颜色;另外,当用户点击Hello World的文字时,将背景色重置为白色。这个例子对应的HTML代码如下:

<body ng-controller="MainCtrl">
<input type="text" ng-model="color" placeholder="Enter a color" />
<hello-world/>
</body>

为了实现这个需求,把helloWorld指令修改如下:

app.directive('helloWorld', function() {
return {
restrict: 'AE',
replace: true,
template: '<p style="background-color:{{color}}">Hello World',
link: function(scope, elem, attrs) {
elem.bind('click', function() {
elem.css('background-color', 'white');
scope.$apply(function() {
scope.color = "white";
});
});
elem.bind('mouseover', function() {
elem.css('cursor', 'pointer');
});
}
};
});

注意到link函数一共有三个参数:

  • Scope: 传入到指令的scope,此处跟parent的controller相同
  • Elem: 被jQLite(是jQuery的一个子集)包裹的元素,也是指令声明所在的元素。如果你在应用中安装了jQuery,此处就是jQuery包裹的。另外这个元素已经是被jQLite或者jquery包裹的元素了,所以不需要再用$()进行DOM操作了
  • Attrs: 一个属性对象,包含了指令元素上的所有属性,比如你可以给html指令元素添加一些属性<hello-world some-attribute></hello-world> ,然后在link函数里用attrs.someAttribute来获取。

一般来说,link函数主要用来给DOM元素添加事件,监听数据的变化,对DOM进行操作。

Compile函数

compile函数用会在link函数运行之前,对DOM元素进行transformation的处理,它可以接受下面两个参数:

  • tElement:  施加指令的元素
  • attrs: 很显然是指令元素的属性对象

需要注意的是,和link函数不同,compile函数不能获取scope内容的权限,另外,complie函数一定要返回一个link函数。当然compile函数不是必须的,没有它你可以直接构建link函数。下面是compile函数的一个基本逻辑:

app.directive('test', function() {
return {
compile: function(tElem,attrs) {
//do optional DOM transformation here
return function(scope,elem,attrs) {
//linking function here
};
}
};
});

大部分时候,只需要link函数就够了,因为指令系统更多是用来支持事件,监听器,更新DOM元素等的,而这些都是在link函数中完成的。

只有某些指令,比如ng-repeat,可能需要在link之前通过compile来将元素repeat多次。那末带来了一个问题,俺们为什么需要两个函数。不能放在一个函数中完成吗?为了解释这个问题,俺们需要理解angular是如何编译指令系统的。

How directives are complied?

当应用被启动之后,angular会用$compile服务处理各个DOM元素。$compile会搜索每一个元素上被声明的指令,然后跟应用中注册的指令进行匹配。当所有的指令都被匹配之后,angular会执行所有指令的compile函数。像上面说的那样,compile函数都会返回一个link函数,并把每个link函数添加到一个列表中,等待稍后执行。到这里为止称为compile阶段。这样做会获得一些性能上的好处,比如ng-repeat这个指令,compile只会执行一次,而link函数会在每个被复制出的元素中执行一次。这也就是compile函数不需要scope的原因。

在compile阶段之后,就是link阶段。之前生成那个list中的每个link函数会一一执行,并生成最终的具有响应事件的DOM元素。

Change a directive’s scope

默认自定义的指令会获取它父级的scope,但是这种默认的行为也是有风险的,比如某个指令需要一些属性和方法作为内部使用,如果加到父级scope里面其实相当于污染了parent的环境。

因此除了默认的行为之外,还有两种方法:child scope和 isolated scope

app.directive('helloWorld', function() {
return {
scope: true,  // use a child scope that inherits from parent
restrict: 'AE',
replace: 'true',
template: '<h3>Hello World!!</h3>'
};
});

上面这种方法,scope: true, 创建了一个子scope并继承了父级的内容

app.directive('helloWorld', function() {
return {
scope: {},  // use a new isolated scope
restrict: 'AE',
replace: 'true',
template: '<h3>Hello World!!</h3>'
};
})

上面这个方法,通过声明一个对象(scope:{}), 创建了一个独立的域。 在实际的应用中,创建独立的scope有很多好处,可以保证这个指令是自包含的,而不会依赖父级的东西,这样可以快速的复用某个指令到一个全新的app中。

创建了独立的scope,不代表就不能获取父级的scope数据了,这里还是有一些方法的。。。下回再说。。。