使用过 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
正如你所见,除了拥有用于实现指令的 <tabs> 和<pane> 标签,页面和常规HTML页面没有什么区别。HTML开发人员无需编写任何代码。固然,总须要有第一个吃螃蟹的人,建立指令共享使用,可是目前Tabs指令已经很常见了,能够在任何地方复用(如BootStrap,、jQueryUI、Wijmo, 和一些知名的前端插件集)。angularjs
因为指令的易用和易编写,许多用户已经开始使用AngularJS编写指令了。例如, AngularJS 开发组已经基于AngularJS实现了一系列指令-UI Bootstrap 来代替Bootstrap; 知名ComponentOne 控件厂商也在AngularJS 基础上建立了Wijmo ;咱们也能够在GitHub上找到一些公共指令资料库:jQueryUI widgets。github
拥有了 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指令。浏览器
文章开头的自定义指令十分的简单。它仅仅实现了同步的功能。通常指令是包含更多元素的:
//建立指令模块 (或者检索现有模块) 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) {…} } });
效果以下:
注意这个自定义指令遵循一种格式:以"my" 为前缀,相似于命名空间,所以若是你在应用中引用了多个模块指令,你能够经过前缀很容易的判断出它是在哪定义的。这不是硬性要求,可是这样作能够带来不少便利。
指令的构造函数会返回带有属性的JavaScript 对象。这些内容在AngularJS 主页中都有清晰说明。如下是我对一些属性的理解:
注意,当调用link 方法时, 经过值传递("@")的scope 变量将不会被初始化,它们将会在指令的生命周期中另外一个时间点进行初始化,若是你须要监听这个事件,可使用scope.$watch 方法。
指令的做用:实现语义化标签
咱们经常使用的HTML标签是这样的:
<div> <span>一点点内容</span> </div>
而使用AngularJS的directive(指令)机制,咱们能够实现这样的东西:
<tabpanel> <panel>子面板1</panel> <panel>子面板2</panel> </tabpanel>
不少人可能要惊呼,这货和JSP或者Struts等等框架里面的taglib很像啊!
呃,说实话,实际上就是这样的,只不过这里是使用JavaScript来实现的。正由于如此,因此不少taglib作不到的功能,使用它就均可以作到,好比访问N层scope里面的对象之类的事情(参见后面第5个例子)。
<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文件中。
先看例子,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结构。
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函数,作法请看下一个例子。
这是《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; } }
本身跑一跑例子,研究一番,很少解释。
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》一书