《AngularJS》Directive(指令)机制详解

为何使用AngularJS 指令?

使用过 AngularJS 的朋友应该最感兴趣的是它的指令。现今市场上的前端框架也只有AngularJS 拥有自定义指令的功能,而且AngularJS 是目前惟一提供Web应用可复用能力的框架。css

目前有不少JavaScript 产品提供插件给Web开发人员。例如, Bootstrap 就是当前比较流行的提供样式和JavaScript插件的前端开发工具包。可是开发人员在使用Booostrap中的插件时, 必须切换到JavaScript 模式来写 jQuery 代码来激活插件虽然jQuery 代码写起来十分简单,可是必须和HTML进行同步,这是一个单调乏味且容易出错的过程。html

AngularJS主页展现了一个简单的例子,用于实现Bootstrap中的 Tab功能,能够在页面中轻松添加 Tab 功能,而且使用方法和 ul 标签同样简单。HTML代码以下:前端

<body ng-app="components"> 
  <h3>BootStrap Tab Component</h3> 
  <tabs> 
    <pane title="First Tab"> 
      <div>This is the content of the first tab.</div> 
    </pane> 
    <pane title="Second Tab"> 
      <div>This is the content of the second tab.</div> 
    </pane> 
  </tabs> 
</body>
复制代码

 

JavaScript代码以下:jquery

复制代码
angular.module('components', []). 
  directive('tabs', function() { 
    return { 
      restrict: 'E', 
      transclude: true, 
      scope: {}, 
      controller: [ "$scope", function($scope) { 
        var panes = $scope.panes = []; 
  
        $scope.select = function(pane) { 
          angular.forEach(panes, function(pane) { 
            pane.selected = false; 
          }); 
          pane.selected = true; 
        } 
  
        this.addPane = function(pane) { 
          if (panes.length == 0) $scope.select(pane); 
          panes.push(pane); 
        } 
      }], 
      template: 
        '<div class="tabbable">' + 
          '<ul class="nav nav-tabs">' + 
            '<li ng-repeat="pane in panes" ng-class="{active:pane.selected}">'+ 
              '<a href="" ng-click="select(pane)">{{pane.title}}</a>' + 
            '</li>' + 
          '</ul>' + 
          '<div class="tab-content" ng-transclude></div>' + 
        '</div>', 
      replace: true 
    }; 
  }). 
  directive('pane', function() { 
    return { 
      require: '^tabs', 
      restrict: 'E', 
      transclude: true, 
      scope: { title: '@' }, 
      link: function(scope, element, attrs, tabsCtrl) { 
        tabsCtrl.addPane(scope); 
      }, 
      template: 
        '<div class="tab-pane" ng-class="{active: selected}" ng-transclude>' + 
        '</div>', 
      replace: true 
    }; 
  })
复制代码

 

你能够从如下连接查看效果:http://jsfiddle.net/powertoolsteam/GBE7N/1/git

image

正如你所见,除了拥有用于实现指令的 <tabs> 和<pane> 标签,页面和常规HTML页面没有什么区别。HTML开发人员无需编写任何代码。固然,总须要有第一个吃螃蟹的人,建立指令共享使用,可是目前Tabs指令已经很常见了,能够在任何地方复用(如BootStrap,、jQueryUIWijmo, 和一些知名的前端插件集)。angularjs

因为指令的易用和易编写,许多用户已经开始使用AngularJS编写指令了。例如, AngularJS 开发组已经基于AngularJS实现了一系列指令-UI Bootstrap 来代替Bootstrap; 知名ComponentOne 控件厂商也在AngularJS 基础上建立了Wijmo ;咱们也能够在GitHub上找到一些公共指令资料库:jQueryUI widgetsgithub

拥有了 AngularJS,是否是以为本身已经站在了巨人的肩膀上了?可是不要高兴的太早,若是已经有了这么多的指令供咱们使用,那咱们为何还要学习AngularJS ,为何还要学习自定义指令呢?bootstrap

举个简单的例子,也许你有特殊的需求:假设你在一家财务公司工做,你须要建立一张财务表单,它须要以表格的形式展现数据、拥有绑定、编辑、校验而且同步数据更新到服务器的功能。表单插件很常见可是可以知足这些具体需求的不得而知了,因此你必须根据实际业务需求来建立自定义指令。数组

复制代码
<body ng-app="abcFinance"> 
  <h3>Offshore Investment Summary</h3> 
  <abc-investment-form 
    customer="currentCustomer" 
    country="currentCountry"> 
  </abc-investment-form data> 
</body> 
复制代码

 

这就是本篇文章的目的,接下来咱们会讨论如何建立 AngularJS指令。浏览器

 

建立自定义AngularJS 指令

文章开头的自定义指令十分的简单。它仅仅实现了同步的功能。通常指令是包含更多元素的:

复制代码
//建立指令模块 (或者检索现有模块) 
var m = angular.module("myApp");

