之前的小例子让我们演示了AngularJS的一些特性,但他们缺少上下文。要解决这个问题,作者要创建一个简单单真实的电子商务应用。
作者将创建一个在线产品分类,客户可以通过分类和页面浏览,一个购物车用户添加或移除产品,当客户在结账时,进入他们的购物明细,列出他们的订单。作者也会创建一个管理区域,包含CRUD,管理分类,并且保护它,只有已经登录的管理员才能修改它。
作者本章的目标,通过创建一个更真实的例子,是让你感觉到,一个真实的AngularJS开发。作者想关注于AngularJS,当然,也要简单地与外部系统集成,如数据存储,会完全忽略其他的,如支付处理。
提示:单元测试
AngularJS对单元测试提供许多杰出的支持,但作者不能在本书的最后一章才说它。因为你确实需要理解AngularJS如何工作,才能写出综合的单元测试。
作者在第25章才讲单元测试,但是他建议咱们按顺序读,这样才能理解单元测试的特性,是如何构建的。
- 开始
这里要安装一些可选的AngularJS特性,来设置服务器,让他分发数据。
- 准备数据
第一步是创建一个新的Deployd应用。你要创建一个路径,来放生成的文件。作者称它路径叫deployd,放在angularjs文件夹同级别。
注意:作者在第一章让你下载Deployd,如果你没那么做,快去下载吧。
在命令行中,进入新路径,输入下面代码
dpd create sportsstore |
要开始新服务器,输入下面的命令
dpd –p 5500 sportsstore\app.dpd dashboard |
提示:这是windows风格的文件分隔符。你在其他平台上可能是sportsstore/app.dpd。
在Deployd dashboard,用于配制服务器,显示在浏览器中。
- 创建数据结构
下一步是高速Deployd,要存储的数据的结构。在dashboard上点击绿色的大按钮,从pop-up菜单中,选择Collection。设置集合的名字为 "/products"(没有双引号)。
Deployd会提示你穿件JSON对象的属性,属性如下:
Name | Type | Required |
name | String | Yes |
description | String | Yes |
category | String | Yes |
price | Number | Yes |
当你完成了属性添加,dashboard会匹配,确保你输入的属性名字正确,并选择了正确的类型。
提示:注意Deployd已经添加id属性。他会用于独一无二地标识数据库中的对象。Deployd会自动指派独一无二的值给id属性。在第8章,当作者实现管理员功能时,会基于这些值。
- 添加数据
现在,作者已经定义了对象的结构。作者可以添加产品的明细。点击Data link,在左边。他会显示grid编辑器,让你填入属性的值。
不要担心指派id属性的值,因为Deployd会自动生成他们。
提示:Deployd的number字段的浮点显示。
- 测试数据服务
要测试Deployd已经正确配置并工作,打开浏览器窗体,导航到下面的URL:
http://localhost:5500/products |
假设你已经在本地计算机安装Deployd,并没有改变它的端口号。/products URL是查询/products集合中的内容,作为一个JSON字符串标示。注意,id字段不同。
- 准备应用
在作者开始写应用前,作者需要准备angularjs文件夹,来创建路径结构,放置AngularJS和Bootstrap文件。
1.2.1、创建路径结构
在angularjs文件夹下,创建如下目录:
Name | Description |
components | 包含自包含的客制化AngularJS组件 |
controllers | 包含应用的控制器,作者会在第13章讲。 |
filters | 包含自定义filters。作者在第14章深度介绍filters |
ngmodules | 包含可选的AngularJS模块。 |
views | 包含SportsStore应用的局部视图。Views包含指令和filters的组合。作者在第10-17章讲。 |
1.2.2、安装AngularJS和BootStrap文件
作者的习惯,将AngularJS JavaScript文件和Bootstrap CSS文件直接放到angularjs路径,将AngularJS可选模块放在ngmodules路径。
Angularjs文件夹下有:angular.js,bootstrap.css,bootstrap-theme.css
Ngmodules文件夹下有:angular-route.js,angular-resource.js
1.2.3、构建基本的草稿
作者喜欢先用静态数据模拟各个部分的内容,开始一个新的AngularJS应用。SportsStroe应用的基本布局,是经典的两列布局。第一列是分类,用户过滤第二列要显示的产品组。
第一步是要创建一个顶级HTML文件。在angularjs文件夹下,新建app.html文件。
<!DOCTYPE html> <html ng-app="sportsStore"> <head> <title>SportsStore</title> <script src="angular.js"></script> <link href="bootstrap.css" rel="stylesheet" /> <link href="bootstrap-theme.css" rel="stylesheet" /> <script> angular.module("sportsStore", []); </script> </head> <body> <div class="navbar navbar-inverse"> <a class="navbar-brand" href="#">SPORTS STORE</a> </div> <div class="panel panel-default row"> <div class="col-xs-3"> Categories go here </div> <div class="col-xs-8"> Products go here </div> </div> </body> </html> |
该文件包含两个AngularJS指定,第一个是调用angular.module方法。
<script> angular.module("sportsStore", []); </script> |
模块是AngularJS应用的顶级构建块,该方法调用穿件一个新的模块,叫做sportsStroe。
第二个方面,是html元素上的ng-app指令:
<html ng-app="sportsStore"> |
Ng-app指令使得sportsStore模块中定义的功能,在HTML范围内可用。作者喜欢将ng-app指令应用到html元素上,你也可以放在body元素上。
提示:用http://localhost:5000/app.html 。来访问该页面。而不是Deployd服务器的5500端口。
- 显示模拟产品数据
作者先定义本地模拟初始化数据,第7章会用从Deployd服务器取得的数据替换该数据。
2.1、创建控制器
首先要加一个控制器。该控制器创建后,会为整个应用服务,作者成为顶级控制器。然后,作者会将几个相关的控制器,放到一个文件中,但把顶级控制器放在app.html文件中。
提示:作者保持顶级控制器和其他文件分割的原因,是它在版本控制系统发生改变时,要一眼就能看到它。当主功能逐渐完成,顶级控制器会发生逐渐发生变化,这是潜在地打破,几乎在所有的应用中。这点在开发周期中,作者想要知道顶级控制器后面的一些东西,可以确保改变经过了充分的测试。
在controllers/sportsStore.js :
angular.module("sportsStore") .controller("sportsStoreCtrl", function ($scope) { $scope.data = { products: [ { name: "Product #1", description: "A product", category: "Category #1", price: 100 }, { name: "Product #2", description: "A product", category: "Category #1", price: 110 }, { name: "Product #3", description: "A product", category: "Category #2", price: 210 }, { name: "Product #4", description: "A product", category: "Category #3", price: 202 }] }; }); |
注意第一行调用angular.module方法,这在app.html也有相同的方法调用。不同之处是,在app.html中定义的module,作者提供了一个额外的参数,像这样:
angular.module("sportsStore", []); |
第二个参数是一个数组,它现在是空的,它是sportsStore模块基于的模块列表,它高速AngularJS,定位并提供这些模块中包含的功能。作者稍后会添加元素到这个数组中。但是现在,重要的是,要知道,当你提供一个数组——空或不空,它都高速AngularJS,要创建一个新的模块。当你尝试创建一个模块,但是它已经存在时,AngularJS会报告一个错误,所以你需要确保你的模块名字的唯一性。
作为对照,在sportsStroe.js文件中调用angular.module方法,不包含第二个参数:
angular.module("sportsStore") |
忽略第二个参数,会告诉AngularJS,你要定位一个已经定义过的模块,在这种情形下,如果指定的模块不存在,AngularJS会报告一个错误, 所以你要确保模块已经被创建。
使用angular.module方法返回的Module对象,都能被用于定义应用功能。作者已经使用controller方法,定义了一个控制器。
注意:作者不常像这样在HTML文件中,创建主应用模块,因为他可以被简单地放置到Javascript文件中。作者将声明分成若干小部分的原因,是多次使用angular.module方法,导致无休止的混乱,作者想让你注意到它。
SportsStore应用中,顶级控制器的主要角色,是定义用于在不同视图上显示的数据。在第13章,会看到多个控制器级联排列。
注意:当作者定义控制器的范围上的数据时,将数据对象的数组,定义在data上,它会附加到scope。你必须在定义数据时非常小心,因为如果你在scope上直接指派属性(如$scope.products=[data]),因其他控制器可以读取,但总是不能修改数据。作者将在第13章详细说明。
2.2、显示产品明细
要显示产品明细,需要给app.html文件添加一些HTML。AngularJS让显示数据变得简单。
<!DOCTYPE html> <html ng-app="sportsStore"> <head> <title>SportsStore</title> <script src="angular.js"></script> <link href="bootstrap.css" rel="stylesheet" /> <link href="bootstrap-theme.css" rel="stylesheet" /> <script> angular.module("sportsStore", []); </script> <script src="controllers/sportsStore.js"></script> </head> <body ng-controller="sportsStoreCtrl"> <div class="navbar navbar-inverse"> <a class="navbar-brand" href="#">SPORTS STORE</a> </div> <div class="panel panel-default row"> <div class="col-xs-3"> Categories go here </div> <div class="col-xs-8"> <div class="well" ng-repeat="item in data.products"> <h3> <strong>{ {item.name}}</strong> <span class="pull-right label label-primary"> { {item.price | currency}} </span> </h3> <span class="lead">{ {item.description}}</span> </div> </div> </div> </body> </html> |
这里有三个变化。第一个,添加了一个script元素,导入sportsStore.js文件。该文件包含sportsStoreCtrl控制器。因为作者已经在app.html文件中定义了sportsStore模块,然后在sportsStore.js文件中定位并使用它,作者需要确保sportsStore模块的定义script,出现在引用之前。
第二个改变,是用ng-controller指令,将控制器应用到视图,像这样:
<body ng-controller="sportsStoreCtrl"> |
作者会使用sportsStoreCtrl控制器,为整个应用提供支持,所以,他把它应用到body元素上。
2.2.1、生成内容元素
最后一个改变,是创建了产品明细元素。AngularJS提供的一个最有用的指令,是ng-repeat,它为数据数组中的每个对象生成元素。
<div class="well" ng-repeat="item in data.products"> |
然后作者在数据绑定表达式中,引用当前对象。
<div class="well" ng-repeat="item in data.products"> <h3> <strong>{ {item.name}}</strong> <span class="pull-right label label-primary">{ {item.price | currency}}</span> </h3> <span class="lead">{ {item.description}}</span> </div> |
Name和description值被直接插入HTML元素,但price属性不是这样:作者应用了一个filter。一个filter格式或order,是要在view中显示的数据值。AngularJS有一些内建的filters,包含currency filter,它用币种金额,格式化数字。Filters使用 | 符号应用。Item.price | currency,会告诉 AngularJS,使用currency filter,传递item对象的price属性的值。
Currency filter默认使用USD格式,但作者会在第14章说明,怎么使用AngularJS本地化filters,来显示其他币种格式。作者也会在第14章,说明内建filters,并给你显示如何创建自己的。
- 显示分类列表
下一步是显示分类列表,让用户可以过滤显示的产品组。实现该特性,需要生成客户用于导航的元素,选择一个产品分类后,更新明细面板,只显示被选中分类的产品。
3.1、创建分类列表
作者想从产品数据对象动态生成分类元素,而不是硬编码HTML元素。动态途径设置起来更复杂一点,但它会允许SportsStore应用能自动反应产品分类的变化。这意味着作者不得不从产品数据对象中生成一个独一无二的分类名称的列表。该特性AngularJS不包含,但通过创建和应用一个自定义过滤器,能够简单地实现。在filters路径下,创建一个customFilters.js:
angular.module("customFilters", []) .filter("unique", function () { return function (data, propertyName) { if (angular.isArray(data) && angular.isString(propertyName)) { var results = []; var keys = {}; for (var i = 0; i < data.length; i++) { var val = data[i][propertyName]; if (angular.isUndefined(keys[val])) { keys[val] = true; results.push(val); } } return results; } else { return data; } } }); |
自定义过滤器,使用Module对象的filter方法定义,它通过angular.module方法获取并创建。作者选择新建一个模块,叫做customFilters,来涵盖他的过滤器,主要可以展示如何定义并在应用中结合多个模块。
提示:当你给现存模块添加组件或创建一个新模块时,没有先后顺序。作者希望他定义的功能在之后的不同应用里重用时,他倾向于创建模块。自定义过滤器倾向于可重用,因为数据格式是任何AngularJS应用都需要的,也是开发者需要的公共格式。
Filter方法的参数,是filter的名字,例子中是unique,它返回一个factory function,该函数并不实际工作,而是返回一个filter function。当AngularJS需要创建一个filter的实例时,它调用factory function,filter function就会被调用,以执行过滤。
所有的filter function都有一个参数,传递他们需要的格式的数据,但作者的filter定义了一个额外的参数,叫做propertyName,它能用于指定要从要生成哪个字段的唯一列表。过滤功能的实现很简单:枚举数据对象的内容,用propertyName参数,构建一个独一无二的值的列表。
提示:作者没有没有将过滤器功能硬编码为查找category属性,这会限制unique过滤器在其他应用的重用。
过滤功能返回过滤后的数据。作者使用anagular.isArray和angular.isString,检查传入的数据是否是数组,属性名是否是字符串。然后,他用angular.isUndefined方法,检查该属性是否定义。AngularJS提供一组有用的工具方法,包含允许你检查对象和属性类型。作者会在第5章完整地描述。如果过滤器接收到一个数组和一个属性名,然后他生成并返回一个独一无二的属性值的数组。不然,他返回将接收到的数据原封不动地返回。
提示:让过滤器仅显示用户需要的内容,而不是在scope中修改原始数据。
3.2、生成分类导航链接
下一步,生成用户可以点击的产品分类的导航链接。这需要使用上节创建的unique过滤器。
<!DOCTYPE html> <html ng-app="sportsStore"> <head> <title>SportsStore</title> <script src="angular.js"></script> <link href="bootstrap.css" rel="stylesheet" /> <link href="bootstrap-theme.css" rel="stylesheet" /> <script> angular.module("sportsStore", ["customFilters"]); </script> <script src="controllers/sportsStore.js"></script> <script src="filters/customFilters.js"></script> </head> <body ng-controller="sportsStoreCtrl"> <div class="navbar navbar-inverse"> <a class="navbar-brand" href="#">SPORTS STORE</a> </div> <div class="panel panel-default row"> <div class="col-xs-3"> <a ng-click="selectCategory()" class="btn btn-block btn-default btn-lg">Home</a> <a ng-repeat="item in data.products | orderBy:'category' | unique:'category'" ng-click="selectCategory(item)" class=" btn btn-block btn-default btn-lg"> { {item}} </a> </div> <div class="col-xs-8"> <div class="well" ng-repeat="item in data.products"> <h3> <strong>{ {item.name}}</strong> <span class="pull-right label label-primary"> { {item.price | currency}} </span> </h3> <span class="lead">{ {item.description}}</span> </div> </div> </div> </body> </html> |
在sportsStore模块中,定义一个对customFilters的依赖。
angular.module("sportsStore", ["customFilters"]); |
这就是declaring a dependency。在本例中,作者声明sportsStore模块依赖于customFilters模块的功能。这会导致AngularJS定位customFilters模块,并确保它可用,然后可以引用它包含的组件,如过滤器和控制器。该过程叫做resolving the dependency。
提示:声明和管理模块和其他种类组件的过程,叫做dependency injection,是AngularJS的核心,作者会在第9章解释该过程。
3.2.1、生成导航元素
最有趣的的部分,是使用ng-repeat元素,为每个产品分类生成一个元素。
<a ng-click="selectCategory()" class="btn btn-block btn-default btn-lg">Home</a> <a ng-repeat="item in data.products | orderBy:'category' | unique:'category'" ng-click="selectCategory(item)" class=" btn btn-block btn-default btn-lg"> { {item}} </a> |
Ng-repeat属性值的第一部分,和生成产品明细的一样,item in data.products,它告诉ng-repeat指令要枚举data.products数组的对象,指派当前对象给叫做item的变量,并复制应用该指令的a元素。
属性值的第二部分,告诉AngularJS,传递data.products数组给叫做orderBy的内建过滤器,用于排序数组。该orderBy过滤器,有一个参数,指定用于排序的属性。作者会在第14章完整地描述orderBy过滤器。
提示:注意作者在单引号之间,指定了属性名。默认地,AngularJS假设表达式中的名字,指向scope中定义的变量。要指定一个静态值,不得不使用字符串,在Javascript中,需要单引号或双引号。
过滤器有一个很棒的特性,通过 | 将他们链式调用。AngularJS按照过滤器排列的顺序,应用他们。这意味着,category属性在排序过后,才传递给unique过滤器。你看看作者如何指定unique过滤器要操作的属性:
<a ng-repeat="item in data.products | orderBy:'category' | unique:'category'" |
这样的结果是,data.products数组传递给orderBy过滤器,它基于category属性排序。Ta排序过的数组传递给unique,它返回一个字符串数组,包含独一无二的category值———因为unique过滤处理过程中不改变值的顺序,结果保持上个过滤器的排序。
提示:作者可以颠倒使用过滤器的顺序,结果一样。不同的是,orderBy过滤器会作用于一个字符串数组,而不是product对象。orderBy过滤器设计用来操作对象的,但你可以通过使用orderBy:'toString()',来排序字符串。不要忘了引号,不然,AngularJS找一个叫做toString的scope属性,而不是调用toString方法。
3.2.2、处理点击事件
作者在a元素上使用ng-click指令,可以相应用户的点击。AngularJS提供一组内建指令,作者将在第11章介绍,它们恩能够在相应事件时,简单地调用控制器行为。ng-click指令的名字,建议指明AngularJS在点击事件触发时,要做什么。
<a ng-click="selectCategory()"class="btn btn-block btn-default btn-lg">Home</a> <a ng-repeat="item in data.products | orderBy:'category' | unique:'category'" ng-click="selectCategory(item)"class=" btn btn-block btn-default btn-lg"> { {item}} </a> |
在app.html中有两个a元素。第一个是静态地,创建Home按钮,作者用户显示所有产品的所有分类。在该元素中,作者设置ng-click指令,它调用一个控制器行为,叫做selectCategory,没有参数。后面作者会创建该行为,现在,重要的事情是记住另一个a元素,它的selectCategory行为有一个item变量值作为参数。点击时,例如selectCategory('Category #1')。
3.3、选择分类
在浏览器中点击分类按钮,不会有任何效果,因为ng-click指令设置为调用一个还没有定义的行为。当你尝试访问一个不存在的行为或数据时,AngularJS不会抱怨。这给debugging带来了小麻烦,因为显示错误结果,但也更灵活。作者将在第13章描述如何在更深的地方使用控制器和它的范围。
3.3.1、定义控制器
作者要定义叫做selectCategory的行为,来响应用户对分类按钮的点击。他不像将行为添加到顶级sportsStoreCtrl控制器上,那是整个应用提供行为和数据的地方。取而代之,作者新建一个控制器,专用于产品列表和分类视图。controllers/productListControllers.js。
提示:你可能会问我,为什么我的控制器的名字,比过滤器的更专业。原因是,过滤器更通用,准备在其他应用中重用。
angular.module("sportsStore") .controller("productListCtrl", function ($scope, $filter) { var selectedCategory = null; $scope.selectCategory = function (newCategory) { selectedCategory = newCategory; } $scope.categoryFilterFn = function (product) { return selectedCategory == null || product.category == selectedCategory; } }); |
作者调用app.html文件中定义的sportsStroe模块的controller方法(记住,一个参数的angular.module方法,以为这找现有模块,而两个参数的意思,是创建一个新模块)。
该控制器叫做productListCtrl,它定义一个叫做selectCategory的行为。该控制器还定义了一个叫做categoryFilterFn的行为,它以一个产品对象作为参数,它在没有分类被选中,或有一个分类被选中并且该产品输入它时,返回true。
提示:注意selectedCategory变量没有定义在scope上,它只是一个常规的JavaScript变量,意味着它不能从指令访问或view的数据绑定中。这样做的结果是,创建了selectCategory行为,用于设置分类,categoryFilterFn用于过滤产品对象,但被选中的分类的细节,保持私有。作者在SportStore应用中,不依赖此特性。
3.3.2、应用控制器和过滤产品
作者不得不使用ng-contorller指令,应用控制器到视图,让ng-click指令可以调用selectCategory行为。不然,包含ng-click指令的元素的返回,会是顶级sportsStoreCtrl控制器,它不包含该行为。
<!DOCTYPE html> <html ng-app="sportsStore"> <head> <title>SportsStore</title> <script src="angular.js"></script> <link href="bootstrap.css" rel="stylesheet" /> <link href="bootstrap-theme.css" rel="stylesheet" /> <script> angular.module("sportsStore", ["customFilters"]); </script> <script src="controllers/sportsStore.js"></script> <script src="filters/customFilters.js"></script> <script src="controllers/productListControllers.js"></script> </head> <body ng-controller="sportsStoreCtrl"> <div class="navbar navbar-inverse"> <a class="navbar-brand" href="#">SPORTS STORE</a> </div> <div class="panel panel-default row" ng-controller="productListCtrl"> <div class="col-xs-3"> <a ng-click="selectCategory()" class="btn btn-block btn-default btn-lg">Home</a> <a ng-repeat="item in data.products | orderBy:'category' | unique:'category'" ng-click="selectCategory(item)" class=" btn btn-block btn-default btn-lg"> { {item}} </a> </div> <div class="col-xs-8"> <div class="well" ng-repeat="item in data.products | filter:categoryFilterFn"> <h3> <strong>{ {item.name}}</strong> <span class="pull-right label label-primary"> { {item.price | currency}} </span> </h3> <span class="lead">{ {item.description}}</span> </div> </div> </div> </body> </html> |
作者添加script元素,导入productListControllers.js文件,在包含分类列表和产品列表的视图部分,使用ng-controller指令来应用productListCtrl控制器。
将productListCtrl控制器放在sportsStorectrl控制器范围内,意味着作者可以使用先进的controller scope inheritance,作者将在第13章解释。productListCtrl继承sportsStoreCtrl中定义的data.products数组和任何其他数据和行为。这样做的好处,是可以限制控制器功能在应用上的范围,更易于执行单元测试,放置组件间非预期的依赖。
另一个改变,是在生成产品明细的ng-repeat上配置:
<div class="well" ng-repeat="item in data.products | filter:categoryFilterFn"> |
AngularJS提供的一个内建的过滤器,叫做filter。它处理一个集合,选择他包含的对象的子集。作者将在第14章描述该技术。通过在ng-repeat指令上使用它,确保只有当前被选中的分类的产品显示。
3.4、高亮被选中的分类
用户可以点击分类按钮,来过滤产品,但没有视觉反馈,哪个范磊被选中了。作者将为选中的分类应用btn-primary CSS class。第一步,在控制器中添加一个行为,它接收一个分类。如果它是被选中的分类,返回CSS class名。
提示:注意作者怎么在AngularJS模块上链式调用方法。这是因为Module定义的方法,同样返回Module,这点和fluent API一样。
angular.module("sportsStore") .constant("productListActiveClass", "btn-primary") .controller("productListCtrl", function ($scope, $filter, productListActiveClass) { var selectedCategory = null; $scope.selectCategory = function (newCategory) { selectedCategory = newCategory; } $scope.categoryFilterFn = function (product) { return selectedCategory == null || product.category == selectedCategory; } $scope.getCategoryClass = function (category) { return selectedCategory == category ? productListActiveClass : ""; } }); |
作者不想在行为代码里指定class的名字,所以作者使用在Moduled对象上使用constant方法,定义一个叫做productListActiveClass的固定值。它允许我改变class,每次要使用时。要在控制器中访问该值,作者不得不声明constant名字作为依赖,如下面那样。
.controller("productListCtrl", function ($scope, $filter, productListActiveClass) { |
接着,可以在getCategoryClass行为中,使用productListActiveClass,它简单地检查它接收到的分类,返回class 名字或空字符串。
getCategoryClass行为看起来有些奇怪,但它可以在每个分类导航按钮中调用,传递分类的名字作为参数。要应用CSS class,作者使用ng-class指令。
<div class="col-xs-3"> <a ng-click="selectCategory()" class="btn btn-block btn-default btn-lg">Home</a> <a ng-repeat="item in data.products | orderBy:'category' | unique:'category'" ng-click="selectCategory(item)" class=" btn btn-block btn-default btn-lg" ng-class="getCategoryClass(item)"> { {item}} </a> </div> |
Ng-class属性,它会应用geCategoryClass返回的class 名。作者将在第11章讲。
3.5、添加分页
分页,一次显示一定数量的产品。需要三步才能实现分页:修改控制器,scope跟踪分页状态,实现过滤器,更新视图。
3.5.1、更新控制器
在productListControllers.js文件中,更新控制器,以跟踪分页。
angular.module("sportsStore") .constant("productListActiveClass", "btn-primary") .constant("productListPageCount", 3) .controller("productListCtrl", function ($scope, $filter, productListActiveClass, productListPageCount) { var selectedCategory = null; $scope.selectedPage = 1; $scope.pageSize = productListPageCount;
$scope.selectCategory = function (newCategory) { selectedCategory = newCategory; $scope.selectedPage = 1; } $scope.selectPage = function (newPage) { $scope.selectedPage = newPage; } $scope.categoryFilterFn = function (product) { return selectedCategory == null || product.category == selectedCategory; } $scope.getCategoryClass = function (category) { return selectedCategory == category ? productListActiveClass : ""; } $scope.getPageClass = function (page) { return $scope.selectedPage == page ? productListActiveClass : ""; } }); |
每页产品的数量被定义为常量,叫做productListPageCount。在控制器里,作者在scope上定义变量,曝露常量值(所以作者能在view上访问它)和当前被选择的页。作者已经定义了行为,selectPage,允许被选择的页,编程其他页。getPageClass,设计用于ng-class指令,来高亮被选中的页,和之前被选中的分类一样。
提示:你可能会疑惑,为什么视图不能直接访问常量值,而是要通过scope来确切地曝露。答案是,AngularJS努力放置组件间紧密地耦合,作者在第3章描述过。如果视图可以直接访问服务和常量值,那么他容易结束无休止的耦合和依赖,难于测试和维护。
3.5.2、实现过滤
在customFilters.js文件中创建两个新的过滤器。
angular.module("customFilters", []) .filter("unique", function () { return function (data, propertyName) { if (angular.isArray(data) && angular.isString(propertyName)) { var results = []; var keys = {}; for (var i = 0; i < data.length; i++) { var val = data[i][propertyName]; ChAPTeR6 ■SPORTSSTORe: A ReAl APPlICATIOn 144 if (angular.isUndefined(keys[val])) { keys[val] = true; results.push(val); } } return results; } else { return data; } } }) .filter("range", function ($filter) { return function (data, page, size) { if (angular.isArray(data) && angular.isNumber(page) && angular.isNumber(size)) { var start_index = (page - 1) * size; if (data.length < start_index) { return []; } else { return $filter("limitTo")(data.splice(start_index), size); } } else { return data; } } }) .filter("pageCount", function () { return function (data, size) { if (angular.isArray(data)) { var result = []; for (var i = 0; i < Math.ceil(data.length / size) ; i++) { result.push(i); } return result; } else { return data; } } }); |
第一个新的过滤器,叫做range,从一个数组中返回一队列元素,代表产品页。过滤器接收参数,当前被选中的页(用于表明range的开始index),和page size(用于表明end index)。
Range过滤器不是特别有趣,不同于我已经在功能上提供的一个内建过滤器,叫做limitTo,它从一个数组中,返回指定号码的项。要使用该过滤器,我再$filter服务上定义了一个依赖。它让作者创建并使用filter的实例。作者将在地14章解释详细,最关键的是这句声明:
return $filter("limitTo")(data.splice(start_index), size); |
作者使用的JavaScript 标准方法splice的结果是选择数据数组的一部分,然后将它传递给limitTo过滤器,来选择要在该页显示的。limitTo过滤器确保遍历数组没有问题,如果指定的数字不可用,会返回几个items。
第二个过滤器,pageCount,是一个肮脏的——但方便的——hack。Ng-repeat指令使得生成内容很简单,但它基于数据数组工作。你不能,例如,让他重复指定的次数。作者的过滤器计算出页码的一个数组。
当心:作者让过滤器的功能绕开受限制的ng-repeat指令,这是非常危险的,但是个应急办法。更好的替代方法,会在第16,17章看到,让ng-repeat指令生成指定次数的元素。
3.5.3、更新视图
更新app.html。
<!DOCTYPE html> <html ng-app="sportsStore"> <head> <title>SportsStore</title> <script src="angular.js"></script> <link href="bootstrap.css" rel="stylesheet" /> <link href="bootstrap-theme.css" rel="stylesheet" /> <script> angular.module("sportsStore", ["customFilters"]); </script> <script src="controllers/sportsStore.js"></script> <script src="filters/customFilters.js"></script> <script src="controllers/productListControllers.js"></script> </head> <body ng-controller="sportsStoreCtrl"> <div class="navbar navbar-inverse"> <a class="navbar-brand" href="#">SPORTS STORE</a> </div> <div class="panel panel-default row" ng-controller="productListCtrl"> <div class="col-xs-3"> <a ng-click="selectCategory()" class="btn btn-block btn-default btn-lg">Home</a> ChAPTeR6 ■SPORTSSTORe: A ReAl APPlICATIOn 146 <a ng-repeat="item in data.products | orderBy:'category' | unique:'category'" ng-click="selectCategory(item)" class=" btn btn-block btn-default btn-lg" ng-class="getCategoryClass(item)"> { {item}} </a> </div> <div class="col-xs-8"> <div class="well" ng-repeat= "item in data.products | filter:categoryFilterFn | range:selectedPage:pageSize"> <h3> <strong>{ {item.name}}</strong> <span class="pull-right label label-primary"> { {item.price | currency}} </span> </h3> <span class="lead">{ {item.description}}</span> </div> <div class="pull-right btn-group"> <a ng-repeat= "page in data.products | filter:categoryFilterFn | pageCount:pageSize" ng-click="selectPage($index + 1)" class="btn btn-default" ng-class="getPageClass($index + 1)"> { {$index + 1}} </a> </div> </div> </div> </body> </html> |
第一处变化,是ng-repeat指令,生成产品列表,数据会通过range过滤器,过滤选择当前页的产品。当前页的明细,和每页产品的数量,使用作者定义在控制器scope上值作为参数,传递给过滤器。
第二处改变,是添加了导航按钮。作者使用ng-repeat指令,计算出当前选中的分类,有多少页产品,传递结果给pageCount过滤器,这会导致ng-repeat指令生成正确数量的导航页按钮。当前选中的页,通过ng-class指令表达,通过ng-click指令,页面变化。