创建自定义AngularJS指令系列
在本系列的第一篇文章我介绍了AngularJS自定义指令并展示了几个简单的例子。在这篇文章中,我们要探讨孤立作用域的话题,看看它在构建指令时的重要性。
什么是孤立作用域?
在AngularJS应用中,默认情况下指令会继承父作用域。例如:下边指令继承父作用域的customer对象的name属性和street属性:
angular.module('directivesModule').directive('mySharedScope', function () {
return {
template: 'Name: {{customer.name}} Street: {{customer.street}}'
};
});
这种继承的方式虽然可以达到我们的目的,但为了使用这个指令,必须知道父作用域的许多信息。我们可以很容易地使用
ng-include和HTML模板完成同样的功能(这个在第一章中讨论过)。如果父作用域发生了改变,这个指令将不能使用。
如果你想创建一个可复用的指令,你必须使用孤立作用域而不是继承父作用域。下边是一个对比继承作用域和孤立作用域的图表:
从这个图表可以看出来,继承作用域方式下,指令可以使用父作用域中的数据。而孤立作用域不能继承父作用中的数据。 对于孤立作用域,就好比创建了一道坚实的墙壁,父作用域无法穿透这道墙壁去影响指令。下边的图片是对这个概念的一个比喻:
在指令中创建孤立作用域
在指令中生成孤立作用域的方法很简单。只需要在指令中添加一个scope属性就可以了,如下边所示。它会自动的将指令地隔离指令和父作用域。
angular.module('directivesModule').directive('myIsolatedScope', function () {
return {
scope: {},
template: 'Name: {{customer.name}} Street: {{customer.street}}'
};
});
由于指令作用域孤立的,因此,父作用域的customer对象在指令中将不可用。这个指令在视图中显示结果如下(customer对象的name和street属性值没有被渲染出来):
Name: Street:
由于指令使用孤立作用域与父作用域完全隔离开来,那么,怎样将数据传入指令中实现数据绑定的目的呢?你可以使用@,=和&符号实现这些功能,这些符号乍一看比较奇怪,但当你真正明白它们代表的含义,你就不会感觉太糟糕。让我们看下这些符号在指令中是怎样使用的。
指令内部scope的属性介绍
使用孤立作用域的指令,提供了三种不同的与外部互动的方式。这三种方式被称为指令内部Scope属性,可以使用前边提到的@,=和&符号来定义。这里介绍是它们是怎样工作的。
指令"@"类型的Scope属性
@类型的Scope属性作用是接收从外部传入的字符串变量。例如,外部的控制器在它的$scope对象中定义了一个name属性,你需要将这个属性传入具有孤立作用域的指令中。你可以通过使用@符号在指令的scope属性中实现这个功能。下边是一个一步一步解释这个概念的高水平的例子:
- 一个外部控制器定义了$scope.name。
- 这个$scope.name属性需要传入指令中。
- 指令创建了一个内部scope对象用于它的孤立作用域,其中有name属性(注意:这个属性名可以自定义,并不需要跟外部$scope对象的name属性名一样。只要保持标签上的属性名字和指令内部scope对象内的属性名一致即可)。这里使用了一致的名字scope { name: ‘@’ }。
- @字符告诉指令,将要传给name属性的值是一个字符串。如果改变外部作用域的name属性的值,指令的孤立作用域中也会自动同步改变name属性的值。
- 这个模板现在绑定了孤立作用域的name属性。
Here’s an example of putting all of the steps together. Assume that the following controller is defined within an app:
下边是一个包含所有步骤的示例。假定下列控制器定义在一个应用中:
var app = angular.module('directivesModule', []);
app.controller('CustomersController', ['$scope', function ($scope) {
var counter = 0;
$scope.customer = {
name: 'David',
street: '1234 Anywhere St.'
};
$scope.customers = [
{
name: 'David',
street: '1234 Anywhere St.'
},
{
name: 'Tina',
street: '1800 Crest St.'
},
{
name: 'Michelle',
street: '890 Main St.'
}
];
$scope.addCustomer = function () {
counter++;
$scope.customers.push({
name: 'New Customer' + counter,
street: counter + ' Cedar Point St.'
});
};
$scope.changeData = function () {
counter++;
$scope.customer = {
name: 'James',
street: counter + ' Cedar Point St.'
};
};
}]);
下边的指令代码创建了一个孤立作用域,允许name属性和外部传进来的值进行绑定:
angular.module('directivesModule').directive('myIsolatedScopeWithName', function () {
return {
scope: {
name: '@'
},
template: 'Name: {{ name }}'
};
});
上边定义的指令可以像下边这样使用:
<div my-isolated-scope-with-name name="{{ customer.name }}"></div>
请注意$scope.customer.name的值是怎样绑定到指令孤立作用域中的name属性上的。上边代码在浏览器中渲染出来的效果如下:
Name: David
正如上边提到的,如果$scope.customer.name的值发生变化,指令将自动检测到这种变化,同时改变内部name属性的值,并且将结果渲染到页面中。然而,如果指令内部改变了name属性 的值,外部的$scope.customer.name却不能检测到这种变化,因此不会发生变化。这种绑定方式可以称作”单向绑定“。如果你要保持指令内外的数据同步变化(双向绑定),可以使用下边将要讲到的"=类型的Scope属性".
如果你要使用一个和你外部作用域属性名不同的标签属性名传入指令内部属性名,你可以使用下边语法方式:
angular.module('directivesModule').directive('myIsolatedScopeWithName', function () {
return {
scope: {
name: '@someOtherName'
},
template: 'Name: {{ name }}'
};
});
指令"="类型的Scope属性
在上边的例子中,当要给指令传入一个字符串类型的变量时,使用@符号可以很好的实现这个功能。然而,当指令内部变量发生改变时,外部作用域中对应变量不会同步改变。因此,当你要创建一个外部作用域和指令的孤立作用域双向绑定的指令时,可以使用=符号。下边是一个一步一步阐述这个概念的示例:
- 一个控制器定义了一个$scope.person对象。
- 这个$scope.person对象需要以双向绑定的方式传入指令。
- 指令创建一个名字叫customer的内部作用域属性在它的孤立作用域中。这里是scope { customer: ‘=’ }。
- 这个=符号告诉指令:这个被传入customer属性的外部对象应该使用双向绑定的方式。如果外部对象值发生改变,指令的customer属性也自动更新;如果指令的customer属性发生改变,外部作用域的对象值也会同步更新。
- 这个指令中的模板现在可以使用绑定到孤立作用域的customer 属性。
下边示例是一个在孤立作用域中使用=符号定义属性的指令:
angular.module('directivesModule').directive('myIsolatedScopeWithModel', function () {
return {
scope: {
customer: '=' //Two-way data binding
},
template: '<ul><li ng-repeat="prop in customer">{{ prop }}</li></ul>'
};
});
在这个示例中,指令获取到 customer属性中的对象,并使用 ng-repeat指令遍历这个对象中的所有属性,然后将属性值使用 <li>元素展示出来。
给指令传入数据通过下边代码方式:
<div my-isolated-scope-with-model customer="customer"></div>
注意一点:在使用=符号方式给指令传入数据时,你传入的是一个对象,而不是对象的属性值(指令实际上只是需要这个属性值),这样做是比较规范的方式,建议读者都是用这种方式,以避免有些特殊问题。
这个例子中,外部控制器的customer对象,传入指令内部作用域的customer属性。指令使用ng-repeat遍历customer的所有属性,并将其值全部显示出来。下边是这个指令运行后在页面中显示的内容:
- David
- 1234 Anywhere St.
指令"&"类型的Scope属性
到目前为止,你已经明白怎样给指令传入字符串变量(使用@符号),也明白了双向数据绑定一个外部变量的技术(使用=符号)。最后,我们学习怎样使用&符号绑定一个外部函数到指令中。
&类型的scope属性允许自定义指令接收一个可使用的外部函数。例如,假设你写了一个指令,在指令中的template模板中,有一个button按钮,当点击这个按钮时,指令外部的控制器需要知道这个操作,并且做出相应的处理。你不能在指令中编写click的回调函数,因为外部控制器不能监听到发生了什么事。使用一个事件可以完成这个功能(使用$emit or $broadcast),但控制器不知道这个事件名,因此也无法监听。这种做法也并不是最优的。
一种比较好的做法是给自定义指令传入一个函数,当指令需要时调用这个函数。当指令需要使用这个被传入的函数时(例如:监听到有人点击了这个按钮),就可以直接调用它。这种方式,自定义指令对发生的事件有百分之百的控制权,并授权从外传入的函数控制权。下边是一个一步一步阐述这个概念的例子:
- 外部控制器定义了一个$scope.click函数。
- $scope.click函数需要传入指令中,以便按钮被点击时,指令能够调用它。
- 指令在它的孤立作用域中创建了一个内部的action属性。在这里是scope { action: ‘&’ }。在这个例子中,action仅仅是click的一个别名。当action被使用时,被传入的click函数将被调用。
- &字符告诉外部作用域:”嗨,传给我一个函数,以便当指令内部发生某事时,我可以调用它“。
- 指令内部的模板包含一个按钮,当点击按钮时,action函数(实际是外部传入的函数)将被调用。
下边是一个&型scope属性的使用例子:
angular.module('directivesModule').directive('myIsolatedScopeWithModelAndFunction', function () {
return {
scope: {
datasource: '=',
action: '&'
},
template: '<ul><li ng-repeat="prop in datasource">{{ prop }}</li></ul> ' +
'<button ng-click="action()">Change Data</button>'
};
});
可也看到,指令模板代码中有下边的DOM,模板使用了action属性,并在按钮被点击时调用它。无论外部传入了什么函数给action,都将执行它。
<button ng-click="action()">Change Data</button>
Here’s an example of using the directive. I’d of course recommend picking a shorter name for your “real life” directives.
下边是一个使用这个指令的例子。当然,我推荐你自己写指令时,定义一个比较短的指令名。
<div my-isolated-scope-with-model-and-function
datasource="customer"
action="changeData()">
</div>
changeData()函数被定义在这篇博文开始部分的控制器中,它会被传入指令的action属性中。它的作用是改变name和address:
$scope.changeData = function () {
counter++;
$scope.customer = {
name: 'James',
street: counter + ' Cedar Point St.'
};
};
总结
到目前为止,你已经了解到AngularJS指令中一些关键并且有用的方面,例如:模板、孤立作用域、内部scope属性。
作为回顾,孤立作用域在指令中使用scope属性创建,scope是一个对象类型的属性。三种类型的scope属性可以被添加到孤立作用域上:
- @ 用作给指令传入字符串变量
- = 用于给传入指令的对象创建双向绑定功能
- & 用于给指令传入一个外部函数,并可以在指令中调用