《用AngularJS开发下一代Web应用》指令学习笔记

一.指令的作用:实现语义化标签

我们常用的HTML标签是这样的:

<div>
    <span>目录</span>
</div>

而使用AngularJS的directive(指令)机制,我们可以实现这样的东西

<tabset>
    <tab title='Home'>
        <p>Welcome home!</p>
    </tab>
    <tab title='Preferences'>
         <p>Content</p>
    </tab>
<tabset>

和JSP或者Struts等等框架里面的taglib功能一样,只不过这里是使用JavaScript来实现的。

二.简单实例

DirectiveStudy01.html

<html ng-app='app'>
    <body>
        <hello></hello>
    </body>
    <script src="lib/angular/angular.js"></script>
    <script src="directive/HelloDirect.js"></script>
</html>

HelloDirect.js

var appModule = angular.module('app', []);
appModule.directive('hello', function() {
    return {
        restrict: 'E',
        template: '<div>Hi there</div>',
        replace: true
    };
});

运行结果:

《用AngularJS开发下一代Web应用》指令学习笔记
        对于DirectiveStudy01.html里面的<hello>标签,浏览器显然是不认识的,它唯一能做的事情就是无视这个标签。那么,为了让浏览器能够认识这个标签,我们需要使用Angular来定义一个hello指令(本质上说就是自己来把<hello>替换成浏览器能识别的那些标准HTML标签)。

        从运行结果可以看到,<hello>已经被<div>Hi there</div>这个标签替换掉了,这也是以上JS代码里面replace:true这行配置的作用,代码里面的template配置项当然就是我们要的div标签啦,至于restrict:'E'这个配置项的含义,请看下表:

《用AngularJS开发下一代Web应用》指令学习笔记
        当然,如果需要替换的HTML标签很长,显然不能用拼接字符串的方式来写,这时候我们可以用templateUrl来替代template,从而可以把模板写到一个独立的HTML文件中。

三.transclude(变换)

        可以通过transclude属性将内容插入新的模板。当transcluden属性设置为true时,指令会删除原来的内容,使你的模板可以用ng-transclude指令进行重新插入。

DirectiveStudy02.html

<html ng-app='app'>
    <body>
        <div hello>Bob</div>
    </body>
    <script src="lib/angular/angular.js"></script>
    <script src="directive/HelloDirect02.js"></script>
</html>
HelloDirect02.js
var appModule = angular.module('app', []);
appModule.directive('hello', function() {
    return {
        template: '<div>Hi there <span ng-transclude></span></div>',
        transclude: true
    };
});
运行结果:

《用AngularJS开发下一代Web应用》指令学习笔记

        和第一个例子对比,这个例子的JS和HTML代码都略有不同,JS代码里面多了一个transclude: true,HTML代码里面在<hello>内部出现了子标签。

        按照我们在第一个例子中的说法,指令的作用是把我们自定义的语义化标签替换成浏览器能够认识的HTML标签。那好,如果我们自定义的标签内部出现了子标签,应该如何去处理呢?很显然,transclude就是用来处理这种情况的。

        对于当前这个例子,transclude的作用可以简化地理解成:把<hello>标签替换成我们所编写的HTML模板,但是<hello>标签内部的内容保持不变

         很显然,由于我们没有加replace:true选项,所以<hello>标签还在,没有被替换掉。

四.compile和link函数

        虽然插入模板的方式很有用,但是对于指令来说,真正有趣的工作发生在compile和link函数中。

        compile和link这两个函数是根据Angular创建动态视图的两个处理阶段来命名的。从高层来看Angular的初始化过程,它们依次如下:

1.加载脚本

        加载Angular库,并查找ng-app指令,从而找到应用的边界。

2.编译阶段

        在这个阶段,Angular将会遍历DOM结构,标识出模板中注册的所有指令。对于每一条指令,它会根据指令定义的规则(template、replace、transclude等)来转换DOM结构,如果存在compile函数,则调用它。调用compile函数将得到一个编译好的template函数,它将会调用从所有指令中搜集而来的link函数。

