转载请写明来源地址:http://www.jb51.cc/article/p-tasriubw-bph.html
directive介绍
directive是DOM元素上的标记,告诉angularjs的HTML编译器($complile
)给DOM元素附加上一些特殊的行为,或者是改变DOM元素和它的子元素。
看到编译两个字,很多人会感到很懵,javascript不是解释执行的吗。其实这里的编译是因为,给html附加directive的递归过程很像是编译源代码的过程,所以才叫编译。
angularjs内置了一套directive,像ngBind,ngModel和ngClass。就像你创建controller和service一样,你也可以创建自己的directive。当angularjs启动你的app时,它会遍历DOM来匹配directive。
匹配directive
在写directive之前,先了解一下angularjs的html编译器怎么觉得何时需要使用给定的指令。
就像element匹配selector一样,当directive是element的声明的一部分时,我们就把这个叫做element匹配directive。
<input>
元素匹配ngModel
指令:<input ng-model="foo">
<input>
元素匹配ngModel
指令:<input data-ng-model="foo">
<person>
元素匹配person
指令:<person>{{name}}</person>
规范化
angular规范了元素的标签名和属性名,以此来决定元素和指令的匹配。我们一般提到指令时都是用他们区分大小写的骆驼命名法,比如ngModel。但是HTML是不区分大小写的,因此我们在DOM中要把他们写成全部小写的作为元素的属性,并且中间用破折号分割,例如ng-model。
标准如下:
- 以
x-
和data-
开头,作为元素或者属性的前缀。 - 用
:
,-
或_
分割,骆驼命名的变种。
下面的例子中,所有写法都是一样匹配ngBind
指令
<!DOCTYPE html>
<html xmlns:ng="http://www.w3.org/1999/xhtml" xmlns:data-ng="http://www.w3.org/1999/xhtml">
<head>
<Meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<script> angular.module('docsBindExample',[]) .controller('Controller',['$scope',function($scope) { $scope.name = 'Max Karl Ernst Ludwig Planck (April 23,1858 - October 4,1947)'; }]); </script>
<body ng-app="docsBindExample">
<div ng-controller="Controller">
Hello <input ng-model='name'> <hr/>
<span ng-bind="name"></span> <br/>
<span ng:bind="name"></span> <br/>
<span ng_bind="name"></span> <br/>
<span data-ng-bind="name"></span> <br/>
<span data-ng:bind="name"></span> <br/>
<span x-ng-bind="name"></span> <br/>
<span x-ng_bind="name"></span> <br/>
</div>
</body>
</html>
最佳的方式是ng-bind
,如果想通过一些html浏览器的校验可以使用data-ng-bind,其他方式最好别用。
directive类型
$compile
可以匹配基于元素name,属性,class的name和注释中的指令。
angular内置的指令都可以匹配元素name,属性,class的name和注释中的指令,下面演示了匹配指令的几种方法。
<my-dir></my-dir>
<span my-dir="exp"></span>
<!-- directive: my-dir exp -->
<span class="my-dir: exp;"></span>
在元素name和属性使用directive,要比在注释和class的name上好很多,这样更容易判断元素匹配哪些指令。
注释指令通常用在跨越多个元素不方便指令的情况下,比如在<table>
中,但是在1.2的时候,提供了ng-repeat-start
和ng-repeat-end
解决了这个问题,之后遇到这类情况尽量使用这两个指令。
创建directive
和controller和service一样,directive也是由module注册的。可以使用module.directive
注册directive,module.directive
接受一个规范的directive名字,后面紧跟一个工厂函数。工厂函数返回的是一个带有不同选项的object,这个object告诉$compile
当元素匹配到这个指令时该如何做。
当编译器第一次匹配到指令时,指令的工厂函数只调用一次。因此,你可以在函数里做一些初始化的操作。函数是由$injector.invoke
调用的,函数像controller一样是可以注入的。
我们将通过几个directive的例子来深入了解一下不同选项和编译过程。
需要注意的一点是,为了不和一些未来的标准冲突,directive名字最好有自己的前缀。例如你创建了一个<carousel>
的指令,而HTML7刚刚好推出了相同的元素,那就冲突了。前缀最好使用两三个字母比如btfCarousel,同样不要使用ng做自己的directive前缀,以防会和angular的新版本冲突。
下面的例子中我们就用my作为directive的前缀。
模版扩展directive
假如你的模版有一大块都是展示客户的信息,这个模版在你代码中重复了好多次,当你需要修改模版的一个地方,其他的几个也需要修改,这就是一个使用directive简化模版的好时机。
下面是一个简单的例子,用静态模版代替html中的内容。
<!DOCTYPE html> <html> <head> <Meta charset="uft-8"/> <title></title> </head> <script src="script/angular.min.js"></script> <body ng-app="docsSimpleDirective"> <div ng-controller="Controller"> <div my-customer></div> </div> </body> <script> angular.module('docsSimpleDirective',function ($scope) { $scope.customer = { name: 'Naomi',address: '1600 Amphitheatre' }; }]) .directive('myCustomer',function () { return { template: 'Name: {{customer.name}} Address: {{customer.address}}' }; }); </script> </html>
注意在这个directive的例子中我们使用了数据绑定,当$compile
编译链接到<div my-customer></div>
时,他会继续在元素的子元素上匹配directive,这意味着我们可以使用directive的组合,在后面的例子中我们再详细讨论。
在上面的例子中,我们列出了template选项的值,但是如果template内容越来越多的话,这会很头疼。
除非你的template非常小,否则的话需要把内容放置在一个html文件中,然后用templateUrl选项加载它。templateUrl的工作原理很像ngInclude,我们把上面的例子用templateUrl改一下:
docsSimpleDirective.html:
<!DOCTYPE html>
<html>
<head>
<Meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="docsSimpleDirective">
<div ng-controller="Controller">
<div my-customer></div>
</div>
</body>
<script> angular.module('docsSimpleDirective',function () { return { templateUrl: 'my-customer.html' }; }); </script>
</html>
my-customer.html:
Name: {{customer.name}} Address: {{customer.address}}
templateUrl也可以是个函数,这个函数返回一个可以被directive加载的template的url。AngularJS会传入两个参数调用这个函数,directive所添加在的元素本身elem,还有这个元素的属性attr。
要注意到,你不能再templateUrl的函数中访问$scope
,因为这个时候$scope
还没初始化。
docsTemplateUrlFunctionDirective.html:
<!DOCTYPE html>
<html>
<head>
<Meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="docsTemplateUrlFunctionDirective">
<div ng-controller="Controller">
<div my-customer type="name"></div>
<div my-customer type="address"></div>
</div>
</body>
<script> angular.module('docsTemplateUrlFunctionDirective',function($scope) { $scope.customer = { name: 'Naomi',function() { return { templateUrl: function(elem,attr) { return 'customer-' + attr.type + '.html'; } }; }); </script>
</html>
customer-address.html:
Address: {{customer.address}}
customer-name.html:
Name: {{customer.name}}
当你创建指令时,它仅仅默认的限定了元素名称和属性名称,如果想要class名称也触发,那就要使用restrict
选项
restrict选项列表:
这些限定也可以组合起来:
上面的例子用restrict: 'E'
再改一下:
docsRestrictDirective.html:
<!DOCTYPE html>
<html>
<head>
<Meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="docsRestrictDirective">
<div ng-controller="Controller">
<my-customer></my-customer>
</div>
</body>
<script> angular.module('docsRestrictDirective',function() { return { restrict: 'E',templateUrl: 'my-customer.html' }; }); </script>
</html>
那么什么时候用元素什么时候用属性呢?
当你创建一个组件时使用元素,大多数情况下,是为template创建DSL。
当你想用新的函数装饰一下已经存在的元素时使用属性。
directive的范围隔离
myCustomer
在之前的例子中表现的非常不错,但是他有一个致命的问题,就是只能在给定的scope中使用一次。
重用directive笨一点的办法就是写多一个controller:
docsScopeProblemExample.html
<!DOCTYPE html>
<html>
<head>
<Meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="docsScopeProblemExample">
<div ng-controller="NaomiController">
<my-customer></my-customer>
</div>
<hr>
<div ng-controller="IgorController">
<my-customer></my-customer>
</div>
</body>
<script> angular.module('docsScopeProblemExample',[]) .controller('NaomiController',address: '1600 Amphitheatre' }; }]) .controller('IgorController',function ($scope) { $scope.customer = { name: 'Igor',address: '123 Somewhere' }; }]) .directive('myCustomer',function () { return { restrict: 'E',templateUrl: 'my-customer.html' }; }); </script>
</html>
当然这可不是个好办法。我们需要做的就是把directive的范围内部和外部的scope区分开,然后把外部scope映射到内部范围。通过创建隔离范围(isolate scope)来做到这一点,这需要使用directive的scope选项。
docsIsolateScopeDirective.html:
<!DOCTYPE html>
<html>
<head>
<Meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="docsIsolateScopeDirective">
<div ng-controller="Controller">
<my-customer info="naomi"></my-customer>
<hr>
<my-customer info="igor"></my-customer>
</div>
</body>
<script> angular.module('docsIsolateScopeDirective',function($scope) { $scope.naomi = { name: 'Naomi',address: '1600 Amphitheatre' }; $scope.igor = { name: 'Igor',scope: { customerInfo: '=info' },templateUrl: 'my-customer-iso.html' }; }); </script>
</html>
my-customer-iso.html:
Name: {{customerInfo.name}} Address: {{customerInfo.address}}
<my-customer>
元素绑定info
属性到naomi
,第二个info
属性到igor
,这样就暴露在了controller的范围中。
我们再仔细看下scope选项:
//...
scope: {
customerInfo: '=info'
},//...
scope选项是一个对象,该对象包含了每一个隔离范围绑定的属性,在这个例子中就一个属性:
directives的scope选项的命名标准和directive名称一样。为了绑定到<div bind-to-this="thing">
的属性,你需要这么写=bindToThis
。
如果属性名称和你要绑定到directive的内部范围的值相同,你可以缩写为:
...
scope: {
// same as '=customer'
customer: '='
},...
除了绑定不同的数据到同一个directive的内部范围外,directive隔离范围还有另一种用法。
我们再增加一个属性vojta
,并试着从我们directive的模版中访问它:
docsIsolationExample.html:
<!DOCTYPE html>
<html>
<head>
<Meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="docsIsolationExample">
<div ng-controller="Controller">
<my-customer info="naomi"></my-customer>
</div>
</body>
<script> angular.module('docsIsolationExample',address: '1600 Amphitheatre' }; $scope.vojta = { name: 'Vojta',address: '3456 Somewhere Else' }; }]) .directive('myCustomer',scope: { customerInfo:'=info',},templateUrl: 'my-customer-plus-vojta.html' }; }); </script>
</html>
my-customer-plus-vojta.html:
Name: {{customerInfo.name}} Address: {{customerInfo.address}} <hr> Name: {{vojta.name}} Address: {{vojta.address}}
然后发现{{vojta.name}}
和{{vojta.address}}
是空的,这意味着它们是未定义的,尽管我们在controller里定义了vojta
,但是在directive确是不可用的。
顾名思义,directive的隔离范围隔离了一切,除了你明确添加到directive的scope选项中的model外。这在构建可重用的组件时相当有用,因为可以在更改model时保护组件,只有明确在scope中指定的model才会影响到组件。
记住一点,scope会继承自父scope,而隔离scope则不会。
创建一个操作DOM的directive
下面的例子演示的是一个显示当前时间的指令的创建,显示的时间一秒更新一次。
操作DOM的指令通常使用link
选项来注册DOM监听器和修改DOM。link会在template被克隆后在directive逻辑放置的地方执行。我们在这里使用$interval
服务做定时调用程序,
link对应的是一个函数function link(scope,element,attrs,controller,transcludeFn) { ... }
,参数详解如下:
- scope : AngularJS的scope对象
- element : directive相匹配的element,由jqLite封装
- attrs : element的属性构成的键值对对象
- controller : directive需要的controller实例,或者directive自身的controller,具体要看directive的require选项如何指定。
- transcludeFn : 预绑定到相应的transclude范围的transclude函数
在我们的link函数中,我们每秒更新一次显示的时间,另外用户在输入框更改了指令绑定了的格式化字符串时也要更新显示时间。这比$timeou
使用起来容易点,而且也更适合做端到端的测试。我们要确保在测试完成之前所有的$timeouts
都已经完成。如果directive被删除,我们也要删除$interval
,这样可以防止内存泄漏。
<!DOCTYPE html>
<html>
<head>
<Meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="docsTimeDirective">
<div ng-controller="Controller">
Date format: <input ng-model="format">
<hr/>
Current time is: <span my-current-time="format"></span>
</div>
</body>
<script> angular.module('docsTimeDirective',function ($scope) { $scope.format = 'M/d/yy h:mm:ss a'; }]) .directive('myCurrentTime',['$interval','dateFilter',function ($interval,dateFilter) { function link(scope,attrs) { var format,timeoutId; function updateTime() { element.text(dateFilter(new Date(),format)); } scope.$watch(attrs.myCurrentTime,function (value) { format = value; updateTime(); }); element.on('$destroy',function () { $interval.cancel(timeoutId); }); // start the UI update process; save the timeoutId for canceling timeoutId = $interval(function () { updateTime(); // update DOM },1000); } return { link: link }; }]); </script>
</html>
和module.controller
API一样,module.directive
也支持依赖注入,因此,我们在link函数中可以使用$interval
,dateFilter
.
我们注册了一个事件element.on(‘$destroy’,…),那么是如何激活$destroy
呢。
通过监听这个事件,你可以移除可能会引起内存泄漏的事件监听器。注册下scope和element上的监听器会随着scope和element的销毁而跟着销毁,但是注册在service或者注册在DOM节点上的监听器则不会,它们需要显示的移除,否则就要承担内存泄漏的风险。
如果想显示清除可能导致内存泄漏的监听器,那么就需要使用$destroy
事件监听,并且需要把它们注册在element上或者scope上。
创建封装其他element的directive
在之前的例子中,我们已经看到,我们可以通过隔离范围(isolate scope)把model传到directive中,但是有时候我们想要传递整个template,而不仅仅是一个字符串或者一个对象。假设我们要创建一个dialog Box
的组件,里面可以封装任意内容。
那么,我们就需要transclude
选项:
docsTransclusionDirective.html:
<!DOCTYPE html> <html> <head> <Meta charset="uft-8"/> <title></title> </head> <script src="script/angular.min.js"></script> <body ng-app="docsTransclusionDirective"> <div ng-controller="Controller"> <my-dialog>Check out the contents,{{name}}!</my-dialog> </div> </body> <script> angular.module('docsTransclusionDirective',function($scope) { $scope.name = 'Tobias'; }]) .directive('myDialog',transclude: true,scope: {},templateUrl: 'my-dialog.html' }; }); </script> </html>
my-dialog.html:
<div class="alert" ng-transclude></div>
transclude选项使得directive的上下文可以访问指令外部的范围而不是访问指令内部的范围。
为了说明这一点我们再看一个例子,我们在上面例子的基础上增加了一个link函数,该函数将name重命名为Jeff,你认为{{name}}绑定会最终显示什么呢。
docsTransclusionExample.html:
<!DOCTYPE html> <html> <head> <Meta charset="uft-8"/> <title></title> </head> <script src="script/angular.min.js"></script> <body ng-app="docsTransclusionExample"> <div ng-controller="Controller"> <my-dialog>Check out the contents,{{name}}!</my-dialog> </div> </body> <script> angular.module('docsTransclusionExample',templateUrl: 'my-dialog.html',link: function(scope) { scope.name = 'Jeff'; } }; }); </script> </html>
我们通常都会认为{{name}}会变成Jeff,但实际上仍然会是Tobias。
transclude选项改变了scope嵌套的方式,它使得指令访问指令外部的scope,而不是指令内部的scope,它给出了访问外部scope的上下文。
请注意,如果directive没有创建自己的scope,那么scope.name = 'Jeff'
中的scope就会是指令外部的scope,那么我们就会看到显示的是Jeff。
封装一些内容的directive还是非常有意义的,如果不这样做,你需要每个model单独传到directive中,如果你要一个个传递model,那么你就不能传递任意的内容。当你想要传递内容时就使用transclude: true
吧。
下面我们加个按钮在dialog Box
上,绑定自己的一些行为在指令上。
docsIsoFnBindExample.html:
<!DOCTYPE html> <html> <head> <Meta charset="uft-8"/> <title></title> </head> <script src="script/angular.min.js"></script> <body ng-app="docsIsoFnBindExample"> <div ng-controller="Controller"> {{message}} <my-dialog ng-hide="dialogIsHidden" on-close="hideDialog(message)"> Check out the contents,{{name}}! </my-dialog> </div> </body> <script> angular.module('docsIsoFnBindExample','$timeout',function($scope,$timeout) { $scope.name = 'Tobias'; $scope.message = ''; $scope.hideDialog = function(message) { $scope.message = message; $scope.dialogIsHidden = true; $timeout(function() { $scope.message = ''; $scope.dialogIsHidden = false; },2000); }; }]) .directive('myDialog',scope: { 'close': '&onClose' },templateUrl: 'my-dialog-close.html' }; }); </script> </html>
my-dialog-close.html:
<div class="alert">
<a href class="close" ng-click="close({message: 'closing for now'})">×</a>
<div ng-transclude></div>
</div>
我们想要在directive的scope调用函数,但是函数运行在它注册的scope上下文中。
我们在之前等例子中已经了解过在directive的scope选项中使用=attr
,但在上面的例子中,我们使用&attr
代替。&
绑定允许directive在特定时间触发表达式的求值计算。任何合法的表达式都可以,包括函数调用的表达式,基于此,&
绑定通常用于将回调函数绑定到directive的行为上。
当用户点击对话框中的x时,directive的close
函数将会被调用,再隔离范围的close方法将会被调用,实际上是计算原始scope中的hideDialog(message)
表达式,从而运行controller的hideDialog
函数。
通过&
绑定也可以把一些数据通过表达式从directive的隔离范围传递到父scope中,它需要将数据写成名称和值的键值对传递到函数中。例如,hideDialog函数接受一个message在dialog隐藏的时候显示,它是由directive的close({message: 'closing for now'})
指定的。这样,本地变量message就可以在on-close的表达式中可用了。
你可以在需要directive给绑定暴露一些api时在directive的scope选项中使用&attr
。
创建增加事件监听器的directive
之前我们已经用link函数创建了可以操作DOM的directive,在其之上,我们来创建可以监听其元素事件的directive。
以下是可以拖动元素的directive:
<!DOCTYPE html>
<html>
<head>
<Meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="dragModule">
<span my-draggable>Drag Me</span>
</body>
<script> angular.module('dragModule',[]) .directive('myDraggable',['$document',function ($document) { return { link: function (scope,attr) { var startX = 0,startY = 0,x = 0,y = 0; element.css({ position: 'relative',border: '1px solid red',backgroundColor: 'lightgrey',cursor: 'pointer' }); element.on('mousedown',function (event) { // Prevent default dragging of selected content event.preventDefault(); startX = event.pageX - x; startY = event.pageY - y; $document.on('mousemove',mousemove); $document.on('mouseup',mouseup); }); function mousemove(event) { y = event.pageY - startY; x = event.pageX - startX; element.css({ top: y + 'px',left: x + 'px' }); } function mouseup() { $document.off('mousemove',mousemove); $document.off('mouseup',mouseup); } } }; }]); </script>
</html>
创建相互通信的directive
你可以在模版中将directives组合着用,有时候需要把几个directives组合成一个组件。
比如下面的点击切换tab的例子:
docsTabsExample.html:
<!DOCTYPE html> <html> <head> <Meta charset="uft-8"/> <title></title> </head> <script src="script/angular.min.js"></script> <body ng-app="docsTabsExample"> <my-tabs> <my-pane title="Hello"> <p>Lorem ipsum dolor sit amet</p> </my-pane> <my-pane title="World"> <em>Mauris elementum elementum enim at suscipit.</em> <p><a href ng-click="i = i + 1">counter: {{i || 0}}</a></p> </my-pane> </my-tabs> </body> <script> angular.module('docsTabsExample',[]) .directive('myTabs',controller: ['$scope',function MyTabsController($scope) { var panes = $scope.panes = []; $scope.select = function (pane) { angular.forEach(panes,function (pane) { pane.selected = false; }); pane.selected = true; }; this.addPane = function (pane) { if (panes.length === 0) { $scope.select(pane); } panes.push(pane); }; }],templateUrl: 'my-tabs.html' }; }) .directive('myPane',function () { return { require: '^^myTabs',restrict: 'E',scope: { title: '@' },link: function (scope,tabsCtrl) { tabsCtrl.addPane(scope); },templateUrl: 'my-pane.html' }; }); </script> </html>
my-tabs.html:
<div class="tabbable"> <ul class="nav nav-tabs"> <li ng-repeat="pane in panes" ng-class="{active:pane.selected}"> <a href="" ng-click="select(pane)">{{pane.title}}</a> </li> </ul> <div class="tab-content" ng-transclude></div> </div>
my-pane.html:
<div class="tab-pane" ng-show="selected"> <h4>{{title}}</h4> <div ng-transclude></div> </div>
myPane指令有个值等于^^myTabs的require选项。当directive指令使用require选项,如果找不到对应的controller,$compile
就会报错。^^前缀的意思是该directive需要在父element搜索指定到controller(^前缀表示在父element或者directive自己所在的element进行搜索controller,如果没有前缀,那么就仅仅在自己所在的element进行搜索)
那么myTabs控制器从何而来呢,不出所料,有一个directive会通过controller选项指定controller,如你所见,myTabs指令使用了这个选项。像ngController一样,该选项为directive的模版附加一个controller。
如果有必要从模版中引用controller或者controller上的函数,你可以使用controllerAs选项来指定controller的别名,为了使用这个配置,directive需要定义scope选项。当directive被用作一个组件时是这样做非常有用的。
再看一下myPane的定义,注意到link函数的第四个参数是tabsCtrl。当directive通过require指定了controller之后,它就可以通过link函数的第四个参数来接收指定的controller,这样myPane就可以调用myTabs的addPane函数。
如果需要多个controller,directive的require选项也可以接收数组参数。那么相应的,link函数的第四个参数也会是一个数组。
angular.module('docsTabsExample',[])
.directive('myPane',function() {
return {
require: ['^^myTabs','ngModel'],scope: {
title: '@'
},link: function(scope,controllers) {
var tabsCtrl = controllers[0],modelCtrl = controllers[1];
tabsCtrl.addPane(scope);
},templateUrl: 'my-pane.html'
};
});
link函数和controller之间的区别在于:controller可以暴露api出来,link函数通过require选项去使用controller。
如果我的文章对您有帮助,请用支付宝打赏: