Angular(三)
一、使用Module(模块)组织依赖关系
模块提供了一种方法,可以用来组织应用中一块功能区域的依赖关系;同时还提供了一种机制,可以自动解析依赖关系(又叫做依赖注入)。一般来说,我们把这些叫做依赖服务,因为它们会负责为应用提供特殊的服务。
例如:如果购物站点中的一个控制器需要从服务器上获取一个商品列表,那么我们就需要某些对象——把它可以叫做Items——来处理从服务器端获取商品的工作。进而,Items对象就需要以某种方式与服务器上的数据库进行交互。
利用模块和模块内置的依赖注入功能,我们就可以把控制器写的更加简单,假设我们已经把Items对象定义成了一个服务,示例如下:
function ShoppingController($scope,Items){ $scope.items=Items.query(); }
服务都是单例(单个示例)的对象,它们用来执行必要的任务,支撑应用的功能。angular内置了很多服务,例如$location服务,用来和浏览器的地址栏进行交互;$route服务,用来根据URL地址的变化切换视图;还有$http服务,用来和服务器进行交互。
当你需要在多个控制器之间进行交互和共享状态时,这些服务就是一种很好的机制。angular内置的服务以$开头。
以下3个函数可以用来创建一般的服务,它们的复杂度和功能不同。例如:provide、factory、service。
先看下针对Item的使用factory的例子,可以这样编写服务:
<!DOCTYPE html> <html lang="en" ng-app='ShoppingModule'> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body ng-controller='ShoppingController'> <h1>Shop</h1> <table> <tr ng-repeat="item in items"> <td>{{item.title}}</td> <td>{{item.description}}</td> <td>{{item.price|currency}}</td> </tr> </table> <script src='angular-1.3.0.js'></script> <script> //创建一个模型用来支撑我们的购物视图 var shoppingModule=angular.module('ShoppingModule',[]); //设置好服务工厂,用来创建我们的Items接口,以便访问服务端数据库 shoppingModule.factory('Items',function(){ var items={}; items.query=function(){ //在真实的应用中,我们会从服务端拉取这块数据 return [ {title:'paint',description:'pots full of paint',price:3.95}, {title:'polka',description:'dots with polka',price:2.95}, {title:'pebbles',description:'just little rocks',price:6.95} ]; }; console.log(items); return items; }) shoppingModule.controller('ShoppingController',['$scope','Items',function($scope,Items){ $scope.items=Items.query(); console.log($scope.items) }]); </script> </body> </html>
当angular创建 ShoppingController 时,它会把$scope对象和刚定义的Items服务作为参数传递进去。这一点是通过参数名匹配来实现的,也就是说,angular会查看我们的 ShoppingController 类的函数签名,然后就会发现它需要一个Items对象。既然我们已经把Items定义成了一个服务,那么angular当然知道去哪里找这个服务了。
以字符串的形式查找这些依赖关系的结果是,可以进行注入的那些函数(例如控制器的构造器)的参数是没有顺序的。
服务自身可以相互依赖,类似地,可以使用Module接口来定义模块之间的依赖关系。例如:如果你引入了model1和model2,那么应用的模块声明看起来可能会向下面这样:
var appMod = angular.module('app',['model1','model2']);
二、使用过滤器格式化数据
可不必受限于内置的过滤器,自己编写过滤器也非常简单。例如,我们需要为文字创建大写的字符串,代码如下:
<!DOCTYPE html> <html lang="en" ng-app='MyModule'> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <div ng-controller='myAppCtrl'> <h1>{{pageHeading|titleCase}}</h1> </div> <script src='angular-1.3.0.js'></script> <script> var myModule=angular.module('MyModule',[]); myModule.filter('titleCase',function(){ var titleCaseFilter=function(input){ var words=input.split(' '); console.log(words); for (var i=0;i<words.length;i++){ words[i]=words[i].charAt(0).toUpperCase()+words[i].slice(1); } return words.join(' '); } return titleCaseFilter; }) myModule.controller('myAppCtrl',['$scope',function($scope){ $scope.pageHeading='behold the majesty of your page title'; }]) </script> </body> </html>
三、校验用户输入
angular自动为<form>元素增加了一些很好用的特性,使其更适合开发单页面应用。其中一个特性是,angular允许你为表单中的输入元素定义一个合法的状态,并且只有当所有元素都是合法状态时才允许提交表单。
<!DOCTYPE html> <html lang="en" ng-app='myModule'> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body ng-controller='signUpCtrl'> <h1>Sing Up</h1> <form name="addUserForm"> <div ng-show='message'>{{message}}</div> <div>First name:<input type="" name="firstName" ng-model='user.first' required></div> <div>Last name:<input type="" name="lastname" ng-model='user.last' required></div> <div><button ng-click='addUser()' ng-disabled='!addUserForm.$valid'>submit</button></div> </form> <script src='angular-1.3.0.js'></script> <script> var myModule=angular.module('myModule',[]); myModule.controller('signUpCtrl',['$scope',function($scope){ $scope.message=''; $scope.addUser=function(){ $scope.message='thanks,'+$scope.user.first } }]) </script> </body> </html>
在控制器中,我们可以通过$valid属性获取表单的校验状态,当表单中的所有输入项都合法时,angular将会把这个属性设置为TRUE。我们可以利用这个属性来做很多很炫的事情,例如当表单没有输入完成时可以禁用submit按钮。
四、作用域
我们经常需要在指令中访问$scope对象,以便观察数据模型的值,当这些值发生变化时刷新UI。
- 绑定策略:
@ 把当前属性作为字符串传递。
= 绑定当前属性,它带有一个来自指令父scope的属性
& 传递一个来自父scope的函数,稍后调用。
例如,我们要创建一个expender指令,它会显示一个很小的扩展条,点击的时候扩展条就会展开,显示额外的内容。
<!DOCTYPE html> <html lang="en" ng-app='myModule'> <head> <meta charset="UTF-8"> <title>Document</title> <style type="text/css"> .expender{ border: 1px solid black; width: 250px; } .expender >.title{ background: black; color: white; padding: .1em .3em; cursor:pointer; } .expender > .body{ padding: .1em .3em } </style> </head> <body ng-controller='scopeCtrl'> <expender class="expender" expender-title='title'> {{text}} </expender> <script src='angular-1.3.0.js'></script> <script> var myModule=angular.module('myModule',[]); myModule.controller('scopeCtrl',['$scope',function($scope){ $scope.title='click me to expend'; $scope.text='Hi there folks,i am the content' + 'that was hidder but is now shown.' }]), myModule.directive('expender',function(){ return{ restrict:'EA', replace:true, transclude:true, scope:{title:'=expenderTitle'}, 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; } } } }) </script> </body> </html>
效果图:
标题的值(click me to expend)以及文本(hi there folks...)都来自外层scope。指令中的每一个选项分别代表什么:
- restrict:EA 可以把这个指令当作一个元素或者一个属性进行调用。也就是说<expender...>...</expender>和<div expender...>...</div>是等价的
- replace:TRUE 使用我们提供的模板来替换原来的元素
- transclude: true 把原来元素中的内容移动到我们所提供的模板中
- scope:{title:=expenderTitle} 创建scope的一个局部属性,名为title,他与父scope中的一个定义在expender-title中的属性绑定。这里,为了方便起见,我们把title重命名为expenderTitle。我们可以把scope编写成{expenderTitle:“=”},然后在模板中用expenderTitle来引用它。注意:这里的命名方式与指令自身一样采用了驼峰法则。
- template:<div>+... 声明当前指令需要插入的模板。注意:我们使用了ng-click和ng-show来显示隐藏模板,使用ng-transclude来声明吧原来的内容放到那里。同时请注意,用来替换的模板会访问父scope,而不会访问封装它的指令所属的scope。
- link:... 设置showMe模型来跟踪扩展条的打开、关闭状态,并定义toggle函数,当用户点击所在的div时调用这个函数。
注意,在使用@策略时,我们仍然可以通过双花括号插值语法把title绑定到控制器scope上:
<expender class="expender" expender-title={{title}}> {{text}} </expender>
五、操作DOM元素
link、compile传递的参数,都指向原始的DOM元素。如果加载了jQuery库,那么他们就会指向经过jQuery包装之后的元素。如果不使用jQuery,那么这些DOM元素都位于angular-native包装器jqLite中。jqLite是jQuery API的子集,在angular中我们需要用它来创建所有的东西。对于很多应用来说,只要使用这个轻量级API就可以实现所有你想做的事情了。
如果你需要直接访问原始的DOM元素,你就可以使用element[0]来访问对象中的第一个元素。
在angular文档中的angular.element()部分,可以看到目前支持的完整API列表,你可以使用angular.element()来创建包装在jqLite中的DOM元素。angular对象还带有addClass(),bind(),find(),toggleClass()等函数。当然,这些是jQuery中常用的核心函数,只是angular的实现代码更精致而已。
<!DOCTYPE html> <html lang="en" ng-app='myModule'> <head> <meta charset="UTF-8"> <title>Document</title> <style type="text/css"> .expender{ border: 1px solid black; width: 250px; } .expender >.title{ background: black; color: white; padding: .1em .3em; cursor:pointer; } .expender > .body{ padding: .1em .3em } .closed{ display: none; } </style> </head> <body ng-controller='scopeCtrl'> <expender class="expender" expender-title={{title}}> {{text}} </expender> <script src='angular-1.3.0.js'></script> <script> var myModule=angular.module('myModule',[]); myModule.controller('scopeCtrl',['$scope',function($scope){ $scope.title='click me to expend'; $scope.text='Hi there folks,i am the content' + 'that was hidder but is now shown.' }]), myModule.directive('expender',function(){ return{ restrict:'EA', replace:true, transclude:true, scope:{title:'@expenderTitle'}, 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)); console.log(titleElement); titleElement.bind('click',toggle); function toggle() { bodyElement.toggleClass('closed') } } } }) </script> </body> </html>
六、控制器
要实现需要彼此通信的嵌套指令,可以使用控制器。<menu>需要知道内部<menu-item>元素的信息,这样它才能够正确地显示和隐藏它们。同样地,<tab-set>需要知道内部<tab>元素的信息。
如前所示,为了创建能够在指令之间进行通信的接口,可以使用controller属性语法把控制器声明成指令的一部分:
controller:function controllerConstructor($scope,$element,$attrs,$transclude) { }
controller函数是通过依赖注入的,所以这里所列出的参数列表都是可选的,可以按照其他顺序将其列出,当然这些参数都是具有某种潜在的用途。他们还是可用的服务子集。
通过require属性语法,其他指令可以把这个控制器传递给自己。require的完整形式如下:
require: '^?directiveName'
- directiveName: 这个以驼峰法则命名的选项名称指定了控制器应该带有哪一条指令,所以,如果<my-menu-item>指令需要在它的父指令<my-menu>上找到一个控制器,就需要把它写成myMenu
- ^ : 默认情况下,angular会从同一个元素上的命名指令中获取控制器。添加^符号的意思是,需要同时遍历DOM树去查找指令。对于<my-menu>,我们需要添加这个符号
- ?: 如果没有找到所需要的控制器,angular会抛出一个异常,告诉问题所在。在字符串中添加一个?号的意思是说这个控制器是可选的,如果没有找到,不需要抛出异常。
例如,重写expender指令,让它可以用在一个“accordion”集合中。它会保证当你打开一个扩展条时,集合中的所有其他扩展条都会自动关闭掉。
<!DOCTYPE html> <html lang="en" ng-app='myModule'> <head> <meta charset="UTF-8"> <title>Document</title> <style type="text/css"> .expender { border: 1px solid black; width: 250px; } .expender > .title { background: black; color: white; padding: .1em .3em; cursor: pointer; } .expender > .body { padding: .1em .3em } </style> </head> <body ng-controller='scopeCtrl'> <accordion> <expender class="expender" ng-repeat="expender in expenders" expender-title={{expender.title}}> {{expender.text}} </expender> </accordion> <script src='angular-1.3.0.js'></script> <script> var myModule = angular.module('myModule', []); myModule.controller('scopeCtrl', ['$scope', function ($scope) { $scope.expenders = [ { title: 'click me to expend', text: 'Hi there folks,i am the content' + 'that was hidder 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' } ] }]); myModule.directive('accordion', function () { return { restrict: 'EA', replace: true, transclude: true, scope: {title: '@expenderTitle'}, template: '<div ng-transclude></div>', controller: function () { var expenders = []; this.gotOpened = function (selectedExpender) { console.log(222); angular.forEach(expenders, function (expender) { if (selectedExpender != expender) { expender.showMe = false; } }) } this.addExpender = function (expender) { expenders.push(expender); console.log(expenders) } } } }); myModule.directive('expender', function () { return { restrict: 'EA', replace: true, transclude: true, require: '^?accordion', scope: {title: '@expenderTitle'}, 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) { //此处的accordionController是由require: '^?accordion'的controller决定的 scope.showMe = false; accordionController.addExpender(scope); console.log(accordionController); scope.toggle = function toggle() { scope.showMe = !scope.showMe; accordionController.gotOpened(scope); } } } }) </script> </body> </html>
效果图:
以上代码,通过编写accordion指令,做一些元素定位工作。我们把控制器的构造函数以及进行元素定位操作的方法添加到accordion指令中,定义了一个addExpander()函数,扩展条可以调用它来注册自身;还创建了一个可被扩展条调用的gotOpen()函数,通过它,accordion的控制器就知道把其他所有处于打开状态的扩展条都关闭。
在expender指令中,我们扩展它的时候要求accordion的控制器来自它的父元素,然后在合适的时候调用addExpender()和gotOpen()函数。
注意:accordion指令中的控制器创建了一个API接口,有了它,所有扩展条控件之间就可以进行通信了。