3.连接阶段

        为了让视图成为动态的,Angular会对每一条指令运行一个link函数。link函数的一般操作是在DOM或者模型上创建监听器,监听器会使视图和模型的内容随时保持同步。

        总结来说,后两个阶段就负责转换模板的编译阶段,以及修改视图中数据的连接阶段。在以上内容中,指令中的compile和link函数的主要不同点在于,compile函数用来对模板自身进行转换,而link函数负责在模型和视图之间进行动态关联。作用域在连接阶段才会被绑定到编译之后的link函数上,然后再通过数据绑定技术,指令就变成了动态的。

        出于性能方面的考虑,这两个阶段是分开处理的。compile函数仅仅在编译阶段运行一次,而link函数会执行很多次——对于指令的每个实例,link函数都会执行一次。例如,在指令的上一层标签里面使用了ng-repeat,这里你并不想调用compile函数,因为那样会导致对ng-repeat中的每一次遍历都进行一次DOM-walk(DOM遍历)。希望只编译一次,然后再连接。

        当然,你需要知道compile和link函数之间的不同点以及它们各自的功能。对于你将编写的大多数指令来说,并不需要对模板进行转换,所以,大部分情况下只要编写link函数就可以了。

        为了进行比较,再分别看一下compile和link函数的语法。对于compile函数,语法是这样的:

compile: function compile(tElement, tAttrs, transclude) {
    return {
       pre: function preLink(scope, iElement, iAttrs, controller) {...},
       post: function postLink(scope, iElement, iAttrs, controller) {...}
    }
}

        对于link函数,是这样:

link: function postLink(scope, iElement, iAttrs) {...}

        注意这里有一个不同点,即link函数会访问scope(作用域)对象,而compile不会。这是因为,scope对象在编译阶段还不存在。当然你也可以在compile函数中返回link函数,这些link函数仍然可以访问scope对象。

        同时请注意,compile和link都会接收到对应DOM元素的引用以及元素的属性列表。这里的不同点是,compile函数会接收模板元素及其属性列表,所以函数形参带有一个t前缀;而link函数会接收到视图实例对象,视图实例是使用模板创建的,所以函数形参带有一个i前缀。

        只有当一个指令位于其他指令内部,而外部指令会生成模板的多份拷贝时,这种差异性才会显得比较重要。ng-repeat指令就是一个很好的例子。

DirectiveCompileAndLink.html

<html ng-app='app'>
    <body ng-controller='MyController'>
        <div ng-repeat='thing in things'>
            {{thing}}.<hello></hello>
        </div>
    </body>
    <script src="lib/angular/angular.js"></script>
    <script src="directive/CompileAndLink.js"></script>
</html>

CompileAndLink.js

var appModule = angular.module('app', []);
appModule.directive('hello', function() {
    return {
        restrict: 'E',
        template: '<span>Hi there</span>',
        replace: true
    };
});
appModule.controller('MyController',function($scope) {
    $scope.things = [1,2,3,4,5,6];
});

运行结果:

《用AngularJS开发下一代Web应用》指令学习笔记
        这里,compile函数只会被调用一次,而link函数的调用次数等于things中的元素个数——对于{{thing}}.<hello></hello>的每一份拷贝,它都会被调用一次。所以,如果对{{thing}}.<hello></hello>的每一份拷贝(实例)都有一些共同的东西需要修改,那么,出于效率方面的考虑,最好在compile函数里面来做这件事情。

    你还会注意到,compile函数会接受一个transclude函数作为属性。如果你需要对内容进行变换,而简单的基于模板的变换并没有提供这种功能,那么,你可以在这里编写一个函数,使用程序的方式对内容进行变换。

    最后,compile可以返回preLink和postLink函数,而link函数只会返回postLink函数。正如它的名称所表示的,preLink会在编译阶段之后、指令连接到子元素之前运行。类似的,postLink会在所有子元素指令都连接之后才运行。这就意味着,如果你需要修改DOM结构,你应该在postLink中来做这件事情,而如果在preLink中做这件事情会破环绑定过程,并导致错误。

五.扩展条实例Expander

        我们经常需要在指令中访问scope对象,以便观察数据模型的值,当这些值发生变化时刷新UI。当你使用jQuery、Closure或其他类库来封装一些非Angular组件时,或者实现简单的DOM事件时,或者把一个Angular表达式传递给指令的属性然后执行时,都需要访问scope对象。

        如果出于以上某种原因需要一个scope对象,那么scope对象的类型有以下三种选择:

        1.指令对应的DOM元素上存在的scope对象。

        2.可以创建一个新的scope对象,它继承了外层控制器的scope。在继承树中,位于当前scope对象上方的所有scope对象的值都可以被读取。对于DOM元素里面的任何其他指令,如果需要这种类型的scope,也可以共享这个scope,并且可以用它和树中其他scope进行通信。

        3.使用独立的scope对象,它不会从父对象上继承模型的任何属性。当创建可复用的组件,并且需要把当前指令的操作和父scope隔离开时,你就需要使用这个选项。

        可以使用以下配置语法来创建这些指令:

《用AngularJS开发下一代Web应用》指令学习笔记
        当创建独立scope时,默认情况是不可以访问父scope模型中的任何东西。但是,你可以指定把某些属性传递到你的指令中,你可以把这些属性名称看成函数的形参。

        注意,虽然独立的scope不会继承模型的属性,但是它们仍然是父scope的孩子。与所有其他scope一样,它们带有一个指向父scope对象的$parent属性。

        你可以通过传递属性名映射的方式把父scope中指定的属性传递给这个独立的scope。有三种方法可以在scope和父scope之间传递数据,我们把这三种方式叫做“绑定策略”,你还可以选择选择给属性名指定一个局部的别名。

        不使用别名的语法形式为:

scope: {attributeName1: 'BINDING_STRATEGY',
        attributeName2: 'BINDING_STRATEGY',...
        }

        使用别名的形式为:

scope: {attributeAlias: 'BINDING_STRATEGY' + 'templateAttributeName',
       ...
       }

        绑定策略符号定义如下表所示:

《用AngularJS开发下一代Web应用》指令学习笔记
        下面通过一个具体例子来示范一下各用用法。例如,我们想要创建一个expander指令,它会显示一个很小的扩展条,点击的时候扩展条就会展开,显示额外的内容。

DirectiveExpander.html

<html ng-app='expanderModule'>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <script src="lib/angular/angular.js"></script>
        <link rel="stylesheet" type="text/css" href="css/ExpanderSimple.css"/>
    </head>
    <body>
        <div ng-controller='SomeController'>
            <expander class='expander' expander-title='title'>
                {{text}}
            </expander>
        </div>
    </body>
    <script src="directive/ExpanderSimple.js"></script>
</html>

ExpanderSimple.js

var expanderModule=angular.module('expanderModule', [])
expanderModule.directive('expander', function() {
    return {
        restrict : 'EA',
        replace : true,
        transclude : true,
        scope : {
            title : '=expanderTitle'
        },
        template : '<div>'
                 + '<div class="title" ng-click="toggle()">{{title}}</div>'
                 + '<div class="body" ng-show="showMe" ng-transclude></div>'
                 + '</div>',
        link : function(scope, element, attrs) {
            scope.showMe = false;
            scope.toggle = function toggle() {
                scope.showMe = !scope.showMe;
            }
        }
    }
});
expanderModule.controller('SomeController',function($scope) {
    $scope.title = '点击展开';
    $scope.text = '这里是内部的内容。';
});

ExpanderSimple.css

.expander {
    border: 1px solid black;
    width: 250px;
}

.expander>.title {
    background-color: black;
    color: white;
    padding: .1em .3em;
    cursor: pointer;
}

.expander>.body {
    padding: .1em .3em;
}

运行结果:

《用AngularJS开发下一代Web应用》指令学习笔记
点击展开,效果如下:

