angular.module('myApp', [])
.directive('myDirective', function() {
return {
restrict: String,//字符串,标示
priority: Number,
terminal: Boolean,
template: String or Template Function:
function(tElement, tAttrs) (...},
templateUrl: String,
replace: Boolean or String,
scope: Boolean or Object,
transclude: Boolean,
controller: String or
function(scope, element, attrs, transclude, otherInjectables) { ... },
controllerAs: String,
require: String,
link: function(scope, iElement, iAttrs) { ... },
compile: // 返回一个对象或连接函数,如下所示:
function(tElement, tAttrs, transclude) {
return {
pre: function(scope, iElement, iAttrs, controller) { ... },
post: function(scope, iElement, iAttrs, controller) { ... }
}
// 或者
return function postLink(...) { ... }
}
};
});
restrict 是一个可选的参数。它告诉AngularJS这个指令在DOM中可以何种形式被声明。默认AngularJS认为 restrict 的值是 A ,即以属性的形式来进行声明。
可选值如下:
E(元素)
<my-directive></my-directive>
A(属性,默认值)
<div my-directive="expression"></div>
C(类名)
<div class="my-directive:expression;"></div>
M(注释)
<--directive:my-directive expression-->
这些选项可以单独使用,也可以混合在一起使用:
angular.module('myDirective', function(){
return {
restrict: 'EA' // 输入元素或属性
};
});
上面的配置可以同时用属性或注释的方式来声明指令:
<-- 作为一个属性 -->
<div my-directive></div>
<-- 或者作为一个元素 -->
<my-directive></my-directive>
属性是用来声明指令最常用的方式,因为它能在包括老版本的IE浏览器在内的所有浏览器中正常工作,并且不需要在文档头部注册新的标签。
优先级参数可以被设置为一个数值。大多数指令会忽略这个参数,使用默认值0,但也有些场景设置高优先级是非常重要甚至是必须的。例如, ngRepeat 将这个参数设置为1000,这样就可以保证在同一元素上,它总是在其他指令之前被调用。
如果一个元素上具有两个优先级相同的指令,声明在前面的那个会被优先调用。如果其中一个的优先级更高,则不管声明的顺序如何都会被优先调用:具有更高优先级的指令总是优先运行。
ngRepeat 是所有内置指令中优先级最高的,它总是在其他指令之前运行。这样设置主要考虑的是性能。在讨论编译参数时会更详细介绍性能相关的内容。
terminal 是一个布尔型参数,可以被设置为 true 或 false 。这个参数用来告诉AngularJS停止运行当前元素上比本指令优先级低的指令。但同当前指令优先级相同的指令还是会被执行。
如果元素上某个指令设置了 terminal 参数并具有较高的优先级,就不要再用其他低优先级的指令对其进行修饰了,因为不会被调用。但是具有相同优先级的指令还是会被继续调用。
使用了 terminal 参数的例子是 ngView 和 ngIf 。 ngIf 的优先级略高于 ngView ,如果 ngIf 的表达式值为 true , ngView 就可以被正常执行,但如果 ngIf 表达式的值为 false ,由于 ngView 的优先级较低就不会被执行。
template 参数是可选的,必须被设置为以下两种形式之一:
1. 一段HTML文本;
2. 一个可以接受两个参数的函数,参数为 tElement 和 tAttrs ,并返回一个代表模板的字符串。 tElement 和 tAttrs 中的 t 代表 template ,是相对于 instance 的。
AngularJS会同处理HTML一样处理模板字符串。模板中可以通过大括号标记来访问作用域,例如 {{ expression }} 。
如果模板字符串中含有多个DOM元素,或者只由一个单独的文本节点构成,那它必须被包含在一个父元素内。换句话说,必须存在一个根DOM元素内,否则会报错:
template: '\
<div> <-- single root element -->\
<a href="http://google.com">Click me</a>\
<h1>When using two elements, wrap them in a parent element</h1>\
</div>\
另外,注意每一行末尾的反斜线,这样AngularJS才能正确解析多行字符串。在实际生产中,更好的选择是使用 templateUrl 参数引用外部模板,因为多行文本阅读和维护起来都是一场噩梦。
模板字符串和 templateURL 中最需要了解的重要功能,是它们如何同作用域链接起来。
templateUrl 是可选的参数,可以是以下类型:
1. 一个代表外部HTML文件路径的字符串;
2. 一个可以接受两个参数的函数,参数为 tElement 和 tAttrs ,并返回一个外部HTML文件路径的字符串。
无论哪种方式,模板的URL都将通过AngularJS内置的安全层,特别是 $getTrustedResourceUrl
, 这样可以保护模板不会被不信任的源加载。
默认情况下,调用指令时会在后台通过Ajax来请求HTML模板文件。有两件事情需要知道。
1. 在本地开发时,需要在后台运行一个本地服务器,用以从文件系统加载HTML模板,否则会导致Cross Origin Request Script(CORS)错误。
2. 模板加载是异步的,意味着编译和链接要暂停,等待模板加载完成。
通过Ajax异步加载大量的模板将严重拖慢一个客户端应用的速度。为了避免延迟,可以在部署应用之前对HTML模板进行缓存。在大多数场景下缓存都是一个非常好的选择,因为AngularJS通过减少请求数量提升了性能。
模板加载后,AngularJS会将它默认缓存到 $templateCache 服务中。在实际生产中,可以提前将模板缓存到一个定义模板的JavaScript文件中,这样就不需要通过XHR来加载模板了。
replace 是一个可选参数,如果设置了这个参数,值必须为 true ,因为默认值为 false 。默认值意味着模板会被当作子元素插入到调用此指令的元素内部,即当该值默认为false时,表示不替换指令标记,而是将内容插入到指令标记中,此时,无论指令标记中是否存在内容,都将被清空。
<div some-directive></div>
.directive('someDirective', function() {
return {
template: '<div>some stuff here<div>'
};
});
调用指令之后的结果如下(这是默认 replace 为 false 时的情况):
<div some-directive>
<div>some stuff here<div>
</div>
如果 replace 被设置为了 true :
.directive('someDirective', function() {
return {
replace: true // 修饰过
template: '<div>some stuff here<div>'
};
});
指令调用后的结果将是:
<div>some stuff here<div>
scope 参数是可选的,可以被设置为 true 或一个对象。默认值是 false 。当 scope 设置为 true 时,会从父作用域继承并创建一个新的作用域对象。
如果一个元素上有多个指令使用了隔离作用域,其中只有一个可以生效。只有指令模板中的根元素可以获得一个新的作用域。因此,对于这些对象来说 scope 默认被设置为 true 。
内置指令 ng-controller 的作用,就是从父级作用域继承并创建一个新的子作用域。它会创建一个新的从父作用域继承而来的子作用域。
用这些新内容更新一下前面的例子:
<div ng-app="myApp" ng-init="someProperty = 'some data'"> <div ng-init="siblingProperty='moredata'"> Inside Div Two: {{ aThirdProperty }} <div ng-init="aThirdProperty = 'data for 3rd property'" ng-controller="SomeController"> Inside Div Three: {{ aThirdProperty }} <div ng-init="aFourthProperty"> Inside Div Four: {{ aThirdProperty }} </div> </div> </div> </div>
如果直接运行这段代码会报错,因为没有在JavaScript中定义所需的控制器,下面就来定义这个控制器:
angular.module('myApp', [])
.controller('SomeController', function($scope) {
// 可以留空,但需要被定义
})
刷新页面,会发现第二个 div 中由于 {{ aTgirdProperty }} 未定义,因此什么都没有输出。第三个 div 显示了设置在继承来的作用域中的 data for a 3rd property
作用域的继承机制是向下而非向上进行的
transclude 是一个可选的参数。如果设置了,其值必须为 true ,它的默认值是 false 。
当开启了之后,就可以在模板中以ng-transclude
的方式替换指令元素中原有的内容。
嵌入有时被认为是一个高级主题,但某些情况下它与我们刚刚学习过的作用域之间会有非常好的配合。使用嵌入也会很好地扩充我们的工具集,特别是在创建可以在团队、项目、AngularJS社区之间共享的HTML代码片段时。
嵌入通常用来创建可复用的组件,典型的例子是模态对话框或导航栏。我们可以将整个模板,包括其中的指令通过嵌入全部传入一个指令中。这样做可以将任意内容和作用域传递给指令。 transclude 参数就是用来实现这个目的的,指令的内部可以访问外部指令的作用域,并且模板也可以访问外部的作用域对象。
为了将作用域传递进去, scope 参数的值必须通过 {} 或 true 设置成隔离作用域。如果没有设置 scope 参数,那么指令内部的作用域将被设置为传入模板的作用域。
只有当你希望创建一个可以包含任意内容的指令时,才使用 transclude: true 。
controller 参数可以是一个字符串或一个函数。当设置为字符串时,会以字符串的值为名字,来查找注册在应用中的控制器的构造函数:
angular.module('myApp', [])
.directive('myDirective', function() {
restrict: 'A', // 始终需要
controller: 'SomeController'
})
// 应用中其他的地方,可以是同一个文件或被index.html包含的另一个文件
angular.module('myApp')
.controller('SomeController', function($scope, $element, $attrs, $transclude) {
// 控制器逻辑放在这里
});
可以在指令内部通过匿名构造函数的方式来定义一个内联的控制器:
angular.module('myApp',[])
.directive('myDirective', function() {
restrict: 'A',
controller:
function($scope, $element, $attrs, $transclude) {
// 控制器逻辑放在这里
}
});
controller
在创建父元素指令时添加,可以在该函数中添加多个方法或属性,在添加后,这些方法和属性都会被实例的对象所集成,而这个实例对象则是子元素指令中link
函数的第4个参数。
也就是说,当在子元素指令中添加了require
属性,并通过属性值指定父元素指令的名称,那么,就可以通过子元素指令中link
函数的第4个参数来访问父元素指令中controller
属性添加的方法,因为这个参数就是父元素指令的实例。
与在子元素指令中访问父元素不同,在父元素中,添加构造函数式,函数中的参数就是志愿书指令中的scope
对象,如下代码:
controller:function(){
this.a=function(childDirective){
//方法a的函数体
}
}
在上述代码中,controller
属性值对应一个构造函数,在函数中,this
代表父元素指令本身,方法a是构造函数中的任意一个方法,在定义这个方法时,形参 childDirective
就是子元素指令中的scope对象,通过这种方式,在父元素中可以很轻易的访问到子元素指令中的scope对象。
我们可以将任意可以被注入的AngularJS服务传递给控制器。例如,如果我们想要将 $log 服务传入控制器,只需简单地将它注入到控制器中,便可以在指令中使用它了。
controllerAs 参数用来设置控制器的别名,可以以此为名来发布控制器,并且作用域可以访问 controllerAs 。这样就可以在视图中引入控制器,甚至无需注入$scope
例如,创建一个 MainController ,然后不要注入 $scope ,如下所示:
angular.module('myApp')
.controller('MainController', function() {
this.name = "Ari";
});
现在,在HTML中无需引用作用域就可以使用 MainController 。
<div ng-appng-controller="MainControllerasmain"> <input type="text" ng-model="main.name" /> <span>{{ main.name }}</span> </div>
这个参数看起来好像没什么大用,但它给了我们可以在路由和指令中创建匿名控制器的强大能力。这种能力可以将动态的对象创建成为控制器,并且这个对象是隔离的、易于测试的。
例如,可以在指令中创建匿名控制器,如下所示:
angular.module('myApp')
.directive('myDirective', function() {
return {
restrict: 'A',
template: '<h4>{{ myController.msg }}</h4>',
controllerAs: 'myController',
controller: function() {
this.msg = "Hello World"
}
};
});
require 参数可以被设置为字符串或数组,字符串代表另外一个指令的名字。 require 会将控制器注入到其值所指定的指令中,并作为当前指令的链接函数(link)的第四个参数。
字符串或数组元素的值是会在当前指令的作用域中使用的指令名称。scope 会影响指令作用域的指向,是一个隔离作用域,一个有依赖的作用域或者完全有作用域。在任何情况下,AngularJS编译器在查找子控制器时都会参考当前指令的模板。
如果不使用 ^ 前缀,指令只会在自身的元素上查找控制器。
restrict: 'EA',
require: 'ngModel'
指令定义只会查找定义在指令作当前用域中的 ng-model=”” 。
require 参数的值可以用下面的前缀进行修饰,这会改变查找控制器时的行为:
?
如果在当前指令中没有找到所需要的控制器,会将 null 作为传给 link 函数的第四个参数。
^
如果添加了 ^ 前缀,指令会在上游的指令链中查找 require 参数所指定的控制器。
?^
将前面两个选项的行为组合起来,我们可选择地加载需要的指令并在父指令链中进行查找。
如果没有前缀,指令将会在自身所提供的控制器中进行查找,如果没有找到任何控制器(或具有指定名字的指令)就抛出一个错误。
compile 选项可以返回一个对象或函数,当返回一个函数时,该函数名为为post
,而返回一个对象时,对象中则包含两个名为pre
和post
的方法函数,这两个函数名是系统提供的,不能修改。
理解 compile 和 link 选项是AngularJS中需要深入讨论的高级话题之一,对于了解AngularJS究竟是如何工作的至关重要。
compile 选项本身并不会被频繁使用,但是 link 函数则会被经常使用。本质上,当我们设置了 link 选项,实际上是创建了一个 postLink() 链接函数,以便 compile() 函数可以定义链接函数。
通常情况下,如果设置了 compile 函数,说明我们希望在指令和实时数据被放到DOM中之前进行DOM操作,在这个函数中进行诸如添加和删除节点等DOM操作是安全的。
compile 和 link 选项是互斥的。如果同时设置了这两个选项,那么会把 compile所返回的函数当作链接函数,而 link 选项本身则会被忽略。
用 link 函数创建可以操作DOM的指令。
链接函数是可选的。如果定义了编译函数,它会返回链接函数,因此当两个函数都定义了时,编译函数会重载链接函数。如果我们的指令很简单,并且不需要额外的设置,可以从工厂函数(回调函数)返回一个函数来代替对象。如果这样做了,这个函数就是链接函数。
link
函数包括3个主要的参数,其中,scope
参数表示指令所在的作用域,它的功能与页面中控制器注入的作用域是相同的,iEle
参数表示指令中的元素,该元素可以通过Angular
内部封装的jqLite
进行调用,jqLite
相当于是一个压缩版的jQuery
,包含了主要的元素操作API
,在语法上与jQuery
类似,iAttrs
参数表示指令元素的属性集合,通过这个参数可以获取元素中的各类属性。
下面两种定义指令的方式在功能上是完全一样的:
angular.module('myApp', [])
.directive('myDirective', function() {
return {
pre: function(tElement, tAttrs, transclude) {
// 在子元素被链接之前执行
// 在这里进行Don转换不安全
// 之后调用'lihk'h函数将无法定位要链接的元素
},
post: function(scope, iElement, iAttrs, controller) {
// 在子元素被链接之后执行
// 如果在这里省略掉编译选项
//在这里执行DOM转换和链接函数一样安全吗
}
};
});
angular.module('myApp', [])
.directive('myDirective', function() {
return {
link: function(scope, ele, attrs) {
return {
pre: function(tElement, tAttrs, transclude) {
// 在子元素被链接之前执行
// 在这里进行Don转换不安全
// 之后调用'lihk'h函数将无法定位要链接的元素
},
post: function(scope, iElement, iAttrs, controller) {
// 在子元素被链接之后执行
// 如果在这里省略掉编译选项
//在这里执行DOM转换和链接函数一样安全吗
}
}
}
});
链接函数的签名如下:
link: function(scope, element, attrs) {
// 在这里操作DOM
}
如果指令定义中有 require 选项,函数签名中会有第四个参数,代表控制器或者所依赖的指令的控制器。
// require 'SomeController',
link: function(scope, element, attrs, SomeController) {
// 在这里操作DOM,可以访问required指定的控制器
}
如果 require 选项提供了一个指令数组,第四个参数会是一个由每个指令所对应的控制器组成的数组。下面看一下链接函数中的参数:
scope
指令用来在其内部注册监听器的作用域
iElement
iElement 参数代表实例元素,指使用此指令的元素。在 postLink 函数中我们应该只操作此元素的子元素,因为子元素已经被链接过了。
iAttrs
iAttrs 参数代表实例属性,是一个由定义在元素上的属性组成的标准化列表,可以在所有指令的链接函数间共享。会以JavaScript对象的形式进行传递。
controller
controller 参数指向 require 选项定义的控制器。如果没有设置 require 选项,那么controller 参数的值为 undefined 。
控制器在所有的指令间共享,因此指令可以将控制器当作通信通道(公共API)。
如果设置了多个 require ,那么这个参数会是一个由控制器实例组成的数组,而不只是一个单独的控制器。
指令的实现:
一共有四种方法:
1. 元素标签
<my-directive></my-directive>
2. 属性
<div my-directive></div>
3. 类
<div class="my-directive"></div>
4. 注释
<!--directive:my-directive-->
其中,我们推荐使用属性方式,因为它有比较好的跨浏览器兼容性
几种指令属性的具体用法,可见另外一篇文章