原文链接:http://www.sitepoint.com/practical-guide-angularjs-directives/
在angular程序中directive是很重要的组件。尽管Angularjs内置了很多的指令,但是你也经常需要创建特定功能的指令。这篇文章给大家一个指令的基本概述并且详解怎么使用。
overview
directive引进了新的语法声明。directive是通过绑定在DOM元素上的提供特定的功能,例如,静态的html不知道怎么创建和显示一个datepicker组件,为完成这个新语法功能的实现我们需要定义个directive。directive以某种方式窗机一个元素并且使他的行为类似一个datepicker。随后我们将看到这是如何实现的。
如果你以前开发过angularjs应用程序,不管你意识到与否你应该使用过指令。你可能使用过简单的指令例如ng-model,ng-repeat,ng-show等等。所有的指令都是给元素上绑定特定的行为。例如,ng-repeat复制特定的元素,ng-show 条件成立则显示某些元素。如果你要实现一个元素的拖拽功能那么你统一需要一个指令directive背后的基本思想非常简单,它使HTML真正互动通过将事件监听器或DOM元素转换。
jquery透视
考虑下如何使用jquery创建一个datepicker。首先添加一个input域然后调用jquery的$(element).datePicker()。事实上把输入的值转换成datepicker。当一个开发者检查标签时,ta可以立马就能猜出来这个输入框是什么元素?仅仅只是一个input域或是一个datepicker?你得去看下jquery的源码去确认下啦。angular的方法是使用directive扩展html。因此,datepicker的指令就可以像如下:
<date-picker></date-picker> 或者 <input type="text" date-picker/>
这种方法创建的UI组件不但直观而且清晰。你看到这个元素就会知道它应该是做什么的。
构造自定义指令
angularjs的指令有四种表现形式:
1、新的html元素<date-picker></date-picker>
2、元素的属性<input type="text" date-picker/>
3、class类<input type="text" class="date-picker"/>
4、注释<!--directive:date-picker-->
当然了,我们可以控制让directive怎么出现在html中。现在,让我们看下是怎么在angularjs中定义一个典型的指令。directive像注册controller一样,但是却返回一个包含多个属性的对象来配置directive,下面的代码展示了一个helloworld指令,
var app = angular.module('myapp', []); app.directive('helloWorld', function() { return { restrict: 'AE', replace: 'true', template: '<h3>Hello World!!</h3>' }; });
在上面的代码,使用app.directive()在module里注册一个新指令,第一个参数是指令名字,第二个参数是一个function,并返回一个对象。如果directive有额外的对象或者service依赖,比如说$rootScope,$http,或者$compile,他们可以被注入。指令可以在HTML这样使用:<hello-world/> 或者 <hello:world/>,或者作为属性使用如下:<div hello-world></div>,<div hello:world/>。如果要遵从html5的特性,可以使用x-或者data-给属性加上前缀。所以,下面的标签会匹配到helloworld指令<div data-hello-world></div>或者<div x-hello-world></div>
备注:当匹配指令时,angular去掉x-或者data-x的前缀,然后转换-或者:分隔字符串为骆驼命名法并且去匹配注册的指令,这就是因为我们需要在指令使用helloWorld在html中使用hello-world. 然而,上面的指令不仅仅显示静态文本,我们还有几个有趣的点需要解释。指令定义时我们定义了三个属性,让我们看下每个属性扮演的角色。
restrict:html中怎么使用指令。在上面的例子中,我们设置的是'A(Attribute)E(Element)',因此指令可以在新html元素中或是属性。如果在class类中使用可以新增一个'C'。
template : 当指令compile和linked时,特定的html片段会被应用。这里没有要求必须是简单的字符串,template可以是复杂的可以包含其他的指令,表达式等等。在大多数的场景中我们需要templateUrl代替template。因此,理想情况下我们应该定义html模板到单独html文件,在templateUrl指向html模板。
replace:该配置项使template替换directive依附的html片段。在外面的例子中我们只用<hello-world></hello-world>并且replace 为true。因此,directive编译后,template选项中的内容会替换<hello-world></hello-world>,最终输出<h3>Hello World!!</h3>如果设置成false(默认值)当指令执行时template中会被插入到元素中。
link方法和Scope
directive的编译如果没有对应的scope那么指令是没有什么意义的。directive默认不会声明一个新的子scope,当然,新directive可以访问父的scope。这就意味着在controller中定义了一个directive将会使用这个controller的scope。
为了使用scope,我们可以使用一个叫做link的方法,可以在定义directive时返回该link。现在让我们来修改下我们的helloworld指令当我们输入一个颜色值的时候,'helloWorld'字的背景色就会自动改变,当我们点击文字的时候颜色恢复成白色 ,代码如下:
<body ng-controller="MainCtrl"> <input type="text" ng-model="color" placeholder="Enter a color" /> <hello-world/> </body>
修改后的指令如下:
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:传入directive的scope,在该例子中等同于其父级controller的scope
elem:jqLite(jquery的子集)包装后的元素。如果引用了jquery,将会变成jquery的包装类。因为该元素已经用jquery/jqlite,所以无需再使用$()包装来操作DOM。
attrs: 代表element元素声明的属性,例如,你可以在html中这么声明<hello-world some-attribute></hello-world>并且在link方法中可以使用attrs.someAttribute进行访问。
compile方法
在link方法执行前,我们使用compile方法实现DOM转换 ,接受两个参数:
tElement : 指令作用的元素
attrs : 元素中声明的属性
注意:compile方法不能访问scope属性,但是必须返回一个链接函数。但是,如果没有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指令,会clone和赋值多次DOM元素,在link方法执行前使用compile方法。问题来了,为什么要使两个方法分离?为什么我们只需要一个方法?
要回答这些问题我们必须了解angularjs是怎么编译的指令的?
指令怎么编译
当应用程序启动时,angular开始使用$compile解析DOM,这项服务搜索的标记指令和对注册指令进行匹配,一旦所有的指令别识别,angular执行他们的compile方法,向前面提到的,compile返回一个链接函数,这个链接方法会被添加到连接方法的list中,list方法中链接方法稍后会运行,这个叫做编译阶段。如果指令需要实现clone多次(ng-repeat),我们可以让compile方法执行一次来获得性能的提升,但是link方法每次clone都要执行一次,这就是为什么compile方法不会接受scope参数了。
经过编译阶段就是链接阶段,link方法串行执行,这是由指令生成的模板进行评估对正确的Scope,都变成实时的DOM的事件作出反应。
改变指令的scope
默认情况下directive获得父的scope。但是这种情况我们不想使用所有的场景。如果我们暴露父controller的scope到directive中,就能随意的改变scope中的属性。有些场景中,directive有可能给scope中增加属性和方法为了内部使用。如果我们在父scope中执行某些操作,则被污染了,
所以,我们可以有两个选择:
(1)子scope:这个scope原型继承它的父scope
(2)独立scope:不继承父scope创建一个新的scope
在指令定义是指定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>' }; });
上面的代码要求angular创建新的从父作用域原型继承来的子作用域。还一个选择就是独立的scope,代码如下:
app.directive('helloWorld', function() { return { scope: {}, // use a new isolated scope restrict: 'AE', replace: 'true', template: '<h3>Hello World!!</h3>' }; });