《用AngularJS开发下一代Web应用》指令学习笔记
        我们先来看一看指令中的每一个选项分别会为我们做一些什么事情,如下所示:

《用AngularJS开发下一代Web应用》指令学习笔记
        如果觉得应该把把扩展条的标题定义在模板中,而不应该定义在数据模型中,那么你可以在scope的定义中使用字符串风格的属性,用@符号作为标志进行传递,示例如下:

scope: {title:'@expanderTitle'},

        在模板中,我们可以使用下面这种方法达到同样的效果:

<expander class='expander' expander-title='Click me to expand'>
    {{text}}
</expander>

        注意,在使用@策略时,我们仍然可以通过双花括号插值语法把title绑定到控制器scope上:

<expander class='expander' expander-title='{{title}}'>
   {{text}}
</expander>

六.扩展条实例Expander——操作DOM元素

        可以向指令的link和compile函数传递iElement或tElement参数,这两个参数都是包装后的引用,它们都指向原始的DOM元素。如果你加载了jQuery库,那么它们就会指向经过jQuery包装之后的元素。

        如果你不使用jQuery,那么这些DOM元素都位于Angular-native包装器jqLite中。jqLite是jQuery API的子集,在Angular中我们需要用它来创建所有东西。对于很多应用来说,只要使用这个轻量级API就可以实现所有你想做的事情了。

        如果你需要直接访问原始的DOM元素,你可以使用element[0]来访问对象中的第一个元素。

        在Angular文档中angular.element()部分,你可以看到目前能够支持的完整API列表,你可以使用angular.element()来创建包装在jqLite中的DOM元素。anuglar对象还带有addClass()、bind()、find()、toggleClass()等函数。当然,这些都是jQuery中常用的核心函数,只是Angular的实现代码更精致而已。

        除了jQuery的API之外,元素还带有一些基于Angular的函数,如下所示。这些函数是否存在与你是否使用完整的jQuery库有关。

《用AngularJS开发下一代Web应用》指令学习笔记
        作为例子,重新实现一下扩展条的实例,新的实现方式不再借助于ng-show和ng-click指令,代码如下:

var expanderModule=angular.module('expanderModule', [])
expanderModule.directive('expander', function() {
    return {
        restrict : 'EA',
        replace : true,
        transclude : true,
        scope : {
            title : '=expanderTitle'
        },
        template : '<div>'
                 + '<div class="title">{{title}}</div>'
                 + '<div class="body closed" ng-transclude></div>'
                 + '</div>',
        link : function(scope, element, attrs) {
            var titleElement = angular.element(element.children().eq(0));
            var bodyElement = angular.element(element.children().eq(1));
            
            titleElement.bind('click', toggle);
            
            function toggle() {
            	bodyElement.toggleClass('closed');
            }
        }
    }
});
expanderModule.controller('SomeController',function($scope) {
    $scope.title = '点击展开';
    $scope.text = '这里是内部的内容。';
});

        从模板中删掉了ng-click和ng-show指令,作为替代,我们给title元素创建了一个jqLite元素,然后把toggle()函数绑定到它的click事件上作为回调,这样就能在用户点击扩展条title的时候执行必要的动作。在toggle()函数中,我们通过调用扩展条body元素上的toggleClass()方法来添加或者删除closed样式类。在toggleClass()函数中我们使用一个CSS样式类把元素设置为display:none,示例如下:

.closed {
    display: none;
}

        PS:jQuery 属性操作-toggleClass() 方法

        toggleClass() 对设置或移除被选元素的一个或多个类进行切换。该方法检查每个元素中指定的类。如果不存在则添加类,如果已设置则删除之。这就是所谓的切换效果。

七.控制器及综合实例

        要实现需要彼此通信的嵌套指令,可以使用控制器。<menu>需要知道内部<menu-item>元素的信息,这样它才能够正确地显示和隐藏它们。同样地,<tab-set>需要知道内部<tab>元素的信息;<grid-view>需要知道内部<grid-element>元素的信息。

        如前所示,为了创建能够在指令之间进行通信的接口,你可以使用controller属性语法把控制器声明成指令的一部分:

