欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-06-12 11:27:32
...

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

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

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

《用AngularJS开发下一代Web应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
        当然,如果需要替换的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应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 

        和第一个例子对比,这个例子的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应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
        这里,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应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
        当创建独立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应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
        下面通过一个具体例子来示范一下各用用法。例如,我们想要创建一个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 angularJSdirective 
点击展开,效果如下:

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

《用AngularJS开发下一代Web应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
        如果觉得应该把把扩展条的标题定义在模板中,而不应该定义在数据模型中,那么你可以在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应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
        作为例子,重新实现一下扩展条的实例,新的实现方式不再借助于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应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
        作为例子,我们来重写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应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 

  • 《用AngularJS开发下一代Web应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
  • 大小: 28.2 KB
  • 《用AngularJS开发下一代Web应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
  • 大小: 43.2 KB
  • 《用AngularJS开发下一代Web应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
  • 大小: 40.4 KB
  • 《用AngularJS开发下一代Web应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
  • 大小: 70.1 KB
  • 《用AngularJS开发下一代Web应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
  • 大小: 47.4 KB
  • 《用AngularJS开发下一代Web应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
  • 大小: 14.5 KB
  • 《用AngularJS开发下一代Web应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
  • 大小: 12.7 KB
  • 《用AngularJS开发下一代Web应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
  • 大小: 18.9 KB
  • 《用AngularJS开发下一代Web应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
  • 大小: 91.6 KB
  • 《用AngularJS开发下一代Web应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
  • 大小: 67.2 KB
  • 《用AngularJS开发下一代Web应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
  • 大小: 60.9 KB
  • 《用AngularJS开发下一代Web应用》指令学习笔记
            
    
    博客分类: AngularJS angularJSdirective 
  • 大小: 20.7 KB
相关标签: angularJS directive