首页 > 代码库 > Angular-自定义指令-下

Angular-自定义指令-下

自定义指令学习有段时间了,学了些纸上谈兵的东西,还没有真正的写个指令出来呢。。。所以,随着学习的接近尾声,本篇除了介绍剩余的几个参数外,还将动手结合使用各参数,写个真正能用的指令出来玩玩。

  我们在自定义指令(上)中,写了一个简单的<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,代码如下:

技术分享 View Code

  需要解释的只有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的指令机制等。所以学与用的转变还需要实践的检验。

  撰写博客使我的学习进度变的异常缓慢,要加油了!

Angular-自定义指令-下