创建自定义指令(一)
一、创建简单指令
使用Module.directive方法来创建指令,参数是新指令的名称和一个用于创建指令的工厂函数。
angular.module("exampleApp",[]) .directive("directiveName",function(){ return function(scope,element,attrs){ // do something } })
指令中的worker函数被称为链接函数,它提供了将指令与HTML文档和作用域相连接的方法。(还有另一种可以与指令相关联的函数,称为编译函数)
当AngularJS建立指令的每个实例时,链接函数便被调用并接收三个参数:指令被应用到的视图的作用域,指令被应用到的HTML元素,HTML元素的属性。惯例是使用scope、element、和attrs这些参数来定义链接函数。
与AngularJS控制器不同,指令并不声明对$scope服务的依赖,取而代之的是,传入的是指令被应用到的视图的控制器所创建的作用域。这很重要,因为它允许单个指令在一个应用程序中使用多次,而每个程序可能是在作用域层次结构的不同作用域上工作的。element是个jqLite对象。链接函数的第三个参数attrs是一个按照名字索引的属性集合。当获取属性值时,如果属性名是以data-为前缀的,AngularJS会在生成传给链接函数的属性集合时移除这一前缀。例如,当属性名被规范化并传给链接函数时,属性data-list和list是一个值。
<div unordered-list="products" list-property="price"></div>
.directive("unorderedList", function () { return function (scope, element, attrs) { var data = scope[attrs["unorderedList"]]; var propertyName = attrs["listProperty"]; if (angular.isArray(data)) { var listElem = angular.element("<ul>"); element.append(listElem); for (var i = 0; i < data.length; i++) { listElem.append(angular.element('<li>') .text(data[i][propertyName])); } } } })
如果我修改了HTML如下:
<div unordered-list="products" list-property="price | currency"></div>
这一修改破坏了我的自定义指令,因为我是从属性中读出值并将该值用作要显示在每个生成的li元素中的属性名(此时propertyExpression="price|currency")。这个问题的解决方案是让作用域将属性值当做一个表达式来进行计算,通过scope.$eval方法可以做到这点,传给该方法的是要计算的表达式和需要用于执行该计算的任意本地数据。
.directive("unorderedList", function () { return function (scope, element, attrs) { var data = scope[attrs["unorderedList"]]; var propertyExpression = attrs["listProperty"]; if (angular.isArray(data)) { var listElem = angular.element("<ul>"); element.append(listElem); for (var i = 0; i < data.length; i++) { listElem.append(angular.element('<li>') .text(scope.$eval(propertyExpression, data[i]))); } } } })
二、创建复杂指令
2.1 定义复杂指令
由指令定义对象所定义的属性
compile 指定一个编译函数
controller 为指令创建一个控制器函数
link 为指令指定链接函数
replace 指定模板内容是否替换指令所应用到的元素
require 声明对某个控制器的依赖
restrict 指定指令如何被使用
scope 为指令创建一个新的作用域或者一个隔离的作用域
template 指定一个将被插入到HTML文档的模板
templateUrl 指定一个将被插入到HTML文档的外部模板
transclude 指定指令是否被用于包含任意内容
当只返回一个链接函数时,所创建的指令只能被当做一个属性来使用。这是大多数AngularJS指令的使用方式,但是也可以使用restrict属性来修改默认配置,并创建可以以其他方式使用的指令。
.directive("unorderedList", function () { return { link: function (scope, element, attrs) { var data = scope[attrs["unorderedList"] || attrs["listSource"]]; // 当作为属性用时attrs["unorderedList"]有值,但当做元素使用时为undefinedvar propertyExpression = attrs["listProperty"] || "price | currency"; if (angular.isArray(data)) { var listElem = angular.element("<ul>"); if (element[0].nodeName == "#comment") { // 当做注释用时,将元素添加到父元素中 element.parent().append(listElem); } else { element.append(listElem); } for (var i = 0; i < data.length; i++) { var itemElement = angular.element("<li>").text(scope.$eval(propertyExpression, data[i])); listElem.append(itemElement); } } }, restrict: "EACM" } })
用于配置restrict定义选项的字母
E 允许指令被用作一个元素
A 允许指令被用作一个属性
C 允许指令被用作一个类
M 允许指令被用作一个注释
1)指令当做元素使用时E
AngularJS中的习惯是将那些通过定义属性template和templateUrl管理模板的指令当做元素来使用。尽管如此,这只是一种习惯,你可以将任何自定义指令当做一个元素来使用,只需在restrict定义属性的值中包含进字母E即可。
<unordered-list list-source="products" list-property="price | currency" />
2)指令当做一个属性使用A
<div unordered-list="products" list-property="price | currency"></div>
3)指令当做一个类使用C
当试图将AngularJS集成到一个不容易修改的程序所生成的HTML时尤为有用。
<div class="unordered-list: products" list-property="price | currency"></div>
这里将类的属性值设置为指令名。我想为指令提供一个配置值,因此在指令名后跟随了一个冒号(:)及这个值。
4)指令当做注释使用M
用注释来使用指令使得其他开发者不易读懂HTML,因为别人都不曾想到注释还能对程序功能起作用。这还能引起一些使用某些构建工具时的问题,因为有些工具会为了发布时缩减文件体积而去除注释。
<div class="panel-body"> <!-- directive: unordered-list products --> </div>
这个注释必须以单词directive开始,跟随一个冒号、指令名以及可选的配置参数。
2.2 使用指令模板
1)使用template定义属性来创建一个简单的模板
.directive("unorderedList", function () { return { link: function (scope, element, attrs) { scope.data = scope[attrs["unorderedList"]]; // 这里使用的scope.data会覆盖controller里面的$scope.data }, restrict: "A", template: "<ul><li ng-repeat='item in data'>" + "{{item.price | currency}}</li></ul>" } })
<div unordered-list="products"> This is where the list will go </div>
链接函数不再负责生成用于向用户展示数据的HTML元素。作为代替,使用了template定义属性来指定一段HTML片段,被用于作为指令所引用到的元素内容。
结果div元素变成如下所示
<div unordered-list="products"> <ul> <li ng-repeat="item in data" class="ng-scope ng-binding">$1.2</li> <li ng-repeat="item in data" class="ng-scope ng-binding">$2.4</li> <li ng-repeat="item in data" class="ng-scope ng-binding">$2.02</li> </ul> </div>
2)使用函数作为模板
在(1)中是使用一个字符串来表示模板内容,但是template属性也可以用作指定一个函数来生成模板化的内容。该函数被传入两个参数(指令所应用到的元素以及属性集合)并返回将被插入到文档中的HTML代码片段。注意不要使用模板函数特性来生成需要以编程方式生成的内容。
<script type="text/template" id="listTemplate"> <ul> <li ng-repeat="item in data">{{item.price | currency}}</li> </ul> </script>
.directive("unorderedList", function () { return { link: function (scope, element, attrs) { scope.data = scope[attrs["unorderedList"]]; }, restrict: "A", template: function () { return angular.element(document.getElementById("listTemplate")).html(); } } })
我添加了一个包含了要使用的模板内容的脚本元素,并设置了template定义对象函数。jqLite不支持通过id属性选择元素(我也不想为了这么简单的一个指令就使用完整的jquery库),所以我使用了DOM API定位脚本元素,并将其包装在一个jqLite对象中。我使用了jqLite得html方法获得模板元素的HTML内容,并作为模板函数的结果返回。
3)使用外部模板
使用脚本元素是一种有用的分离模板内容的方法,但是元素中仍然存在部分HTML文档,在复杂项目中,当你想在程序各部分之间或者甚至程序之间自由地共享模板时,这将变得难以管理。一个可供替代的方法是在一个单独的文件中定义模板内容,并使用templateUrl定义对象属性来指定文件名。
<!--itemTemplate.html--> <p>This is the list from the template file</p> <ul> <li ng-repeat="item in data">{{item.price | currency}}</li> </ul
.directive("unorderedList", function () { return { link: function (scope, element, attrs) { scope.data = scope[attrs["unorderedList"]]; }, restrict: "A", templateUrl: "itemTemplate.html" } })
4)使用函数选择一个外部模板
templateUrl属性可设置为一个函数,用于指定指令所使用的URL,从而提供基于指令所应用的元素来动态地选择模板的方式。
.directive("unorderedList", function () { return { link: function (scope, element, attrs) { scope.data = scope[attrs["unorderedList"]]; }, restrict: "A", templateUrl: function (elem, attrs) { return attrs["template"] == "table" ? "tableTemplate.html" : "itemTemplate.html"; } } })
5)替换元素
默认情况下,模板的内容是被插入到指令所应用的元素里的。replace定义属性能够用于修改这个行为,使得模板可以替换元素。
.directive("unorderedList", function () { return { link: function (scope, element, attrs) { scope.data = scope[attrs["unorderedList"]]; }, restrict: "A", templateUrl: "tableTemplate.html", replace: true } })
<div class="panel-body"> <div unordered-list="products" class="table table-striped"> This is where the list will go </div> </div>
设置replace属性为true后的效果是模板内容将替换掉指令所应用到的div元素。下面是生成的HTML部分:
<div class="panel-body"> <table unordered-list="products" class="table table-striped"> <thead>...</thead> <tbody>...</tbody> </table> </div>
replace属性不仅仅只是用模板替换了元素,还将元素中的属性也转移给了模板内容。这是一种有用的技术,允许指令生成的内容可以被指令所应用到的上下文所配置。例如,我可以将我的自定义指令应用到程序的不同部分,并对每个表格应用不同的bootstrap样式。还可以使用这个特性来将其他AngularJS指令直接转移到一个指令的模板内容中。
<div unordered-list="products" class="table table-striped" ng-repeat="count in [1, 2, 3]"> This is where the list will go </div>
生成3个表格。
2.3 管理指令的作用域
默认情况下,链接函数被传入了控制器的作用域,而该控制器管理着的视图包含了指令所应用到的元素。
<div ng-controller="scopeCtrl" class="panel panel-default"> <div class="panel-body" scope-demo></div> <div class="panel-body" scope-demo></div> </div>
angular.module("exampleApp", []) .directive("scopeDemo", function () { return { template: "<div class='panel-body'>Name: <input ng-model=name /></div>", } }) .controller("scopeCtrl", function ($scope) { // do nothing - no behaviours required });
尽管这里有该指令的两个实例,但它们都在scopeCtrl控制器上更新同一个name属性。双向数据绑定确保了两个输入框元素都会被同步。
2.3.1 创建多个控制器
最简单但不优雅的方式是重用指令来为指令的每个实例创建单独的控制器,这样每个实例就有自己的作用域了。
<div class="panel panel-default"> <div ng-controller="scopeCtrl" class="panel-body" scope-demo></div> <div ng-controller="secondCtrl" class="panel-body" scope-demo></div> </div>
使用两个控制器的结果是有了两个作用域,每个作用域都有自己的name属性,这允许输入框元素可以各自独立地运作。在程序启动时每个控制器都未包含数据。编辑输入框元素能够在包含管理输入框元素的指令实例的控制器的作用域中动态地创建name属性,但是这些属性都是完全独立于另外一个控制器的。
2.3.2 给每个指令实例创建自己的作用域
通过创建控制器来给予指令自己的作用域并不是必需的。另一种更优雅的方式是通过设置scope定义对象属性为true来请求AngularJS为每个指令实例创建一个作用域。
<body ng-controller="scopeCtrl"> <div class="panel panel-default"> <div class="panel-body" scope-demo></div> <div class="panel-body" scope-demo></div> </div> </body>
.directive("scopeDemo", function () { return { template: "<div class='panel-body'>Name: <input ng-model=name /></div>", scope: true } })
设置scope属性为true将允许我在同一个控制器里复用这个指令,所创建的作用域也是普通作用域层次结构中的一部分。这意味着所描述的那些关于对象和属性的继承关系的规则仍然奏效,给予你设置被自定义指令的实例所使用(以及有可能所共享的)数据的灵活性。
<body ng-controller="scopeCtrl"> <div class="panel panel-default"> <div class="panel-body" scope-demo></div> <div class="panel-body" scope-demo></div> </div> </body>
angular.module("exampleApp", []) .directive("scopeDemo", function () { return { template: function() { return angular.element( document.querySelector("#scopeTemplate")).html(); }, scope: true } }) .controller("scopeCtrl", function ($scope) { $scope.data = { name: "Adam" }; $scope.city = "London"; });
<script type="text/ng-template" id="scopeTemplate"> <div class="panel-body"> <p>Name: <input ng-model="data.name" /></p> <p>City: <input ng-model="city" /></p> <p>Country: <input ng-model="country" /></p> </div> </script>
结果:
data.name 这个属性是在一个对象上定义的,意味着这个值将会在指令的各个实例之间所共享,而且所有绑定到该属性的输入框元素将会保持同步
city 这个属性是在控制器的作用域上被直接赋值的,意味着指令所有的作用域将会从同一个初始值开始,但是在输入框元素被修改时会在自己的作用域上创建和修改自己的版本
country 这个属性没有被赋任何值。当相应输入框元素被修改时,指令的每个实例将会创建出独立的country属性
2.3.3 创建隔离的作用域
创建一个隔离的作用域就是AngularJS为指令的每个实例创建一个独立的作用域的地方,但是这个作用域并不继承自控制器的作用域。在创建一个打算在许多各种不同情况下重用的指令时,以及不想要任何由控制器或作用域层次上的其他地方定义的对象和属性导致的继承时,这是很有用的。当scope定义属性被设置为一个对象时,可创建一个隔离的作用域。隔离的作用域的最基本类型是用一个没有属性的对象表示。
.directive("scopeDemo", function () { return { template: function() { return angular.element( document.querySelector("#scopeTemplate")).html(); }, scope: {} } })
把文件加载到作用域就可以看到隔离作用域的效果,六个输入框都是空的。这正是隔离作用域的效果,因为这里没有来自控制器作用域的继承,ng-model指令所指定的任何属性都没有定义值。如果编辑输入框元素,AngularJS就会动态的创建这些属性,但是这些属性只是被修改的输入框元素相关联的指令的隔离作用域的一部分。
指令的每个实例都有自己的作用域,但是并未从控制器作用域继承任何数据值。因为没有继承关系,对通过对象定义的属性做修改就不会被传播到控制器的作用域上。简而言之,隔离的作用域是从作用域层次结构上被隔绝出来的。
1)通过属性值进行绑定
当创建打算在不同情况下复用的指令时,隔离的作用域是一种重要的构件,因为它防止了在控制器作用域和指令之间出现意料之外的交互。隔离的作用域允许你使用应用于指令旁边的元素上的属性将数据值绑定到控制器作用域上。
<script type="text/ng-template" id="scopeTemplate"> <div class="panel-body"> <p>Data Value: {{local}}</p> </div> </script> <body ng-controller="scopeCtrl"> <div class="panel panel-default"> <div class="panel-body"> Direct Binding: <input ng-model="data.name" /> </div> <div class="panel-body" scope-demo nameprop="{{data.name}}"></div> </div> </body>
angular.module("exampleApp", []) .directive("scopeDemo", function () { return { template: function() { return angular.element( document.querySelector("#scopeTemplate")).html(); }, scope: { local: "@nameprop" } } }) .controller("scopeCtrl", function ($scope) { $scope.data = { name: "Adam" }; });
在scope定义对象中,设置了在指令作用域内一个特性和一个属性之间的一个单向映射。在给scope定义对象赋值时,在该对象上定义了一个名为local的属性,这告诉AngularJS我要在指令作用域上根据那个名称定义一个新的属性。local属性的值以一个@字符作为前缀,指定了属性local的值应该从一个来自名为nameprop的特性的单向绑定来获得。
datavalue的值随着输入框的值改变。
这给予了我对所需的作用域继承关系的有选择的控制权,而且,作为附加,在指令被使用时选择范围是可以配置的,这是在不需要修改任何代码或html标记的情况下允许一个指令以多种不同方式重用的关键所在。
<div class="panel panel-default"> <div class="panel-body"> Direct Binding: <input ng-model="data.name" /> </div> <div class="panel-body" scope-demo nameprop="{{data.name}}"></div> <div class="panel-body" scope-demo nameprop="{{data.name + 'Freeman'}}"></div> </div>
Note:在隔离作用域上的单向绑定总是被计算字符串值。如果你想访问一个数组,就必须使用双向绑定,即使你不打算修改它。
2)创建双向绑定
要创建一个双向绑定,得在创建隔离作用域时将字符@替换为字符=;在使用单向绑定时,提供了一个被{{和}}字符所包围的绑定表达式,但是AngularJS需要知道在双向绑定中那个属性需要被更新,所以我将该值设置为一个属性名:<div class="panel-body" scope-demo nameprop="data.name"></div>
<script type="text/ng-template" id="scopeTemplate"> <div class="panel-body"> <p>Data Value: <input ng-model="local" /></p> </div> </script> <script type="text/javascript"> angular.module("exampleApp", []) .directive("scopeDemo", function () { return { template: function() { return angular.element( document.querySelector("#scopeTemplate")).html(); }, scope: { local: "=nameprop" } } }) .controller("scopeCtrl", function ($scope) { $scope.data = { name: "Adam" }; }); </script> <body ng-controller="scopeCtrl"> <div class="panel panel-default"> <div class="panel-body"> Direct Binding: <input ng-model="data.name" /> </div> <div class="panel-body" scope-demo nameprop="data.name"></div> </div> </body>
3)计算表达式
最后一个隔离作用域的特性是指定表达式作为属性并将其在控制器作用域中进行计算的能力。
<script type="text/ng-template" id="scopeTemplate"> <div class="panel-body"> <p>Name: {{local}}, City: {{cityFn()}}</p> </div> </script> <script type="text/javascript"> angular.module("exampleApp", []) .directive("scopeDemo", function () { return { template: function () { return angular.element( document.querySelector("#scopeTemplate")).html(); }, scope: { local: "=nameprop", cityFn: "&city" } } }) .controller("scopeCtrl", function ($scope) { $scope.data = { name: "Adam", defaultCity: "London" }; $scope.getCity = function (name) { return name == "Adam" ? $scope.data.defaultCity : "Unknown"; } }); </script> <body ng-controller="scopeCtrl"> <div class="panel panel-default"> <div class="panel-body"> Direct Binding: <input ng-model="data.name" /> </div> <div class="panel-body" scope-demo city="getCity(data.name)" nameprop="data.name"></div> </div> </body>
city特性的值是一个表达式,调用了getCity行为并将data.name属性作为参数传入进行处理。要使这个表达式在隔离作用域中可用,在scope对象上增添了一个新的属性cityFn。前缀&告诉AngularJS我想将所指定特性的值绑定到一个函数。在这里该特性为city,我想将其绑定到一个名为cityFn的函数。剩下来的就是在指令模板中调用函数来计算表达式。
注意在调用cityFn()时,是使用了圆括号的,要计算被这个特性所指定的表达式,这是必需的,即使当表达式本身就是一个函数调用时。
4)使用隔离作用域的数据来计算一个表达式
(3)的变体是允许你将来自待计算的隔离作用域的数据作为控制器作用域表达式的一部分。要做到这点,修改了表达式以便传递给行为的参数是在控制器作用域上没有定义过的属性名,如下:
<div class="panel-body" scope-demo city="getCity(nameVal)" nameprop="data.name"></div>
在这里我选用nameVal作为参数名,为了传递来自隔离作用域的数据,更新了表达式的模板中的绑定,传入一个为表达式参数提供值的对象:
<div class="panel-body"> <p>Name:{{local}},City:{{cityFn({nameVal:local})}}</p> </div>
这所产生的结果是创建出了一个可混合使用定义在隔离作用域和控制器作用域中的数据对表达式进行计算的数据绑定。要小心地注意保证控制器作用域没有定义一个名字和表达式中参数相同的属性,如果定义了,那么来自隔离作用域的值将会被忽略。