// 建立"my-dir"指令 
myApp.directive("myDir", function() { 
  return { 
    restrict: "E",        // 指令是一个元素 (并不是属性) 
    scope: {              // 设置指令对于的scope 
      name: "@",          // name 值传递 (字符串,单向绑定) 
      amount: "=",        // amount 引用传递(双向绑定) 
      save: "&"           // 保存操做 
    }, 
    template:             // 替换HTML (使用scope中的变量) 
      "<div>" + 
      "  {{name}}: <input ng-model='amount' />" + 
      "  <button ng-click='save()'>Save</button>" + 
      "</div>", 
    replace: true,        // 使用模板替换原始标记 
    transclude: false,    // 不复制原始HTML内容 
    controller: [ "$scope", function ($scope) { …  }], 
    link: function (scope, element, attrs, controller) {…} 
  } 
});  
复制代码

 

效果以下:

image


注意这个自定义指令遵循一种格式:以"my" 为前缀,相似于命名空间,所以若是你在应用中引用了多个模块指令,你能够经过前缀很容易的判断出它是在哪定义的。这不是硬性要求,可是这样作能够带来不少便利。

指令的构造函数会返回带有属性的JavaScript 对象。这些内容在AngularJS 主页中都有清晰说明。如下是我对一些属性的理解:

  1. restrict: 说明指令在HTML中的应用形式,备选项有"A"、"E" 和 "C", "M" ,分别表明 attribute、element、class和comment(默认值为"A")。咱们将更多的关注attributes-如何建立UI元素。
  2. scope: 建立指令的做用范围,scope在指令中做为属性标签传递。Scope 是建立能够复用指令的必要条件,每一个指令(不管是处于嵌套指令的哪一级)都有其惟一的做用域,它不依赖于父scope。scope 对象定义names 和types 变量。上面的例子即建立了3个scope变量。
    • name: "@" (值传递,单向绑定): 
      "@"符号表示变量是值传递。指令会检索从父级scope中传递而来字符串中的值。指令可使用该值但没法修改,是最经常使用的变量。
    • amount: "=" (引用,双向绑定) 
      "="符号表示变量是引用传递。指令检索主Scope中的引用取值。值能够是任意类型的,包括复合对象和数组。指令能够更改父级Scope中的值,因此当指令须要修改父级Scope中的值时咱们就须要使用这种类型。
    • save: "&" (表达式) 
      “&”符号表示变量是在父级Scope中启做用的表达式。它容许指令实现比修改值更高级的操做。
  3. template: 替代原始模板中的标记的字符串。替换功能将替换全部旧元素为新值。注意template是如何使用Scope中定义的变量的。这容许你无需写任何额外的代码便可建立macro-style 风格指令。replace: 说明是否替换原始标记中的值或是追加原始标记中的值。默认值是false,这时原始标记将被保留。
  4. transclude: 说明自定义指令是否复制原始标记中的内容。例如,以前展现的“tab”指令设置了transclude 为 true,由于tab 元素包含其余HTML 元素。 "dateInput" 指令则须要在初始化时为空,因此须要设置transclude 为false。
  5. link: 该方法在指令中扮演着重要的角色。它负责执行DOM 操做和注册事件监听器等。link 方法包含如下参数:
    • scope: 指令Scope的引用。scope 变量在初始化时是不被定义的,link 方法会注册监视器监视值变化事件。
    • element: 包含指令的DOM元素的引用, link 方法通常经过jQuery 操做实例(若是没有加载jQuery,还可使用Angular's jqLite )。
    • controller: 在有嵌套指令的状况下使用。这个参数做用在于把子指令的引用提供给父指令,容许指令之间进行交互, tab 指令就是使用该参数较典型的例子:http://jsfiddle.net/powertoolsteam/GBE7N/1/

注意,当调用link 方法时, 经过值传递("@")的scope 变量将不会被初始化,它们将会在指令的生命周期中另外一个时间点进行初始化,若是你须要监听这个事件,可使用scope.$watch 方法。 

AngularJS 指令实例讲解

1.一点小说明

 

指令的做用:实现语义化标签

 

咱们经常使用的HTML标签是这样的:

 

<div>
    <span>一点点内容</span>
</div>

 

而使用AngularJS的directive(指令)机制,咱们能够实现这样的东西:

 

<tabpanel>
    <panel>子面板1</panel>
    <panel>子面板2</panel>
</tabpanel>

 

不少人可能要惊呼,这货和JSP或者Struts等等框架里面的taglib很像啊!

 

呃,说实话,实际上就是这样的,只不过这里是使用JavaScript来实现的。正由于如此,因此不少taglib作不到的功能,使用它就均可以作到,好比访问N层scope里面的对象之类的事情(参见后面第5个例子)。

 

2.实例1:从最简单的开始

 

<html ng-app='app'>
    <body>
        <hello></hello>
    </body>

    <script src="../angular-1.0.3/angular.min.js"></script>
    <script src="HelloDirect.js"></script>
</html>

 

对于以上代码里面的<hello>标签,浏览器显然是不认识的,它惟一能作的事情就是无视这个标签。那么,为了让浏览器可以认识这个标签,咱们须要使用Angular来定义一个hello指令(本质上说就是本身来把<hello>这种玩意儿替换成浏览器能识别的那些标准HTML标签)。

 

来看这段舒适的JS代码:

 

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

 

以上代码大概看两眼就能够了,不要太在乎细节。

 

而后咱们就能够在浏览器里面看到这样的内容:

 



 

 

实际产生的标签结构是这样的:

 



 

 

能够看到,<hello>这个东东已经被<div>Hi there</div>这个标签替换掉了,这也是以上JS代码里面replace:true这行配置的做用,代码里面的template配置 项固然就是咱们要的div标签啦,至于restrict:'E'这个配置项的含义,请看下表:

 



 

 

ok,看完上面的表格,对于restrict这个属性相信你已经秒懂了,那么咱们来玩儿点花样吧。若是咱们须要替换的HTML标签很长,显然不能用 拼接字符串的方式来写,这时候咱们能够用templateUrl来替代template,从而能够把模板写到一个独立的HTML文件中。

 

3.实例2:transclude(变换)

 

先看例子,JS代码:

 

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

 

HTML代码:

 

<html ng-app='app'>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    </head>
    <body>
        <hello>
            <br/>
            <span>原始的内容,</span><br/>
            <span>还会在这里。</span>
        </hello>
        <hello>
        </hello>
    </body>

    <script src="../angular-1.0.3/angular.min.js"></script>
    <script src="Transclude.js"></script>
</html>

 

运行效果以下:

 



 

 

生成的HTML标签结构以下:

 



 

 

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

 

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

 

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

 

很显然,因为咱们没有加replace:true选项,因此<hello>标签还在,没有被替换掉。同时,经过这个例子你还还会发现一 个暗藏的属性,那就是浏览器实际上很是智能,虽然它并不认识<hello>这个标签,可是页面没有出错,它只是默默地把这个标签忽略掉了!怎 么样?是否是碉堡了?

 

你能够本身在上面的JS代码里面加上replace:true,而后再看生成的HTML结构。

 

4.实例3:关于compile和link

 

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];
});

 

