走进AngularJs(五)自定义指令----(下)
自定义指令学习有段时间了,学了些纸上谈兵的东西,还没有真正的写个指令出来呢。。。所以,随着学习的接近尾声,本篇除了介绍剩余的几个参数外,还将动手结合使用各参数,写个真正能用的指令出来玩玩。
我们在自定义指令(上)中,写了一个简单的<say-hello></say-hello>,能够跟美女打招呼。但是看看人家ng内置的指令,都是这么用的:ng-model=”m”,ng-repeat=”a in array”,不单单是作为属性,还可以赋值给它,与作用域中的一个变量绑定好,内容就可以动态变化了。假如我们的sayHello可以这样用:<say-hello speak=”content”>美女</say-hello>,把要对美女说的话写在一个变量content中,然后只要在controller中修改content的值,页面就可以显示对美女说的不同的话。这样就灵活多了,不至于见了美女只会说一句hello,然后就没有然后了。
为了实现这样的功能,我们需要使用scope参数,下面来介绍一下。
使用scope为指令划分作用域
顾名思义,scope肯定是跟作用域有关的一个参数,它的作用是描述指令与父作用域的关系,这个父作用域是指什么呢?想象一下我们使用指令的场景,页面结构应该是这个样子:
<div ng-controller="testC"> <say-hello speak="content">美女</say-hello> </div>
外层肯定会有一个controller,而在controller的定义中大体是这个样子:
var app = angular.module('MyApp', [], function(){console.log('here')}); app.controller('testC',function($scope){ $scope.content = '今天天气真好!'; });
所谓sayHello的父作用域就是这个名叫testC的控制器所管辖的范围,指令与父作用域的关系可以有如下取值:
取值 |
说明 |
false |
默认值。使用父作用域作为自己的作用域 |
true |
新建一个作用域,该作用域继承父作用域 |
javascript对象 |
与父作用域隔离,并指定可以从父作用域访问的变量 |
乍一看取值为false和true好像没什么区别,因为取值为true时会继承父作用域,即父作用域中的任何变量都可以访问到,效果跟直接使用父作用域差不多。但细细一想还是有区别的,有了自己的作用域后就可以在里面定义自己的东西,与跟父作用域混在一起是有本质上的区别。好比是父亲的钱你想花多少花多少,可你自己挣的钱父亲能花多少就不好说了。你若想看这两个作用域的区别,可以在link函数中打印出来看看,还记得link函数中可以访问到scope吧。
最有用的还是取值为第三种,一个对象,可以用键值来显式的指明要从父作用域中使用属性的方式。当scope值为一个对象时,我们便建立了一个与父层隔离的作用域,不过也不是完全隔离,我们可以手工搭一座桥梁,并放行某些参数。我们要实现对美女说各种话就得靠这个。使用起来像这样:
scope: { attributeName1: 'BINDING_STRATEGY', attributeName2: 'BINDING_STRATEGY',... }
键为属性名称,值为绑定策略。等等!啥叫绑定策略?最讨厌冒新名词却不解释的行为!别急,听我慢慢道来。
先说属性名称吧,你是不是认为这个attributeName1就是父作用域中的某个变量名称?错!其实这个属性名称是指令自己的模板中要使用的一个名称,并不对应父作用域中的变量。好难懂啊。。。可能这么说太不负责任了,稍后的例子中我们来说明。再来看绑定策略,它的取值按照如下的规则:
符号 |
说明 |
举例 |
@ |
传递一个字符串作为属性的值. |
str : ‘@string’ |
= |
使用父作用域中的一个属性,绑定数据到指令的属性中. |
name : ‘=username’ |
& |
使用父作用域中的一个函数,可以在指令中调用 |
getName : ‘&getUserName’ |
总之就是用符号前缀来说明如何为指令传值。你肯定迫不及待要看例子了,我们结合例子看一下,小二,上栗子~
举例说明
我想要实现上面想像的跟美女多说点话的功能,即我们给sayHello指令加一个属性,通过给属性赋值来动态改变说话的内容。主要代码如下:
app.controller('testC',function($scope){ $scope.content = '今天天气真好!'; }); app.directive('sayHello',function(){ return { restrict : 'E', template : '<div>hello,<b ng-transclude></b>,{{cont}}</div>', replace : true, transclude : true, scope : { cont : '=speak' } }; });
然后在模板中,我们如下使用指令:
<div ng-controller="testC"> <say-hello speak="content">美女</say-hello> </div>
看看运行效果:
执行的流程是这样的:
① 指令被编译的时候会扫描到template中的{ {cont} },发现是一个表达式;
② 查找scope中的规则:通过speak与父作用域绑定,方式是传递父作用域中的属性;
③ speak与父作用域中的content属性绑定,找到它的值“今天天气真好!”
④ 将content的值显示在模板中
这样我们说话的内容cont就跟父作用域绑定到了一其,如果动态修改父作用域的content的值,页面上的内容就会跟着改变,正如你点击“换句话”所看到的一样。
这个例子也太小儿科了吧!简单虽简单,但可以让我们理解清楚,为了检验你是不是真的明白了,可以思考一下如何修改指令定义,能让sayHello以如下两种方式使用:
<span say-hello speak="content">美女</span>
<span say-hello="content" >美女</span>
答案我就不说了,简单的很。下面有更重要的事情要做,我们说好了要写一个真正能用的东西来着。接下来就结合所学到的东西来写一个折叠菜单,即点击可展开,再点击一次就收缩回去的菜单,(偷偷告诉你,这个例子其实是从大漠穷秋的书上抄来的~)。
控制器及指令的代码如下:(为了不让文章太长,我后面的代码要折叠起来了,请自行点开)
app.controller('testC',function($scope){ $scope.title = '个人简介'; $scope.text = '大家好,我是一名前端工程师,我正在研究AngularJs,欢迎大家与我交流,Email:lvxiaobao_fbi@163.com'; }); app.directive('expander',function(){ return { restrict : 'E', templateUrl : 'expanderTemp.html', replace : true, transclude : true, scope : { mytitle : '=etitle' }, link : function(scope,element,attris){ scope.showText = false; scope.toggleText = function(){ scope.showText = ! scope.showText; } } }; });
HTML中的代码如下:
<script id="expanderTemp.html" type="text/ng-template"> <div class="mybox"> <div class="mytitle" ng-click="toggleText()"> {{mytitle}} </div> <div ng-transclude ng-show="showText"></div> </div> </script>
看看运行效果:
还是比较容易看懂的,我只做一点必要的解释。首先我们定义模板的时候使用了ng的一种定义方式<script type=”text/ng-template” id="expanderTemp.html">,在指令中就可以用templateUrl根据这个id来找到模板。指令中的{{mytitle}}表达式由scope参数指定从etitle传递,etitle指向了父作用域中的title。为了实现点击标题能够展开收缩内容,我们把这部分逻辑放在了link函数中,link函数可以访问到指令的作用域,我们定义showText属性来表示内容部分的显隐,定义toggleText函数来进行控制,然后在模板中绑定好。 如果把showText和toggleText定义在controller中,作为$scope的属性呢?显然是不行的,这就是隔离作用域的意义所在,父作用域中的东西除了title之外通通被屏蔽。
上面的例子中,scope参数使用了=号来指定获取属性的类型为父作用域的属性,如果我们想在指令中使用父作用域中的函数,使用&符号即可,是同样的原理。
以上是本人对scope的理解,另外有一篇文章对Angular作用域的解释也比较详细,有兴趣可以参考http://www.angularjs.cn/A09C。
使用controller和require进行指令间通信
使用指令来定义一个ui组件是个不错的想法,首先使用起来方便,只需要一个标签或者属性就可以了,其次是可复用性高,通过controller可以动态控制ui组件的内容,而且拥有双向绑定的能力。当我们想做的组件稍微复杂一点,就不是一个指令可以搞定的了,就需要指令与指令的协作才可以完成,这就需要进行指令间通信。
想一下我们进行模块化开发的时候的原理,一个模块暴露(exports)对外的接口,另外一个模块引用(require)它,便可以使用它所提供的服务了。ng的指令间协作也是这个原理,这也正是自定义指令时controller参数和require参数的作用。
controller参数用于定义指令对外提供的接口,它的写法如下:
controller: function controllerConstructor($scope, $element, $attrs, $transclude)
它是一个构造器函数,将来可以构造出一个实例传给引用它的指令。为什么叫controller(控制器)呢?其实就是告诉引用它的指令,你可以控制我。至于可以控制那些东西呢,就需要在函数体中进行定义了。先看controller可以使用的参数,作用域、节点、节点的属性、节点内容的迁移,这些都可以通过依赖注入被传进来,所以你可以根据需要只写要用的参数。关于如何对外暴露接口,我们在下面的例子来说明。
require参数便是用来指明需要依赖的其他指令,它的值是一个字符串,就是所依赖的指令的名字,这样框架就能按照你指定的名字来从对应的指令上面寻找定义好的controller了。不过还稍稍有点特别的地方,为了让框架寻找的时候更轻松些,我们可以在名字前面加个小小的前缀:^,表示从父节点上寻找,使用起来像这样:require : ‘^directiveName’,如果不加,$compile服务只会从节点本身寻找。另外还可以使用前缀:?,此前缀将告诉$compile服务,如果所需的controller没找到,不要抛出异常。
所需要了解的知识点就这些,接下来是例子时间,依旧是从书上抄来的一个例子,我们要做的是一个手风琴菜单,就是多个折叠菜单并列在一起,此例子用来展示指令间的通信再合适不过。
首先我们需要定义外层的一个结构,起名为accordion,代码如下:
app.directive('accordion',function(){ return { restrict : 'E', template : '<div ng-transclude></div>', replace : true, transclude : true, controller :function(){ var expanders = []; this.gotOpended = function(selectedExpander){ angular.forEach(expanders,function(e){ if(selectedExpander != e){ e.showText = false; } }); } this.addExpander = function(e){ expanders.push(e); } } } });
需要解释的只有controller中的代码,我们定义了一个折叠菜单数组expanders,并且通过this关键字来对外暴露接口,提供两个方法。gotOpended接受一个selectExpander参数用来修改数组中对应expander的showText属性值,从而实现对各个子菜单的显隐控制。addExpander方法对外提供向expanders数组增加元素的接口,这样在子菜单的指令中,便可以调用它把自身加入到accordion中。
看一下我们的expander需要做怎样的修改呢:
app.directive('expander',function(){ return { restrict : 'E', templateUrl : 'expanderTemp.html', replace : true, transclude : true, require : '^?accordion', scope : { title : '=etitle' }, link : function(scope,element,attris,accordionController){ scope.showText = false; accordionController.addExpander(scope); scope.toggleText = function(){ scope.showText = ! scope.showText; accordionController.gotOpended(scope); } } }; });
首先使用require参数引入所需的accordion指令,添加?^前缀表示从父节点查找并且失败后不抛出异常。然后便可以在link函数中使用已经注入好的accordionController了,调用addExpander方法将自己的作用域作为参数传入,以供accordionController访问其属性。然后在toggleText方法中,除了要把自己的showText修改以外,还要调用accordionController的gotOpended方法通知父层指令把其他菜单给收缩起来。
指令定义好后,我们就可以使用了,使用起来如下:
<accordion> <expander ng-repeat="expander in expanders" etitle="expander.title">{{expander.text}}</expander> </accordion>
外层使用了accordion指令,内层使用expander指令,并且在expander上用ng-repeat循环输出子菜单。请注意这里遍历的数组expanders可不是accordion中定义的那个expanders,如果你这么认为了,说明还是对作用域不够了解。此expanders是ng-repeat的值,它是在外层controller中的,所以,在testC中,我们需要添加如下数据:
$scope.expanders = [ {title: '个人简介', text: '大家好,我是一名前端工程师,我正在研究AngularJs,欢迎大家与我交流,Email:lvxiaobao_fbi@163.com'}, {title: '我的爱好', text: '运动类:篮球、足球、乒乓球。 电脑类:前端技术、打DOTA。 其他类:欣赏美女'}, {title: '性格及工作', text: '追求完美主义的处女座极品男人就是我啦~严重的代码洁癖以及对垃圾代码的零容忍!希望通过自己的努力进入理想的公司工作。'} ];
这下就都全乎了,试一下我们的accordion组件是不是可以正常使用了呢:
理解了其中的道理之后,使用起来就可以得心应手了,我也将在以后的实践中尝试编写更加复杂的组件,此小例子就当是抛砖引玉了~
总结
又到了总结时间,到此为止自定义指令的学习就告一段落了,但我相信相关的知识肯定远远不止这些,真正要将指令在项目中用好,还需要理解指令与ng的其他机制如何相互作用,还需更加深入的了解ng的指令机制等。所以学与用的转变还需要实践的检验。
撰写博客使我的学习进度变的异常缓慢,要加油了!