controller: function controllerConstructor($scope, $element, $attrs, $transclude)

        controller函数是通过依赖注入的,所以这里所列出的参数列表都是可选的,可以按照其他顺序将其列出,当然这些参数都具有某种潜在的用途。它们还是可用的服务子集。

        通过require属性语法,其他指令可以把这个控制器传递给自已。require的完整形式如下:

require: '^?directiveName'

        require字符串的解释如下表所示:

《用AngularJS开发下一代Web应用》指令学习笔记
        作为例子,我们来重写expander指令,让它可以用在一个"accordion"集合中。它会保证当你打开一个扩展条时,集合中的所有其他扩展条都会自动关闭掉。

Accordion.js

var expModule=angular.module('expanderModule',[])
expModule.directive('accordion', function() {
    return {
        restrict : 'EA',
        replace : true,
        transclude : true,
        template : '<div ng-transclude></div>',
        controller : function() {
            var expanders = [];
            this.gotOpened = function(selectedExpander) {
                angular.forEach(expanders, function(expander) {
                    if (selectedExpander != expander) {
                        expander.showMe = false;
                    }
                });
            }
            this.addExpander = function(expander) {
                expanders.push(expander);
            }
        }
    }
});

expModule.directive('expander', function() {
    return {
        restrict : 'EA',
        replace : true,
        transclude : true,
        require : '^?accordion',
        scope : {
            title : '=expanderTitle'
        },
        template : '<div>'
                   + '<div class="title" ng-click="toggle()">{{title}}</div>'
                   + '<div class="body" ng-show="showMe" ng-transclude></div>'
                   + '</div>',
        link : function(scope, element, attrs, xccordionController) {
            scope.showMe = false;
            xccordionController.addExpander(scope);
            scope.toggle = function toggle() {
                scope.showMe = !scope.showMe;
                xccordionController.gotOpened(scope);
            }
        }
    }
});

expModule.controller("SomeController",function($scope) {
    $scope.expanders = [{
        title : 'Click me to expand',
        text : 'Hi there folks, I am the content that was hidden but is now shown.'
    }, {
        title : 'Click this',
        text : 'I am even better text than you have seen previously'
    }, {
        title : 'No,click me!',
        text : 'I am text that should be seen before seeing other texts'
    }];
});

        accordion指令,它会做一些元素定位工作。我们把控制器的构造函数以及进行元素定位操作的方法添加到accordion指令中。accordion指令中定义了一个addExpander()函数,扩展条可以调用它来注册自身;还创建了一个可被扩展条调用的gotOpened()函数,通过它,accordion的控制器就知道需要把其他所有处于打开状态的扩展条都关闭。

        在expander指令自身中,我们扩展它的时候要求accordion的控制器来自它的父元素,然后 在合适的时候调用addExpander()和gotOpened()函数。

        注意,accordion指令中的控制器创建了一个API接口,有了它,所有扩展条控件之间就可以进行通信了。

        编写模板来使用以上指令如下:

DirectiveAccordionExpander.html

<html ng-app="expanderModule">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <script src="lib/angular/angular.js"></script>
        <link rel="stylesheet" type="text/css" href="css/Accordion.css"/>
    </head>
    <body ng-controller='SomeController' >
        <accordion>
            <expander class='expander' ng-repeat='expander in expanders' expander-title='expander.title'>
                {{expander.text}}
            </expander>
        </accordion>
    </body>
    <script src="directive/Accordion.js"></script>
</html>

Accordion.css

.expander {
    border: 1px solid black;
    width: 250px;
}

.expander>.title {
    background-color: black;
    color: white;
    padding: .1em .3em;
    cursor: pointer;
}

.expander>.body {
    padding: .1em .3em;
}

.closed {
	display: none;
}

运行效果:

《用AngularJS开发下一代Web应用》指令学习笔记

相关推荐