HTML代码:

 

<html ng-app='app'>
    <body ng-controller='MyController'>
        <div ng-repeat='thing in things'>
            {{thing}}.<hello></hello>
        </div>
    </body>

    <script src="../angular-1.0.3/angular.min.js"></script>
    <script src="CompileAndLink.js"></script>
</html>

 

呃,这个例子是用来解释一点点理论的,因此单纯看效果可能看不出个鸟。

 

如前所述,指令的本质实际上是一个替换过程。好,既然如此,Angular究竟是如何进行替换的呢?嗯嗯,这个过程分2个阶段,也就是本节标题所说的compile(编译)和link(链接)了。

 

简而言之,compile阶段进行标签解析和变换,link阶段进行数据绑定等操做。这里面更加细节的处理过程请参见《AngularJS》这本书中的解析,这里就不赘述了(呃,其实是由于解释起来很长很麻烦,叔懒得在这儿说了

 )。

 

那么,知道这件事情有什么用途呢?

 

比方说,你有一些事件须要绑定到某个元素上,那么你须要提供一个link函数,作法请看下一个例子。

 

5.实例4:一个复杂一点的例子Expander

 

这是《AngularJS》这本书里面提供的一个例子,可是书里面没有给出完整的可运行代码,因此这里给出来,你们参考一下。

 

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 = '这里是内部的内容。';
});

 

HTML代码:

 

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

 

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;
}

 

运行效果以下:

 



 

 

注意一下JS代码里面的这一段:

 

link : function(scope, element, attrs) {
    scope.showMe = false;
    scope.toggle = function toggle() {
        scope.showMe = !scope.showMe;
    }
}

 

本身跑一跑例子,研究一番,很少解释。

 

6.实例5:一个综合的例子

 

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, accordionController) {
            scope.showMe = false;
            accordionController.addExpander(scope);
            scope.toggle = function toggle() {
                scope.showMe = !scope.showMe;
                accordionController.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 : 'Test',
        text : 'test'
    }];
});

 

HTML代码:

 

<html ng-app="expanderModule">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <script src="../angular-1.0.3/angular.min.js"></script>
        <link rel="stylesheet" type="text/css" href="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="Accordion.js"></script>
</html>

 

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;
}

 

运行效果:

 



 

 

这个例子主要的难点在于如何在子Expander里面访问外层Accordion的scope中的数据,这一点解释起来略复杂,这里就不展开了,详细描述参见《AngularJS》一书 

相关文章
相关标签/搜索