18.8. 指令定义时的参数
指令定义时的参数如下:
name
priority
terminal
scope
controller
require
restrict
template
templateUrl
replace
transclude
compile
link
现在我们开始一个一个地吃掉它们……,但是并不是按顺序讲的。
priority
这个值设置指令的权重,默认是 0 。当一个节点中有多个指令存在时,就按着权限从大到小的顺序依次执行它们的 compile 函数。相同权重顺序不定。
terminal
是否以当前指令的权重为结束界限。如果这值设置为 true ,则节点中权重小于当前指令的其它指令不会被执行。相同权重的会执行。
restrict
指令可以以哪些方式被使用,可以同时定义多种方式。
E 元素方式 <my-directive></my-directive>
A 属性方式 <div my-directive="exp"> </div>
C 类方式 <div class="my-directive: exp;"></div>
M 注释方式 <!-- directive: my-directive exp -->
transclude
前面已经讲过基本的用法了。可以是 ‘element’ 或 true 两种值。
compile
基本的定义函数。 function compile(tElement, tAttrs, transclude) { ... }
link
前面介绍过了。大多数时候我们不需要单独定义它。只有 compile 未定义时 link 才会被尝试。function link(scope, iElement, iAttrs, controller) { ... }
scope
scope 的形式。 false 节点的 scope , true 继承创建一个新的 scope , {}
不继承创建一个新的隔离 scope 。 {@attr: '引用节点属性', =attr: '把节点属性值引用成scope属性值', &attr: '把节点属性值包装成函数'}
controller
为指令定义一个 controller , function controller($scope, $element, $attrs, $transclude) { ... }
name
指令的 controller 的名字,方便其它指令引用。
require
要引用的其它指令 conroller 的名字, ?name
忽略不存在的错误, ^name
在父级查找。
template
模板内容。
templateUrl
从指定地址获取模板内容。
replace
是否使用模板内容替换掉整个节点, true 替换整个节点, false 替换节点内容。
1 | <a b></a> |
上面几个参数值都是比较简单且容易理想的。
再看 scope 这个参数:
1 | <div ng-controller="TestCtrl"> |
对于 scope :
默认为 false , link 函数接受的 scope 为节点所在的 scope 。
为 true 时,则 link 函数中第一个参数(还有 controller 参数中的 $scope
), scope 是节点所在的 scope 的 child scope ,并且如果节点中有多个指令,则只要其中一个指令是 true 的设置,其它所有指令都会受影响。
这个参数还有其它取值。当其为 {}
时,则 link 接受一个完全隔离(isolate)的 scope ,于 true 的区别就是不会继承其它 scope 的属性。但是这时,这个 scope 的属性却可以有很灵活的定义方式:
@attr
引用节点的属性。
1 | <div ng-controller="TestCtrl"> |
@abc
引用 div 节点的 abc 属性。@xx
引用 div 节点的 xx 属性,而 xx 属性又是一个变量绑定,于是 scope 中 b 属性值就和 TestCtrl 的 a 变量绑定在一起了。@
没有写 attr name ,则默认取自己的值,这里是取 div 的 c 属性。=attr
相似,只是它把节点的属性值当成节点 scope 的属性名来使用,作用相当于上面例子中的 @xx
:
1 | <div ng-controller="TestCtrl"> |
&attr
是包装一个函数出来,这个函数以节点所在的 scope 为上下文。来看一个很爽的例子:
1 | <div ng-controller="TestCtrl"> |
scope.a
是 &abc
,即:
scope.a = function(){here = here + 1}
只是其中的 here 是 TestCtrl 的。
scope.b
是 &ngClick
,即:
scope.b = function(){show(here)}
这里的 show() 和 here 都是 TestCtrl 的,于是上面的代码最开始会在终端输出一个 124 。
当点击“这里”时,这时执行的 show(here)
就是 llink 中定义的那个函数了,与 TestCtrl 无关。但是,其间的 scope.a({here:5})
,因为 a 执行时是 TestCtrl 的上下文,于是向 a 传递的一个对象,里面的所有属性 TestCtrl 就全收下了,接着执行 here=here+1 ,于是我们会在屏幕上看到 6 。
这里是一个上下文交错的环境,通过 &
这种机制,让指令的 scope 与节点的 scope 发生了互动。真是鬼斧神工的设计。而实现它,只用了几行代码:
1 | case '&': { |
再看 controller 这个参数。这个参数的作用是提供一个 controller 的构造函数,它会在 compile 函数之后, link 函数之前被执行。
1 | <a>haha</a> |
controller 的最后一个参数, $transclude
,是一个只接受 cloneAttachFn 作为参数的一个函数。
按官方的说法,这个机制的设计目的是为了让各个指令之间可以互相通信。参考普通节点的处理方式,这里也是处理指令 scope 的合适位置。
1 | <a b>kk</a> |
name 参数在这里可以用以为 controller 重起一个名字,以方便在 require 参数中引用。
require 参数可以带两种前缀(可以同时使用):
?
,如果指定的 controller 不存在,则忽略错误。即:require: '?not_b'
如果名为 not_b 的 controller 不存在时,不会直接抛出错误, link 函数中对应的 $controller
为 undefined
。^
,同时在父级节点中寻找指定的 controller ,把上面的例子小改一下:<a><b>kk</b></a>
把 a 的 require 改成(否则就找不到 not_a 这个 controller ):require: '?^not_a'
还剩下几个模板参数:
template 模板内容,这个内容会根据 replace 参数的设置替换节点或只替换节点内容。
templateUrl 模板内容,获取方式是异步请求。
replace 设置如何处理模板内容。为 true 时为替换掉指令节点,否则只替换到节点内容。
1 | <div ng-controller="TestCtrl"> |
template 中可以包括变量引用的表达式,其 scope 遵寻 scope 参数的作用(可能受继承关系影响)。
templateUrl 是异步请求模板内容,并且是获取到内容之后才开始执行指令的 compile 函数。
最后说一个 compile 这个参数。它除了可以返回一个函数用为 link 函数之外,还可以返回一个对象,这个对象能包括两个成员,一个 pre ,一个 post 。实际上, link 函数是由两部分组成,所谓的 preLink 和 postLink 。区别在于执行顺序,特别是在指令层级嵌套的结构之下, postLink 是在所有的子级指令 link 完成之后才最后执行的。 compile 如果只返回一个函数,则这个函数被作为 postLink 使用:
1 | <a><b></b></a> |
18.9. Attributes的细节
节点属性被包装之后会传给 compile 和 link 函数。从这个操作中,我们可以得到节点的引用,可以操作节点属性,也可以为节点属性注册侦听事件。
1 | <test a="1" b c="xxx"></test> |
整个 Attributes 对象是比较简单的,它的成员包括了:
$$element
属性所在的节点。$attr
所有的属性值(类型是对象)。$normalize
一个名字标准化的工具函数,可以把 ng-click 变成 ngClick 。$observe
为属性注册侦听器的函数。$set
设置对象属性,及节点属性的工具。
除了上面这些成员,对象的成员还包括所有属性的名字。
先看 $observe
的使用,基本上相当于 $scope
中的 $watch
:
1 | <div ng-controller="TestCtrl"> |
$set
方法的定义是: function(key, value, writeAttr, attrName) { ... }
。
key 对象的成员名。
value 需要设置的值。
writeAttr 是否同时修改 DOM 节点的属性(注意区别“节点”与“对象”),默认为 true 。
attrName 实际的属性名,与“标准化”之后的属性名有区别。
1 | <div ng-controller="TestCtrl"> |
从例子中可以看到,原始的节点属性值对,放到对象中之后,名字一定是“标准化”之后的。但是手动 $set
的新属性,不会自动做标准化处理。
18.10. 预定义的 NgModelController
在前面讲 conroller 参数的时候,提到过可以为指令定义一个 conroller 。官方的实现中,有很多已定义的指令,这些指令当中,有两个已定义的 conroller ,它们是 NgModelController 和 FormController ,对应 ng-model 和 form 这两个指令(可以参照前面的“表单控件”一章)。
在使用中,除了可以通过 $scope
来取得它们的引用之外,也可以在自定义指令中通过 require 参数直接引用,这样就可以在 link 函数中使用 controller 去实现一些功能。
先看 NgModelController 。这东西的作用有两个,一是控制 ViewValue 与 ModelValue 之间的转换关系(你可以实现看到的是一个值,但是存到变量里变成了另外一个值),二是与 FormController 配合做数据校验的相关逻辑。
先看两个应该是最有用的属性:
$formatters
是一个由函数组成的列表,串行执行,作用是把变量值变成显示的值。$parsers
与上面的方向相反,把显示的值变成变量值。
假设我们在变量中要保存一个列表的类型,但是显示的东西只能是字符串,所以这两者之间需要一个转换:
1 | <div ng-controller="TestCtrl"> |
上面在定义 test 这个指令, require 参数指定了 ngModel 。同时因为 DOM 结构, ng-model 是存在的。于是, link 函数中就可以获取到一个 NgModelController 的实例,即代码中的 $ctrl
。
我们添加了需要的过滤函数:
从变量( ModelValue )到显示值( ViewValue )的过程, $formatters
属性,把一个列表变成一个字符串。
从显示值到变量的过程, $parsers
属性,把一个字符串变成一个列表。
对于显示值和变量,还有其它的 API ,这里就不细说了。
另一部分,是关于数据校验的,放到下一章同 FormController 一起讨论。
18.11. 预定义的 FormController
前面的“表单控制”那章,实际上讲的就是 FormController ,只是那里是从 scope 中获取到的引用。现在从指令定义的角度,来更清楚地了解 FormController 及 NgModelController 是如何配合工作的。
先说一下, form 和 ngForm 是官方定义的两个指令,但是它们其实是同一个东西。前者只允许以标签形式使用,而后者允许 EAC 的形式。DOM 结构中, form 标签不能嵌套,但是 ng 的指令没有这个限制。不管是 form 还是 ngForm ,它们的 controller 都被命名成了 form 。 所以 require 这个参数不要写错了。
FormController 的几个成员是很好理解的:
$pristine
表单是否被动过$dirty
表单是否没被动过$valid
表单是否检验通过$invalid
表单是否检验未通过$error
表单中的错误$setDirty()
直接设置 $dirty
及 $pristine
1 | <div ng-controller="TestCtrl"> |
$error
这个属性,是一个对象, key 是错误名, value 部分是一个列表,其成员是对应的 NgModelController 的实例。
FormController 可以自由增减它包含的那些,类似于 NgModelController 的实例。在 DOM 结构上,有 ng-model 的 input 节点的 NgMoelController 会被自动添加。
$addControl()
添加一个 conroller$removeControl()
删除一个 controller
这两个手动使用机会应该不会很多。被添加的实例也可以手动实现所有的 NgModelController 的方法
1 | <div ng-controller="TestCtrl"> |
整合 FormController 和 NgModelController 就很容易扩展各种类型的字段:
1 | <div ng-controller="TestCtrl"> |
虽然官方原来定义了几种 type ,但这不妨碍我们继续扩展新的类型。如果新的 type 参数值不在官方的定义列表里,那会按 text 类型先做处理,这其实什么影响都没有。剩下的,就是写我们自己的验证逻辑就行了。
上面的代码是参见官方的做法,使用格式化的过程,同时在里面做有效性检查。
18.12. 示例:文本框
这个例子与官网上的那个例子相似。最终是要显示一个文本框,这个文本框由标题和内容两部分组成。而且标题和内容则是引用 controller 中的变量值。
HTML 部分的代码:
1 | <div ng-controller="TestCtrl"> |
从这个期望实现效果的 HTML 代码中,我们可以考虑设计指令的实现方式:
这个指令的使用方式是“标签”, 即 restrict 这个参数应该设置为 E 。
节点的属性值是对 controller 变量的引用,那么我们应该在指令的 scope 中使用 = 的方式来指定成员值。
最终的效果显示需要进行 DOM 结构的重构,那直接使用 template 就好了。
自定义的标签在最终效果中是多余的,所有 replace 应该设置为 true 。
JS 部分的代码:
1 | var app = angular.module('Demo', [], angular.noop); |
可以看到,这种简单的组件式指令,只需要作 DOM 结构的变换即可实现,连 compile 函数都不需要写。
18.13. 示例:模板控制语句 for
这个示例尝试实现一个重复语句,功能同官方的 ngRepeat ,但是使用方式类似于我们通常编程语言中的 for 语句:
1 | <div ng-controller="TestCtrl" ng-init="obj_list=[1,2,3,4]; name='name'"> |
同样,我们从上面的使用方式去考虑这个指令的实现:
这是一个完全的控制指令,所以单个节点应该只有它一个指令起作用就好了,于是权重要比较高,并且“到此为止”—— priority 设置为 1000 , terminal 设置为 true 。
使用时的语法问题。事实上浏览器会把 for 节点补充成一个正确的 HTML 结构,即里面的属性都会变成类似 o=”” 这样。我们通过节点的 outerHTML 属性取到字符串并解析取得需要的信息。
我们把 for 节点之间的内容作为一个模板,并且通过循环多次渲染该模板之后把结果填充到合适的位置。
在处理上面的那个模板时,需要不断地创建新 scope 的,并且 o 这个成员需要单独赋值。
注意:这里只是简单实现功能。官方的那个 ngRepeat 比较复杂,是做了专门的算法优化的。当然,这里的实现也可以是简单把 DOM 结构变成使用 ngRepeat 的形式 :)
JS 部分代码:
1 | var app = angular.module('Demo', [], angular.noop); |
18.14. 示例:模板控制语句 if/else
这个示例是尝试实现:
1 | <div ng-controller="TestCtrl"> |
考虑实现的思路:
else 与 if 是两个指令,它们是父子关系。通过 scope 可以联系起来。至于 scope 是在 link 中处理还是 controller 中处理并不重要。
true 属性的条件判断通过 $parse 服务很容易实现。
如果最终效果要去掉 if 节点,我们可以使用注释节点来“占位”。
JS 代码:
1 | var app = angular.module('Demo', [], angular.noop); |
代码中注意一点,就是 if_node 在得到之时,就已经是做了变量绑定的了。错误的思路是,在 $watch
中再去不断地得到新的 if_node 。
完!
关注 web翎云阁,定时推送,互动精彩多,若你有更好的见解,欢迎留言探讨!