英文地址:directivejavascript
Directive是教HTML玩一些新把戏的途径。在DOM编译期间,directives匹配HTML并执行。这容许directive注册行为或者转换DOM结构。
Angular自带一组内置的directive,对于创建Web App有很大帮助。继续扩展的话,能够在HTML定义领域特定语言(domain specific language ,DSL)。css
Directive有驼峰式(camel cased)的风格的命名,如ngBind(放在属性里貌似用不了~)。但directive也能够支蛇底式的命名(snake case),须要经过:(冒号)、-(减号)或_(下划线)链接。做为一个可选项,directive能够用“x-”或者“data-”做为前缀,以知足HTML验证须要。这里列出directive的合法命名:html
Directive能够放置于元素名、属性、class、注释中。下面是引用myDir这个directive的等价方式。(但不少directive都限制为“属性”的使用方式)java
<span my-dir="exp"></span> <span class="my-dir: exp;"></span> <my-dir></my-dir> <!-- directive: my-dir exp -->
Directive能够经过多种方式引用,下面列出N种等价的方式:angularjs
<!DOCTYPE HTML> <html lang="zh-cn" ng-app> <head> <meta charset="UTF-8"> <title>invoke-directive</title> <style type="text/css"> .ng-cloak { display: none; } </style> </head> <body> <div ng-controller="MyCtrl"> Hello <input ng-model="name"/><hr/> ngBind="name" 这个用不了~~ <span ngBind="name"></span><br/> ng:bind="name"<span ng:bind="name"></span><br/> ng_bind="name"<span ng_bind="name"></span><br/> ng-bind="name"<span ng-bind="name"></span><br/> data-ng-bind="name"<span data-ng-bind="name"></span><br/> x-ng-bind="name"<span x-ng-bind="name"></span><br/> </div> <script src="../angular-1.0.1.js" type="text/javascript"></script> <script type="text/javascript"> function MyCtrl($scope) { $scope.name = "beauty~~"; } </script> </body> </html>
在编译过程当中,compiler经过$interpolate服务匹配文本与属性中的嵌入表达式(如{{something}})。这些表达式将会注册为watches,而且做为digest cycle(以前不是digest-loop吗?!)的一部分,一同更新。下面是一个简单的interpolation:chrome
<img src="img/{{username}}.jpg"/>Hello {{username}}!
HTML“编译”的三个步骤:express
1. 首先,经过浏览器的标准API,将HTML转换为DOM对象。这是很重要的一步。由于模版必须是可解析(符合规范)的HTML。这里能够跟大多数的模版系统作对比,它们通常是基于字符串的,而不是基于DOM元素的。api
2. 对DOM的编译(compilation)是经过调用$comple()方法完成的。这个方法遍历DOM,对directive进行匹配。若是匹配成功,那么它将与对应的DOM一块儿,加入到directive列表中。只要全部与指定DOM关联的directive被识别出来,他们将按照优先级排序,并按照这个顺序执行他们的compile() 函数。directive的编译函数(compile function),拥有一个修改DOM结构的机会,并负责产生link() function的解析。$compile()方法返回一个组合的linking function,是全部directive自身的compile function返回的linking function的集合。数组
3. 经过上一步返回的linking function,将模版与scope链接起来。这反过来会调用directive自身的linking function,容许它们在元素上注册一些监听器(listener),以及与scope一块儿创建一些watches。这样得出的结果,是在scope与DOM之间的一个双向、即时的绑定。scope发生改变时,DOM会获得对应的响应。浏览器
var $compile = ...; // injected into your code var scope = ...; var html = '<div ng-bind='exp'></div>'; // Step 1: parse HTML into DOM element var template = angular.element(html); // Step 2: compile the template var linkFn = $compile(template); // Step 3: link the compiled template with the scope. linkFn(scope);
在这个时候,你可能会想知道为何编译进程会划分为compile和linke两个步骤。为了明白这一点,让咱们看看一个真实的例子(repeater)
Hello {{user}}, you have these actions: <ul> <li ng-repeat="action in user.actions">{{action.description}}</li> </ul>
简单地讲,之因此分开compile和linke两步,是由于有时候须要在model改变后,对应的DOM结构也须要改变的状况,如repeaters。
当上面的例子被编译时,编译器会遍历全部节点以寻找directive。{{user}}是一个interpolation directive的例子。ngRepeat又是另一个directive。但ngRepeat有一个难点。它须要可以很快地为每个在users.actions中的action制造出新的li的能力。这意味着它为了知足克隆li而且嵌入特定的action(这里是指user的actions的其中一个值)的目的,须要保持一个干净li元素的拷贝,li元素须要被克隆和插入ul元素。但仅仅克隆li元素是不够的。还须要编译li,以便它的directive({{action.descriptions}})可以在正确的scope中被解析。原始的方法,通常会简单地插入一个li元素的拷贝,而后编译它。但编译每个li元素的拷贝会比较缓慢,由于编译过程须要咱们遍历DOM节点树,查找directive并运行它们。若是咱们有一个编译,须要经过repeater对100个item进行处理,那么咱们将陷入性能问题。
问题的解决方案,是将编译过程分解为两个步骤。compile阶段识别出全部directive,而且将它们按照优先级进行排序,在linking阶段将特定的scope与特定的li绑定在一块儿。
ngRepeat将各个li分开编译以防止编译过程落入li元素中。li元素的编译结果是一个包含全部包含在li元素中的directive的linking function,准备与特定li元素的拷贝进行链接。在运行时,ngRepeat监测表达式,并做为一个item,被加入到一个li元素拷贝的数组,为克隆好的li元素建立新的scope,并调用该拷贝对应的link function。
总结:
编译函数(compile function) - 编译函数在directive中是比较少见的,由于大多数directive只关心与指定的DOM元素工做,而不是改变DOM元素的模版(DOM自身以及内部的结构)。为了优化性能,一些能够被directive实例共享的操做,能够移动到compile函数中。
链接函数(link function) - 极少directive是没有link function的。link function容许directive在指定的拷贝后的DOM元素实例上注册监听器,也能够将scope中特定内容复制到DOM中。
在这个例子里面,咱们将创建一个根据输入格式,显示当前时间的directive。
<!DOCTYPE HTML> <html lang="zh-cn" ng-app="TimeFormat"> <head> <meta charset="UTF-8"> <title>time-format</title> </head> <body> <div ng-controller="MyCtrl" id="main"> Date format: <input ng-model="format" type="text"/><hr/> <!--下面使用属性x-current-time,是为了试试上面说的合法命名~~current:time、current-time、current_time、data-current-time -_-!!! --> Current time is : <span x-current-time="format" id="myFormat"></span><br/> <button ng-click="remove()">remove the span</button> </div> <script src="../angular-1.0.1.js" type="text/javascript"></script> <script type="text/javascript"> angular.module("TimeFormat", []) //在TimeFormat应用中注册“currentTime”这个directive的工厂方法 //前文提到过,依赖注入,能够直接在function的参数中写入,这里注入了$timeout、dataFilter .directive("currentTime", function (dateFilter) { //这个是上面提到的linking function。(不须要添加compile function,为啥?。。。) return function (scope, element, attr) { var intervalId; //更新对应element的text值,即更新时间 function updateTime() { element.text(dateFilter(new Date(), scope.format)); } //经过watch,监控span对象的currentTime的值(是format这个model值,即input的值!!) //这个方法仅仅在format发生改变的时候执行 scope.$watch(attr.currentTime, function (value) { scope.format = value; updateTime(); }); //当span被去掉的时候,取消更新 element.bind("$destroy", function () { clearInterval(intervalId); }); intervalId = setInterval(updateTime, 1000); }; }).controller("MyCtrl",function($scope,$rootScope) { $scope.format = "M/d/yy h:mm:ss a"; $scope.remove = function() { var oFormat = document.getElementById("myFormat"); if(oFormat) { angular.element(oFormat).remove();//经过这种方式调用remove,能够触发$destroy事件啊!!!试了我N久。。。 } }; }); </script> </body> </html>
下面是一个建立directive样例(directive对象定义模版)。想看详细列表,请继续往下看。
var myModule = angular.module(...); myModule.directive('directiveName', function factory(injectables) { var directiveDefinitionObject = { priority: 0, template: '<div></div>', templateUrl: 'directive.html', replace: false, transclude: false, restrict: 'A', scope: false, compile: function compile(tElement, tAttrs, transclude) { return { pre: function preLink(scope, iElement, iAttrs, controller) { ... }, post: function postLink(scope, iElement, iAttrs, controller) { ... } } }, link: function postLink(scope, iElement, iAttrs) { ... } }; return directiveDefinitionObject; });
在大多数场景下,咱们并不须要精确控制,因此上面的定义是能够化简的。定义模版中的每一部分,将在下面章节讲解。在这个章节,咱们仅仅关注定义模版的异构体(isomers of this skeleton,没看懂。。。期待你们补充)。
简化代码的第一步是依赖默认值。所以,上面的代码能够简化为:
var myModule = angular.module(...); myModule.directive('directiveName', function factory(injectables) { var directiveDefinitionObject = { compile: function compile(tElement, tAttrs) { return function postLink(scope, iElement, iAttrs) { ... } } }; return directiveDefinitionObject; });
大多数directive只关心实例,而不是模版转换,因此能够进一步化简(翻译得很勉强。。。期待你们补充):
var myModule = angular.module(...); myModule.directive('directiveName', function factory(injectables) { return function postLink(scope, iElement, iAttrs) { ... } });
工厂方法负责建立directive。它仅仅使用一次,就在compiler第一次匹配到directive的时候。你能够在这里执行一些初始化操做。工厂方法经过$injector.invoke执行,让它遵照全部注入声明规则(rules of injection annotation),让其变为可注入的。
directive定义对象提供了compiler的结构。属性以下:
name - 当前scope的名称,注册时可使用默认值(不填)。
priority(优先级)- 当有多个directive定义在同一个DOM元素时,有时须要明确它们的执行顺序。这属性用于在directive的compile function调用以前进行排序。若是优先级相同,则执行顺序是不肯定的(经初步试验,优先级高的先执行,同级时按照相似栈的“后绑定先执行”。另外,测试时有点不当心,在定义directive的时候,两次定义了一个相同名称的directive,但执行结果发现,两个compile或者link function都会执行)。
terminal(最后一组)- 若是设置为”true”,则表示当前的priority将会成为最后一组执行的directive。任何directive与当前的优先级相同的话,他们依然会执行,但顺序是不肯定的(虽然顺序不肯定,但基本上与priority的顺序一致。当前优先级执行完毕后,更低优先级的将不会再执行)。
scope - 若是设置为:
true - 将为这个directive建立一个新的scope。若是在同一个元素中有多个directive须要新的scope的话,它仍是只会建立一个scope。新的做用域规则不适用于根模版(root of the template),所以根模版每每会得到一个新的scope。
{}(object hash) - 将建立一个新的、独立(isolate)的scope。”isolate” scope与通常的scope的区别在于它不是经过原型继承于父scope的。这对于建立可复用的组件是颇有帮助的,能够有效防止读取或者修改父级scope的数据。这个独立的scope会建立一个拥有一组来源于父scope的本地scope属性(local scope properties)的object hash。这些local properties对于为模版建立值的别名颇有帮助(useful for aliasing values for templates --!)。本地的定义是对其来源的一组本地scope property的hash映射(Locals definition is a hash of local scope property to its source #&)$&@#)($&@#):
@或@attr - 创建一个local scope property到DOM属性的绑定。由于属性值老是String类型,因此这个值老是返回一个字符串。若是没有经过@attr指定属性名称,那么本地名称将与DOM属性的名称一直。例如
=或=expression(这里也许是attr) - 在本地scope属性与parent scope属性之间设置双向的绑定。若是没有指定attr名称,那么本地名称将与属性名称一致。例如
&或&attr - 提供一个在父scope上下文中执行一个表达式的途径。若是没有指定attr的名称,那么local name将与属性名称一致。例如
controller - controller 构造函数。controller会在pre-linking步骤以前进行初始化,并容许其余directive经过指定名称的require进行共享(看下面的require属性)。这将容许directive之间相互沟通,加强相互之间的行为。controller默认注入了如下本地对象:
$scope - 与当前元素结合的scope
$element - 当前的元素
$attrs - 当前元素的属性对象
$transclude - 一个预先绑定到当前转置scope的转置linking function :function(cloneLinkingFn)。(A transclude linking function pre-bound to the correct transclusion scope)
require - 请求另外的controller,传入当前directive的linking function中。require须要传入一个directive controller的名称。若是找不到这个名称对应的controller,那么将会抛出一个error。名称能够加入如下前缀:
? - 不要抛出异常。这使这个依赖变为一个可选项。
^ - 容许查找父元素的controller
restrict - EACM的子集的字符串,它限制directive为指定的声明方式。若是省略的话,directive将仅仅容许经过属性声明:
E - 元素名称:
A - 属性名:
C - class名:
M - 注释 :
template - 若是replace 为true,则将模版内容替换当前的HTML元素,并将原来元素的属性、class一并迁移;若是为false,则将模版元素看成当前元素的子元素处理。想了解更多的话,请查看“Creating Widgets”章节(在哪啊。。。Creating Components就有。。。)
templateUrl - 与template基本一致,但模版经过指定的url进行加载。由于模版加载是异步的,因此compilation、linking都会暂停,等待加载完毕后再执行。
replace - 若是设置为true,那么模版将会替换当前元素,而不是做为子元素添加到当前元素中。(注:为true时,模版必须有一个根节点)
transclude - 编译元素的内容,使它可以被directive所用。须要(在模版中)配合ngTransclude使用(引用)。transclusion的优势是linking function可以获得一个预先与当前scope绑定的transclusion function。通常地,创建一个widget,建立isolate scope,transclusion不是子级的,而是isolate scope的兄弟。这将使得widget拥有私有的状态,transclusion会被绑定到父级(pre-isolate)scope中。(上面那段话没看懂。但实际实验中,若是经过
true - 转换这个directive的内容。(这个感受上,是直接将内容编译后搬入指定地方)
‘element’ - 转换整个元素,包括其余优先级较低的directive。(像将总体内容编译后,看成一个总体(外面再包裹p),插入到指定地方)
compile - 这里是compile function,将在下面章节详细讲解
link - 这里是link function ,将在下面章节详细讲解。这个属性仅仅是在compile属性没有定义的状况下使用。
function compile ( tElement, tAttrs, transclude) { … }
compile function用于处理DOM模版的转换。因为大多数directive都不须要转换模版,因此compile不会常常被使用到。须要compile function的directive,通常是那种须要转换DOM模版的(如ngRepeat),或者是须要异步加载内容的(如ngView)。compile function拥有一下的参数:
tElement - 模版元素 – 使用当前directive的元素。仅仅在当前元素或者当前元素子元素下作模版转换是安全的。
tAttrs - 模版属性 - 标准化的属性,在当前元素声明的,能够在各个directive之间共享。详情请看Attributes章节
transclude – 一个转换用的linking function: function(scope,cloneLinking).
注意:若是template被克隆过,那么template实例和link实例不能是同一个object。为此,在compile function中作任何除了DOM转换之外的事情,是不安全的,这将应用到全部克隆中。特别是,DOM事件监听器的注册的操做,应该在linking function中进行,而不是compile function。
compile function 能够有一个返回值,类型能够是function或者object。
返回function – 一般在不须要compile function(为空)时使用,等同于经过link(directive定义模版的属性)注册linking function。
返回包含pre、post属性的object - 容许咱们控制在linking phase期间什么时候调用linking function。详情请看下面关于pre-linking、post-linking function的章节。
function link( scope, iElement, iAttrs, controller) { … }
link function负责注册DOM事件监听器,也能够进行DOM的更新操做。link function会在模版克隆操做完毕以后执行。这里存放着directive大多数的逻辑。
Pre-link function
在子元素linked以前执行。在这里作DOM转换是不安全的,由于compiler的linking function在link时可能会定位不到正确的元素。
Post-link function
在子元素linked以后执行。在这里执行DOM转换是安全的。
attribute object - 在link()或compile()中做为参数 - 是一个访问如下东东的途径:
<!DOCTYPE HTML> <html lang="zh-cn" ng-app="DirectiveProperty"> <head> <meta charset="UTF-8"> <title>directive-attribute-test</title> <style type="text/css"> .ng-cloak { display: none; } </style> </head> <body ng-controller="MyCtrl"> <input type="text" ng-model="name" value="myName"/> <p my-attr="123" directive-p2 attr-dd="{{name}}"></p> <script src="../angular-1.0.1.js" type="text/javascript"></script> <script type="text/javascript"> var app = angular.module("DirectiveProperty", []); app.controller("MyCtrl", function ($scope) { $scope.name = "my little dada"; }); var directiveP2 = app.directive("directiveP2", function () { return { link:function postLink(scope,lEle,lAttr) { console.log("myAttr:" + lAttr.myAttr);//123 console.log("myAttr:" + lAttr.attrDd);//undefinded lAttr.$observe('attrDd', function(value) { console.log('attrDd has changed value to ' + value); //my little dada //除此之外,还可啥办法能够拿到这个值啊。。。¥&()@#&¥(@# }); } }; }); </script> </body> </html>
咱们常常须要一些可重用的组件。下面是一段伪代码,展现一个简单的dialog组件如何可以工做。
< div> <button ng-click="show=true">show</button> <dialog title="Hello {{username}}." visible="show" on-cancel="show = false" on-ok="show = false; doSomething()"> Body goes here: {{username}} is {{title}}. </dialog>
点击“show”按钮将会打开dialog。dialog有一个标题,与数据“username”绑定,还有一段咱们想放置在dialog内部的内容。
下面是一段为了dialog而编写的模版定义:
<div ng-show="show()"> <h3>{{title}}</h3> <div class="body" ng-transclude></div> <div class="footer"> <button ng-click="onOk()">Save changes</button> <button ng-click="onCancel()">Close</button> </div> </div>
这将没法正确渲染,除非咱们对scope作一些特殊处理。
第一个咱们须要解决的问题是,dialog模版指望title会被定义,而在初始化时会与username绑定。此外,按钮须要onOk、onCancel两个function出如今scope里面。这限制了插件的效能(this limits the usefulness of the widget。。。)。为了解决映射问题,经过以下的本地方法(locals,估计在说directive定义模版中的scope)去建立模版期待的本地变量:
scope :{ title: 'bind', // set up title to accept data-binding onOk: 'expression', // create a delegate onOk function onCancel: 'expression', // create a delegate onCancel function show: 'accessor' // create a getter/setter function for visibility. }
在控件scope中建立本地属性,带来了两个问题:
1. isolation(属性隔离?) - 若是用户忘记了在控件模版设置title这个元素属性,那么title将会与祖先scope的属性”title”(若有)绑定。这是不可预知、不可取的。
2. transclusion - transcluded DOM能够查看控件的locals(isolate scope?),locals会覆盖transclusion中真正须要绑定的属性。在咱们的例子中,插件中的title属性破坏了transclusion的title属性。
为了解决这个缺少属性隔离的问题,咱们须要为这个directive定义一个isolated scope。isoloted scope不是经过从child scope(这里为啥是child scope?不是parent scope吗?)原型继承的,因此咱们无需担忧属性冲突问题(做为当前scope的兄弟)。
然而,isolated scope带来了一个新问题:若是transcluded DOM 是控件的isolated scope的一个子元素,那么他将不能与任何东西绑定(if a transcluded DOM is a child of the widget isolated scope then it will not be able to bind to anything)。所以,transcluded scope是原始scope的一个子scope,在控件为本地属性而建立isolated scope以前建立的。transcluded与控件isolated scope属于兄弟节点(scope树中)。
这也许看起来会有点意想不到的复杂,但这样作让控件用户与控件开发者至少带来了惊喜。(解决了问题)
所以,最终的directive定义大体以下:
transclude:true, scope :{ title: 'bind', // set up title to accept data-binding onOk: 'expression', // create a delegate onOk function onCancel: 'expression', // create a delegate onCancel function show: 'accessor' // create a getter/setter function for visibility. //我试过这么整,失败……请继续往下看 }
我尝试把上面的代码拼凑成一个完整的例子。直接复制的话,没法达到预期效果。但稍做修改后,插件能够运行了。
<!DOCTYPE html> <html ng-app="Dialog"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>directive-dialog</title> <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible"> <script src="../angular.js" type="text/javascript"></script> </head> <body> <div ng-controller="MyCtrl"> <button ng-click="show=true">show</button> <dialog title="Hello {{username}}" visible="{{show}}" on-cancel="show=false;" on-ok="show=false;methodInParentScope();"> <!--上面的on-cancel、on-ok,是在directive的isoloate scope中经过&引用的。若是表达式中包含函数,那么须要将函数绑定在parent scope(当前是MyCtrl的scope)中--> Body goes here: username:{{username}} , title:{{title}}. <ul> <!--这里还能够这么玩~names是parent scope的--> <li ng-repeat="name in names">{{name}}</li> </ul> </dialog> </div> <script type="text/javascript"> var myModule = angular.module("Dialog", []); myModule.controller("MyCtrl", function ($scope) { $scope.names = ["name1", "name2", "name3"]; $scope.show = false; $scope.username = "Lcllao"; $scope.title = "parent title"; $scope.methodInParentScope = function() { alert("记住。。scope里面经过&定义的东东,是在父scope中玩的!!。。。"); }; }); myModule.directive('dialog', function factory() { return { priority:100, template:['<div ng-show="visible">', ' <h3>{{title}}</h3>', ' <div class="body" ng-transclude></div>', ' <div class="footer">', ' <button ng-click="onOk()">OK</button>', ' <button ng-click="onCancel()">Close</button>', ' </div>', '</div>'].join(""), replace:false, transclude: true, restrict:'E', scope:{ title:"@",//引用dialog标签title属性的值 onOk:"&",//以wrapper function形式引用dialog标签的on-ok属性的内容 onCancel:"&",//以wrapper function形式引用dialog标签的on-cancel属性的内容 visible:"@"//引用dialog标签visible属性的值 } }; }); </script> </body> </html>
咱们一般会指望经过复杂的DOM结构替换掉directive(所在的元素?目的大概是使directive内部复杂点,看起来牛点@_@)。这让directive成为使用可复用组件的创建应用的一个快捷方式。
下面是一个可复用组件的例子:
<!DOCTYPE html> <html ng-app="ZippyModule"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>ZippyModule</title> <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible"> <style type="text/css"> .zippy { border: 1px solid black; display: inline-block; width: 250px; } .zippy.opened > .title:before { content: '▼ '; } .zippy.opened > .body { display: block; } .zippy.closed > .title:before { content: '► '; } .zippy.closed > .body { display: none; } .zippy > .title { background-color: black; color: white; padding: .1em .3em; cursor: pointer; } .zippy > .body { padding: .1em .3em; } </style> <script src="../angular.js" type="text/javascript"></script> </head> <body> <div ng-controller="MyCtrl"> Title: <input ng-model="title" type="text"><br/> Text: <textarea ng-model="text" ></textarea> <hr/> <div class="zippy" zippy-title="Details: {{title}}...">{{text}}</div> </div> <script type="text/javascript"> var myModule = angular.module("ZippyModule", []); myModule.controller("MyCtrl", function ($scope) { $scope.title = "这里是标题"; $scope.text = "这里是内容哇。。。"; }); myModule.directive('zippy', function () { return { template: '<div>' + ' <div class="title">{{title}}</div>' +//这个title属于当前directive isolate scope的property ' <div class="body" ng-transclude></div>' + //这里的东西,获取的是父scope的property咯 '</div>', replace:true, transclude: true, restrict:'C', scope:{ title:"@zippyTitle"//绑定directive元素身上的zippy-title属性 }, link:function(scope,element,attrs) { var title = angular.element(element.children()[0]), opened = false; title.bind("click", toogle); element.addClass("closed"); function toogle() { opened = !opened; element.removeClass(opened ? "closed" : "opened"); element.addClass(opened ? "opened" : "closed"); } } }; }); </script> </body> </html>