AngularJS 高级程序设计

《AngularJS 高级程序设计》

第 1 章 准备

AngularJS应用程序是围绕着MVC模式而构建的,该模式的重点在于构建这样的应用程序:

  • 可扩展
  • 可维护
  • 可测试
  • 标准化

你需要知道哪些知识

本书的组织结构

第一部分:准备
第二部分:使用AngularJS工作
第三部分:AngularJS模块和服务

会有许多实例吗

从哪里可以获得实例代码

如何搭建你的开发环境

选择Web浏览器
Chrome扩展插件Batarang AngularJS

选择代码编辑器

安装Node.js

安装Web服务器
这里使用Connect,在Node.js的安装目录下运行如下命令:

1
npm install connect

在Node.js安装目录下创建一个名为server.js的文件

1
2
3
4
5
var connect = require('connect');

connect.createServer(
connect.static("../angularjs")
).listen(5000);

这个简单的文件创建了一个基本的Web服务器,将会在端口5000上响应请求,对外提供名为angularjs的文件夹下所包含的各个文件,该文件夹与Node.js安装目录处在同一级

安装测试系统

1
npm install -g karma

创建AngularJS文件夹
在开发过程中这个文件夹将包含你的AngularJS应用程序

  1. 获取AngularJS库
  2. 获取AngularJS的附加物
  3. 获取Bootstrap
  4. 获取Deployd

执行一个简单的测试
为确保所有的一切都已安装好并能够工作,在angularjs文件夹下创建一个名为test.html的HTML文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html ng-app>

<head>
<title>First Test</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
</head>

<body>
<div class="btn btn-default">{{"AngularJS"}}</div>
<div class="btn btn-success">Bootstrap</div>
</body>

</html>

1.启动Web服务器
要启动Web服务器,从Node.js的安装目录下运行下列命令

1
node server.js

这将加载之前创建的server.js,并在端口5000上开始监听HTTP请求

提示:
以上示例如果不能运行,则是因为:connect包在其最新的3.x版本的代码库中进行了一些更改,将static中间件移动到它自己的包中,你有两个选择:

  • 安装较旧的2.x版本的connect并按原样使用:
1
$ npm install connect@2.XX
  • 继续使用3.x版本的connect,并添加serve-static:
1
$ npm install serve-static

同时更新server.js文件以包含新serve-static模块:

1
2
3
4
5
6
7
var connect = require('connect'),
serveStatic = require('serve-static');

var app = connect();

app.use(serveStatic("../angularjs"));
app.listen(5000);

2.加载测试文件

第 2 章 你的第一个 AngularJS应用

准备项目

在angular文件夹下新建一个名为todo.html的HTML文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!DOCTYPE html>
<html data-ng-app>

<head>
<title>TO DO List</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
</head>

<body>
<div class="page-header">
<h1>Adam's To Do List</h1>
</div>
<div class="panel">
<div class="input-group">
<input class="form-control" />
<span class="input-group-btn">
<button class="btn btn-default">Add</button>
</span>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
<tr>
<td>Buy Flowers</td>
<td>No</td>
</tr>
<tr>
<td>Get Shoes</td>
<td>No</td>
</tr>
<tr>
<td>Collect Tickets</td>
<td>Yes</td>
</tr>
<tr>
<td>Call Joe</td>
<td>No</td>
</tr>
</tbody>
</table>
</div>
</body>

</html>

这个文件还没有用到AngularJS,现在todo.html文件只包含了一些静态HTML元素,提供了一个待办事项应用的骨架

使用 AngularJS

将AngularJS应用到HTML文件
将AngularJS加入到HTML文件中是挺简单的,只需简单增加一个script元素来引入angular.js文件,创建一个AngularJS模块,并对html元素应用一个属性即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<!DOCTYPE html>
<html ng-app="todoApp">

<head>
<title>TO DO List</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script src="angular.js"></script>
<script>
var todoApp = angular.module("todoApp", []);
</script>
</head>

<body>
<div class="page-header">
<h1>Adam's To Do List</h1>
</div>
<div class="panel">
<div class="input-group">
<input class="form-control" />
<span class="input-group-btn">
<button class="btn btn-default">Add</button>
</span>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
<tr>
<td>Buy Flowers</td>
<td>No</td>
</tr>
<tr>
<td>Get Shoes</td>
<td>No</td>
</tr>
<tr>
<td>Collect Tickets</td>
<td>Yes</td>
</tr>
<tr>
<td>Call Joe</td>
<td>No</td>
</tr>
</tbody>
</table>
</div>
</body>

</html>

AngularJS应用是由一个或多个模块组成。模块调用是由angular.module方法而创建的

1
2
3
...
var todoApp = angular.module("todoApp", []);
...

传递给angular.module方法的参数是要创建的模块名以及一个由所需要的其他模块构成的数组。我创建了一个名为todoApp的模块,遵循了那个让人有点困惑的将App附加到模块名称后面的习惯用法,并通过将空数组传递给第二个参数来告诉AngularJS不再需要其他模块

警告:
一个常见的错误是忽略了依赖参数,这将会导致错误。你必须提供一个依赖参数,如果没有依赖时就使用一个空数组

我通过ng-app属性告诉AngularJS如何使用这个模块。AngularJS通过增加新元素、属性、CSS类和特殊注释(虽然鲜有使用)的方法来扩展HTML,完成工作。AngularJS库动态地编译一个文档中的HTML,以定位和处理这些附加品,并创建应用程序。你可以使用JavaScript代码对内置功能进行补充,定制应用程序的行为,并定义自己向HTML的附加品

注意:
只有当浏览器对内容完成加载,并使用标准DOM API和JavaScript特性来添加或删除元素、设置事件处理器等等时,AngularJS库才会对HTML元素进行评估。在AngularJS开发中没有明显的编译步骤,只需要修改你的HTML和JavaScript文件并将其加载到浏览器中

AngularJS对HTML的最重要的附加品是ng-app属性,该属性指定了示例中的html元素包含一个应当被AngularJS编译和处理的模块。当AngularJS是唯一被使用的JavaScript框架时,惯例是对HTML元素使用ng-app属性。如果你在将AngularJS与其他技术如jQuery混用,你可以通过将ng-app属性应用到文档里的某个元素来缩小AngularJS应用的边界

对HTML应用AngularJS
向HTML文档中添加非HTML标准的属性和元素看起来会有些奇怪。如果只是不习惯类似于ng-app这样的属性用法,有一种可供使用的替代方法。你可以使用data属性,即那些以data-为前缀的AngularJS指令

1
2
3
...
<html data-ng-app="todoApp">
...

创建数据模型
AngularJS支持MVC模式。遵循MVC模式需要将你的应用程序分成3个不同的区域:程序中的数据(模型)、对数据进行操作的逻辑(控制器),以及显示数据的逻辑(视图)
在我的待办事项程序中的数据现在分布在各个HTML元素之间。用户名包含在header中,如下:

1
2
3
...
<h1>Adam's To Do List</h1>
...

待办事项项目的细节包含在表格中的td元素里,如下:

1
2
3
...
<tr><td>Buy Flowers</td><td>No</td></tr>
...

我的第一个任务就是将所有数据放到一起,并将数据从HTML元素中分离出来,以便创建一个模型。因为AngularJS程序存在于浏览器中,我需要使用一个script元素中的JavaScript来定义我的数据模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html ng-app="todoApp">

<head>
<title>TO DO List</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script src="angular.js"></script>
<script>
var model = {
user: "Adam",
items: [{ action: "Buy Flowers", done: false },
{ action: "Get Shoes", done: false },
{ action: "Collect Tickets", done: true },
{ action: "Call Joe", done: false }]
};

var todoApp = angular.module("todoApp", []);
</script>
</head>

<body>
<div class="page-header">
<h1>To Do List</h1>
</div>
<div class="panel">
<div class="input-group">
<input class="form-control" />
<span class="input-group-btn">
<button class="btn btn-default">Add</button>
</span>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</body>

</html>

提示:
这里做了简化。模型也能够包含创建、加载、存储和修改数据对象所需的逻辑。在一个AngularJS应用中,这些逻辑经常在服务器上,并通过Web服务器来访问

我定义了一个名为model的JavaScript对象,具有与分布在各个HTML元素中的数据相对应的属性。user属性定义了用户名,items属性定义了一个对象数组,描述了各个待办事项
通常在定义一个模型时,不需要同时定义MVC模式的其他部分,但是我想演示如何搭建这个简单的AngularJS应用程序,所以也同时定义了其他部分

提示:
在任何AngularJS开发项目里,总有一段时期需要定义MVC模式的主要部分并整合到一起。在这段时期里,会让人觉得好像在倒退,特别是在类似本章这样的静态程序上工作时。但是这段时间的初始投资最终将会得到回报。当开始搭建一个更复杂和实际的AngularJS应用程序时,开始时需要有许多设置和配置,但是很快就能使各种特性就位

创建控制器
控制器定义了用于支持视图的业务逻辑,描述控制器的最好方式是解释清楚它不应包含什么样的逻辑,以及控制器中剩下了哪些逻辑
处理存储或读取数据的逻辑是模型的一部分。处理将数据格式化并显示给用户的逻辑是视图的一部分。控制器位于模型和视图之间并连接它们。控制器对用户交互做出响应,更新模型中的数据并向视图提供所需要的数据

控制器是由调用angular.module所返回的module对象上的controller方法创建的。传给controller方法的参数是新控制器的名称和一个将会被调用的函数,用于定义控制器功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!DOCTYPE html>
<html ng-app="todoApp">

<head>
<title>TO DO List</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script src="angular.js"></script>
<script>
var model = {
user: "Adam",
items: [{ action: "Buy Flowers", done: false },
{ action: "Get Shoes", done: false },
{ action: "Collect Tickets", done: true },
{ action: "Call Joe", done: false }]
};

var todoApp = angular.module("todoApp", []);

todoApp.controller("ToDoCtrl", function ($scope) {
$scope.todo = model;
});
</script>
</head>

<body ng-controller="ToDoCtrl">
<div class="page-header">
<h1>To Do List</h1>
</div>
<div class="panel">
<div class="input-group">
<input class="form-control" />
<span class="input-group-btn">
<button class="btn btn-default">Add</button>
</span>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</body>

</html>

惯例一般是对控制器命名为<Name>Ctrl,这里的Name帮助你识别出该控制器在你的应用程序中负责做什么,真正的应用程序一般会生成多个控制器

提示:
像这样的控制器命名方式仅仅是一种习惯,你可以自由选择任何喜欢的名称。遵循广泛使用的惯例是为了能使熟悉AngularJS的程序员快速了解你的项目结构

控制器的主要目的之一是为了向视图提供其所需的数据。你不会总是希望视图具有访问整个模型的权限,所以需要使用控制器明确地选出那部分可用的数据,被称作scope
传给我的控制器函数的参数叫做$scope——也就是说,在$符号后紧跟着scope一词。在一个AngularJS应用中,以$开头的变量名表示AngularJS提供的内置特性。当你看到这个$符号使,一般是指一个内置服务,是一种自包含的组件,能够对多个控制器提供特性,但是$scope是比较特殊的,常用于向视图暴露数据和功能
对于这个应用,我想让视图中可以使用整个model变量,所以我在$scope服务对象上定义了一个名为todo的属性,并将整个model变量赋给它,如下:

1
2
3
...
$scope.todo = model;
...

创建视图
通过将控制器所提供的数据和用于为浏览器生成显示内容的HTML元素绑定在一起,可以生成视图。这需要使用到一种被称为数据绑定的注释来使用模型数据对HTML文档进行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
...
<body ng-controller="ToDoCtrl">
<div class="page-header">
<h1>
{{todo.user}}'s To Do List
<span class="label label-default">{{todo.items.length}}</span>
</h1>
</div>
<div class="panel">
<div class="input-group">
<input class="form-control" />
<span class="input-group-btn">
<button class="btn btn-default">Add</button>
</span>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in todo.items">
<td>{{item.action}}</td>
<td>{{item.done}}</td>
</tr>
</tbody>
</table>
</div>
</body>
...

1.插入模型值
AngularJS使用两对大括号()表示一个数据绑定表达式。表达式的内容会被当作JavaScript进行计算,仅限于通过控制器赋给作用域上的数据和函数。在本例中,我只能访问模型中在定义控制器时赋给类$scope对象的那一部分,通过使用在$scope对象上创建的属性名即可
这就是说,如果我想访问model.user属性,我得定义一个引用todo.user的数据绑定表达式,这是因为我将model对象赋给了$scope.todo属性
AngularJS在文档中对HTML进行编译,发现ng-controller属性后,调用ToDoCtrl函数设置将被用于创建视图的作用域。当遇到每个数据绑定表达式后,AngularJS会查找$scope对象上的具体值,并向HTML文档中插入该值。这被称为数据绑定或者模型绑定

2.计算表达式
数据绑定表达式的内容可以是任何有效的JavaScript语句,也就是说你可以执行从模型中创建新数据的操作,如下:

1
2
3
4
5
6
...
<h1>
{{todo.user}}'s To Do List
<span class="label label-default">{{todo.items.length}}</span>
</h1>
...

AngularJS计算这个表达式并显示数组中的元素个数,以告诉用户在待办事项列表中有多少个元素

提示:
你应该仅使用表达式来执行一些简单的操作,只为显示而准备数据值。不要使用数据绑定来执行复杂逻辑或者对模型进行操作,那是控制器该做的事

3.使用指令
表达式还经常和指令一起使用,用于告诉AngularJS你希望内容如何被处理。在示例中用到了ng-repeat属性,这个指令用于告诉AngularJS从一个集合中的各个对象生成所应用到的元素及其内容,如下:

1
2
3
4
5
6
...
<tr ng-repeat="item in todo.items">
<td>{{item.action}}</td>
<td>{{item.done}}</td>
</tr>
...

ng-repeat属性值的格式为<name> in <collection>。我在todo.items中指定了item,就是说为todo.items数组中的每个对象生成tr元素和包含的td元素,并将数组中的对象逐个赋值给一个名为item的变量
使用变量item,我就能够为数组中的每个对象的属性定义绑定表达式,产生如下的HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
<tr ng-repeat="item in todo.items" class="ng-scope">
<td class="ng-binding">Buy Flowers</td>
<td class="ng-binding">false</td>
</tr>
<tr ng-repeat="item in todo.items" class="ng-scope">
<td class="ng-binding">Get Shoes</td>
<td class="ng-binding">false</td>
</tr>
<tr ng-repeat="item in todo.items" class="ng-scope">
<td class="ng-binding">Collect Tickets</td>
<td class="ng-binding">true</td>
</tr>
<tr ng-repeat="item in todo.items" class="ng-scope">
<td class="ng-binding">Call Joe</td>
<td class="ng-binding">false</td>
</tr>
...

指令是AngularJS的工作机制的核心,ng-repeat指令将会是其中频繁使用的一个指令

基本功能之外

使用双向模型绑定
在前一节中所使用的绑定被称为单向绑定,其值是从模型中取得的,并用于操作模板中的元素
AngularJS走的更远一步,还提供了双向绑定,模型用于生成元素,元素中的变化也能引起模型中的相应变化。为了演示双向绑定是如何实现的,我修改了todo.html文件以便用复选框代表每个任务的状态:

1
2
3
4
5
6
7
8
9
...
<tr ng-repeat="item in todo.items">
<td>{{item.action}}</td>
<td>
<input type="checkbox" ng-model="item.done" />
</td>
<td>{{item.done}}</td>
</tr>
...

我增添了一个新的td元素,以包含一个复选框类型的input元素。重要的增加之处在于ng-model属性,用于告诉AngularJS在input元素和对应的数据对象的done属性之间创建一个双向绑定
当HTML第一次被编译时,AngularJS将使用done属性的值来设置input元素的值
双向绑定可被应用到接收用户输入的元素上,一般即是与HTML表单元素相关联的元素。具有灵活而动态的模型使得使用AngularJS创建复杂的应用程序变得简单

创建和使用控制器行为
控制器在作用域上定义行为。行为是对模型中的数据进行操作的函数,用于实现应用程序的业务逻辑。控制器定义的行为用于向视图提供数据并显示给用户,并且根据用户交互更新模型
为了演示这一简单的行为,我打算修改改todo.html的header中右边显示的标签,以便使其仅显示未完成待办事项的个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<!DOCTYPE html>
<html ng-app="todoApp">

<head>
<title>TO DO List</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script src="angular.js"></script>
<script>
var model = {
user: "Adam",
items: [{ action: "Buy Flowers", done: false },
{ action: "Get Shoes", done: false },
{ action: "Collect Tickets", done: true },
{ action: "Call Joe", done: false }]
};

var todoApp = angular.module("todoApp", []);

todoApp.controller("ToDoCtrl", function ($scope) {
$scope.todo = model;

$scope.incompleteCount = function () {
var count = 0;
angular.forEach($scope.todo.items, function (item) {
if (!item.done) { count++ }
});
return count;
}
});
</script>
</head>

<body ng-controller="ToDoCtrl">
<div class="page-header">
<h1>
{{todo.user}}'s To Do List
<span class="label label-default" ng-hide="incompleteCount() == 0">
{{incompleteCount()}}
</span>
</h1>
</div>
<div class="panel">
<div class="input-group">
<input class="form-control" />
<span class="input-group-btn">
<button class="btn btn-default">Add</button>
</span>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in todo.items">
<td>{{item.action}}</td>
<td>
<input type="checkbox" ng-model="item.done" />
</td>
</tr>
</tbody>
</table>
</div>
</body>

</html>

行为是通过在传入给控制器的$scope对象上添加函数而定义的。在示例中,我定义了一个函数用于返回未完成项目的数量,该数量是通过遍历$scope.todo.items数组中的对象,对于done属性为false的对象进行计数而得到的

提示:
我使用angular.forEach方法来遍历数据数组中的内容。AngularJS包含了一些有用的工具方法,对JavaScript语言形成很好的补充

向$scope对象添加的函数所赋给的属性名称,被用作行为名。行为名叫做incompleteCount,我可以在ng-controller属性的作用域中调用它,ng-controller属性会将控制器应用到构成视图的HTML元素上
在示例中我将incompleteCount行为使用了两次。第一次是控制标签显示或隐藏,第二次是作为一个简单的数据绑定用于显示项目个数,如下:

1
2
3
4
5
...
<span class="label label-default" ng-hide="incompleteCount() == 0">
{{incompleteCount()}}
</span>
...

如果赋给属性值ng-hide的表达式计算结果为true,ng-hide指令将会隐藏所使用到的元素

注意:
我调用该行为时使用了圆括号。你可以将对象作为参数传递给行为,这使得创建可使用不同数据对象的通用行为成为可能。我的应用程序足够简单,因此我决定不传入任何参数,而是直接从控制器的$scope对象中获取所需数据

使用依赖于其他行为的行为
始终贯穿于AngularJS的主题之一便是HTML、CSS和JavaScript等的潜在特性是如何被吸纳到Web应用程序开发中的。举个例子,因为行为是通过JavaScript函数而创建的,所以你可以在同一个控制器中其他行为所提供的功能的基础上创建新的行为,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE html>
<html ng-app="todoApp">

<head>
<title>TO DO List</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script src="angular.js"></script>
<script>
var model = {
user: "Adam",
items: [{ action: "Buy Flowers", done: false },
{ action: "Get Shoes", done: false },
{ action: "Collect Tickets", done: true },
{ action: "Call Joe", done: false }]
};

var todoApp = angular.module("todoApp", []);

todoApp.controller("ToDoCtrl", function ($scope) {
$scope.todo = model;

$scope.incompleteCount = function () {
var count = 0;
angular.forEach($scope.todo.items, function (item) {
if (!item.done) { count++ }
});
return count;
}

$scope.warningLevel = function () {
return $scope.incompleteCount() < 3 ? "label-success" : "label-warning";
}
});
</script>
</head>

<body ng-controller="ToDoCtrl">
<div class="page-header">
<h1>
{{todo.user}}'s To Do List
<span class="label label-default" ng-class="warningLevel()" ng-hide="incompleteCount() == 0">
{{incompleteCount()}}
</span>
</h1>
</div>

<!-- ...elements omitted for brevity... -->

</body>

</html>

我定义了一个名为warningLevel的新行为,该行为基于未完成事项的数目返回一个Bootstrap CSS类名,而未完成事项数是通过调用incompleteCount行为而得到的。这种方式减少了控制器中的重复逻辑
我使用ng-class指令来应用warningLevel行为

1
2
3
...
<span class="label label-default" ng-class="warningLevel()" ng-hide="incompleteCount() == 0">
...

提示:
注意这个span元素有两个指令,每一个都依赖于不同的行为。你可以自由地将行为和指令组合起来,以实现在程序中所需要的效果

响应用户交互
行为和指令结合在一起产生了AngularJS应用中的许多功能。最强大的组合之一便是将指令和行为用于响应用户交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<!DOCTYPE html>
<html ng-app="todoApp">

<head>
<title>TO DO List</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script src="angular.js"></script>
<script>
var model = {
user: "Adam",
items: [{ action: "Buy Flowers", done: false },
{ action: "Get Shoes", done: false },
{ action: "Collect Tickets", done: true },
{ action: "Call Joe", done: false }]
};

var todoApp = angular.module("todoApp", []);

todoApp.controller("ToDoCtrl", function ($scope) {
$scope.todo = model;

$scope.incompleteCount = function () {
var count = 0;
angular.forEach($scope.todo.items, function (item) {
if (!item.done) { count++ }
});
return count;
}

$scope.warningLevel = function () {
return $scope.incompleteCount() < 3 ? "label-success" : "label-warning";
}

$scope.addNewItem = function (actionText) {
$scope.todo.items.push({ action: actionText, done: false });
}
});
</script>
</head>

<body ng-controller="ToDoCtrl">
<div class="page-header">
<h1>
{{todo.user}}'s To Do List
<span class="label label-default" ng-class="warningLevel()" ng-hide="incompleteCount() == 0">
{{incompleteCount()}}
</span>
</h1>
</div>
<div class="panel">
<div class="input-group">
<input class="form-control" ng-model="actionText" />
<span class="input-group-btn">
<button class="btn btn-default" ng-click="addNewItem(actionText)">Add</button>
</span>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in todo.items">
<td>{{item.action}}</td>
<td>
<input type="checkbox" ng-model="item.done" />
</td>
</tr>
</tbody>
</table>
</div>
</body>

</html>

我增加了一个名为addNewItem的行为,能够取得新的待办事项的文本并向数据模型中添加一个对象,将该文本用作action属性的值并设置done属性为false,类似这样:

1
2
3
4
5
...
$scope.addNewItem = function (actionText) {
$scope.todo.items.push({ action: actionText, done: false });
}
...

本例中的神奇之处在于对指令的两处使用:

1
2
3
...
<input class="form-control" ng-model="actionText" />
...

这是与设置复选框时所使用的同一个ng-model指令,当使用表单元素时将会多次遇到这个指令。需要注意的是我为指令指定了一个属性名,用于更新本不是模型的一部分。ng-model指令将会在控制器的作用域中为我动态地创建这个属性,实际上是创建出了用于处理用户输入的动态模型属性。在本例中增添的第二处指令里我使用了这个动态属性:

1
2
3
...
<button class="btn btn-default" ng-click="addNewItem(actionText)">Add</button>
...

ng-click指令设置了一个当click事件被触发时的处理器,将会计算一个表达式,在这里,该表达式调用addNewItem行为,传入动态的actionText属性作为参数

提示:
你很可能已经被教导过不要为个别元素添加事件处理代码,所以将ng-click指令应用于button元素很可能看起来有点奇怪。不要担心——当AngularJS编译HTML文档并遇到该指令时,它会不引人注意地设置一个遵循JavaScript方式的处理器,这样事件处理器代码就与元素分隔开了。将AngularJS指令与编译过程中由那些指令产生的HTML和JavaScript区分开是非常重要的

对模型数据过滤和排序

1
2
3
4
5
6
7
8
9
10
...
<tbody>
<tr ng-repeat="item in todo.items | filter:{done: false} | orderBy:'action'">
<td>{{item.action}}</td>
<td>
<input type="checkbox" ng-model="item.done" />
</td>
</tr>
</tbody>
...

过滤器可被应用于数据模型的任何部分,在这里你能够看到我使用了过滤器来控制被ng-repeat指令用于操作table元素中待办事项列表项目详情信息的数据。我使用了两个过滤器:filter和orderBy
filter过滤器基于所配置的条件筛选对象。orderBy过滤器对数据项进行排序

提示:
注意在使用orderBy过滤器时,我对所指定的用于排序的属性名是作为一个字符串常量使用的。默认情况下,AngularJS假设一切都是由作用域所定义的属性,不带引号的,将会视图查找一个名为action的作用域属性。这在要通过程序定义值时是非常有帮助的,但是在想指定为一个常量时却意味着要记得使用常量

改进过滤器
前一个例子演示了过滤器特性是如何工作的,但结果却没有多大意义,因为被勾选中的项会永远地对用户隐藏。幸运的是,创建一个自定义的过滤器是件简单的事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script>
var model = {
user: "Adam",
items: [{ action: "Buy Flowers", done: false },
{ action: "Get Shoes", done: false },
{ action: "Collect Tickets", done: true },
{ action: "Call Joe", done: false }]
};

var todoApp = angular.module("todoApp", []);

todoApp.filter("checkedItems", function () {
return function (items, showComplete) {
var resultArr = [];
angular.forEach(items, function (item) {
if (item.done == false || showComplete == true) {
resultArr.push(item);
}
});
return resultArr;
}
});

todoApp.controller("ToDoCtrl", function ($scope) {
$scope.todo = model;

// ...statements omitted for brevity...
});
</script>

AngularJS模块对象所定义的filter方法用于创建一个过滤器工厂,该工厂会返回一个函数用于过滤一组数据对象。目前暂时不要担心工厂这部分细节,只需要知道使用filter方法需要传入一个函数,该函数中需要一个能够返回过滤后数据的函数就足够了。我对过滤器所起的名字是checkedItems,实际执行过滤功能的函数有两个参数:

1
2
3
...
return function (items, showComplete) {
...

参数items是由AngularJS提供的,是应当被过滤的对象集合。在使用过滤器时我将提供showComplete参数的值,该值用于决定已经被标记为完成的项是否会被包含在过滤后的数据中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
...
<div class="panel">
<div class="input-group">
<input class="form-control" ng-model="actionText" />
<span class="input-group-btn">
<button class="btn btn-default" ng-click="addNewItem(actionText)">Add</button>
</span>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in todo.items | checkedItems:showComplete | orderBy:'action'">
<td>{{item.action}}</td>
<td>
<input type="checkbox" ng-model="item.done" />
</td>
</tr>
</tbody>
</table>

<div class="checkbox-inline">
<label><input type="checkbox" ng-model="showComplete"> Show Complete</label>
</div>
</div>
...

我增加了一个复选框,使用ng-model指令来设置一个名为showComplete的模型值,该值通过表格中的ng-repeat指令传递给我的自定义过滤器:

1
2
3
...
<tr ng-repeat="item in todo.items | checkedItems:showComplete | orderBy:'action'">
...

自定义过滤器的语法与内置过滤器所支持的语法相同。我指定了通过filter方法所创建的过滤器的名称,随后跟一个:(冒号),然后跟随着我要传递给过滤器函数的模型属性名

通过Ajax获取数据
我要做的最后一项修改是通过一个Ajax请求以JSON数据形式获取待办事项列表的数据。我在angularjs文件夹下创建了一个名为todo.json的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
"action": "Buy Flowers",
"done": false
},
{
"action": "Get Shoes",
"done": false
},
{
"action": "Collect Tickets",
"done": true
},
{
"action": "Call Joe",
"done": false
}
]

接下在修改todo.html从todo.json文件中加载数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
...
<script>
var model = {
user: "Adam"
};

var todoApp = angular.module("todoApp", []);

todoApp.run(function ($http) {
$http.get("todo.json").then(function (data) {
model.items = data.data;
});
});

todoApp.filter("checkedItems", function () {
return function (items, showComplete) {
var resultArr = [];
angular.forEach(items, function (item) {
if (item.done == false || showComplete == true) {
resultArr.push(item);
}
});
return resultArr;
}
});

todoApp.controller("ToDoCtrl", function ($scope) {
$scope.todo = model;

$scope.incompleteCount = function () {
var count = 0;
angular.forEach($scope.todo.items, function (item) {
if (!item.done) { count++ }
});
return count;
}

$scope.warningLevel = function () {
return $scope.incompleteCount() < 3 ? "label-success" : "label-warning";
}

$scope.addNewItem = function (actionText) {
$scope.todo.items.push({ action: actionText, done: false });
}
});
</script>
...

我从静态定义的数据模型中移除了items数组,并增加了一个对run方法的调用,该方法是由AngularJS模型对象所定义的。run方法接受一个函数,并仅在AngularJS执行完初始化设置后运行一次,长用于一次性的任务。我对传给run方法的函数指定了$http作为参数,告诉AngularJS我要使用对Ajax请求提供支持的服务对象。这种使用参数告诉AngularJS需要哪些特性方法,是被称为依赖注入的方法的一部分
$http服务提供了对底层Ajax请求的访问功能。与用于和RESTful的Web服务交互的$resource服务相比较,这里的底层显得并不那么底层。我使用$http.get方法来创建一个HTTP GET请求,向服务器请求todo.json文件

1
2
3
4
5
...
$http.get("todo.json").then(function (data) {
model.items = data.data;
});
...

从get方法得到的结果是一个promise对象,该对象用于表示将在未来完成的工作。调用then方法能够让我指定一个将在发往服务器的Ajax请求完成时被调用的函数,而从服务器获取到的JSON数据将会被解析并创建一个JavaScript对象,传入给我的then函数作为data参数,我使用收到的data参数对模型进行更新

1
2
3
4
5
...
$http.get("todo.json").then(function (data) {
model.items = data.data;
});
...

第 3 章 结合背景理解 AngularJS

理解 AngularJS 的擅长之处

AngularJS将那些曾经仅对服务器端开发者可用的功能完整地搬到了浏览器端。这意味着使用了AngularJS的HTML文档每次加载时,AngularJS会有许多事情要做——需要编译HTML元素,需要计算数据绑定,需要执行指令等等
这类工作需要时间去执行,所需时长取决于HTML文档及其相关联的JavaScript代码的复杂程度,而且关键是浏览器的质量和设备处理能力
因此,优化的目标应该是尽可能地降低这些设置的执行频率,并在其执行时尽可能多地向用户发送应用的更多内容。这意味着需要你仔细考虑所搭建的Web应用程序类型。广义上来讲存在两种类型的Web应用程序:回合式和单页面

理解回合式和单页面应用程序
很长一段时间以来,Web应用程序都是遵循回合式模式开发的。由浏览器向服务器请求一个初始的HTML文档。用户交互会使得浏览器发送请求并接收一个全新的HTML文档。在这类应用程序中,浏览器本质上是一个HTML内容的解析引擎,所有应用程序逻辑和数据都保留在服务器上。浏览器发出一系列无状态的HTTP请求,服务器处理这些请求并动态生成HTML文档
现在许多Web开发仍然是为回合式应用而准备的,尤其是因为这对浏览器的要求很少,能够保证最大限度地对客户端的支持。但回合式应用程序存在一些严重的不足之处:用户在下一个HTML文档被请求并且加载之前必须等待,它需要大型的服务器端基础设施来处理所有请求并管理所有的应用程序状态,需要许多带宽,因为每个HTML文档必须是自包含的
单页面应用程序则另辟蹊径。一个初始的HTML文档被发送给浏览器,但是用户交互所产生的Ajax请求只会请求较小的HTML片段,或者要插入到已有的显示给用户元素中的数据
初始的HTML文档不会被再次加载或者替换,在Ajax请求被异步执行时用户还可以继续与已有的HTML进行交互,即使这只意味着只能看到“数据加载中”这样的信息
现在大多数应用会落入到这两种极端之间,倾向于使用以JavaScript增强的基本回合式模型,以减少整个页面的变化次数,虽然重点往往在于减少执行客户端校验所产生的表单错误的次数
对于更贴近单页面模型的应用程序,AngularJS能够对最初付出的工作量给予最大的回报。这并不是说回合式应用程序就不能使用AngularJS——你当然可以,但是有其他更简单和更适合于分离的HTML页面的技术,例如jQuery
AngularJS以单页面应用程序和复杂的回合式应用程序见长。对于较简单的项目,一般来说jQuery或者类似的替代者会是更好的选择
现在的Web应用项目有一种逐步向单页应用程序模型转移的趋势,这正是AngularJS的擅长之处,不仅仅是因为初始化过程得到了优化,更是因为使用MVC模式所带来的好处确实在逐步证明其在大型和复杂项目上的优势

AngularJS与jQuery
AngularJS与jQuery在Web应用开发上走的是不同的路。jQuery完全是通过显式操作浏览器中的DOM来创建应用程序。AngularJS采用的方法则是将浏览器吸收为应用程序开发的基础
但是使用jQuery编写和管理大型应用将会比较困难,全面的单元测试也将会是一个挑战
我喜欢使用AngularJS工作的原因之一是,它是建立于jQuery的核心功能之上的。事实上AngularJS包含了一个裁剪版的jQuery,叫做jqLite,在编写自定义指令时将用到。而且,如果你将jQuery加入到HTML文档中,AngularJS将会自动检测到并优先使用jQuery替代jqLite,尽管会很少需要这么做
那么,简而言之,对于单元测试不那么重要而且需要立即得到结果的低复杂度Web应用,适用于jQuery。jQuery对于增强回合式类型的Web应用生成的HTML也是非常理想的,因为你可以轻松使用jQuery而无需修改由服务器生成的HTML内容。对于更复杂一些的单页面Web应用,当你有时间精心设计和规划时,以及当你能够轻松控制由服务器生成的HTML时,适于使用AngularJS

理解 MVC 模式

使用MVC模式的关键前提在于实现关注点分离,即应用程序中的数据模型与业务逻辑和展示逻辑解耦。在客户端Web开发中,这意味着将数据、操作数据的逻辑和HTML元素相分离。结果就是得到一个更为容易开发、维护和测试的客户端应用程序
主要的三个构件就是模型、视图和控制器。应用于服务器端开发的MVC模式的传统形式:

AngularJS是在浏览器中工作的,导致对MVC的形式产生一些影响

理解模型
模型包含了用户赖以工作的数据。有两种广义上的模型:视图模型,只表示从控制器传往视图的数据;领域模型,包含了业务领域的数据,以及用于创建、存储和操作这些数据的各种操作、转换和规则,统称为模型逻辑

提示:
许多MVC模式的新手会对在数据模型中包含逻辑的理念感到困惑,相信MVC模式的理念应该是将数据从逻辑中剥离出来。这是一种误解:MVC框架的目标是将一个应用程序分成三部分功能区域,每一部分都可能同时包含逻辑与数据。其目标并不是从模型中消除逻辑。而是为了确保模型中所包含的逻辑只是用于创建和管理模型数据的

使用MVC模式构建的应用程序中的模型应该:

  • 包含领域数据
  • 包含创建、管理和修改领域数据的逻辑(这意味着要通过Web服务来执行远程逻辑)
  • 提供整洁的API,能够暴露模型数据以及之上的操作

模型不应该:

  • 暴露模型数据是如何获取或管理的细节(换句话说就是数据存储逻辑)
  • 包含根据用户交互对模型进行转换的逻辑(因为这是控制器的职责)
  • 包含将数据显示给用户的逻辑(因为这是视图的职责)

确保将模型与控制器和视图分离的好处是使你可以更容易地测试你的逻辑,并且使得对整个程序的优化和维护变得简单和容易
最好的领域模型应该包含获取和存储持久化数据的逻辑,以及创建、读取、更新和删除操作(CRUD操作)。这也可以被理解为模型可以直接包含逻辑,但是更常见的是包含那些用于调用RESTful的Web服务以调用服务端数据库操作的逻辑

理解控制器
在一个AngularJS应用程序中,控制器作为数据模型和视图之间的渠道,控制器会向作用域中添加业务领域逻辑(行为/动作方法),而作用域是模型的子集

使用MVC模式构建的控制器应当:

  • 包含初始化作用域所需的逻辑
  • 包含视图所需的用于表示作用域中的数据的逻辑/行为
  • 包含根据用户交互来更新作用域所需的逻辑/行为

控制器不应当:

  • 包含操作DOM的逻辑(那是视图的职责)
  • 包含管理数据持久化的逻辑(那是模型的职责)
  • 在作用域之外操作数据

理解视图数据
领域模型并不是AngularJS应用程序中的唯一数据。控制器可以创建视图数据(视图模型数据/视图模型),以简化视图的定义。视图数据不会被持久化,而且要么是通过综合领域模型数据的几部分而成的,要么是存在于对用户交互的响应之中。视图数据通常是通过控制器作用域来创建和访问的

理解视图
AngularJS视图是通过HTML元素来定义的,而这些元素是通过使用数据绑定或者指令来进行增强或生成的。正是AngularJS指令使得视图变得如此灵活,也将HTML元素变为动态Web应用的基础

视图应当:

  • 包含将数据呈现给用户所需的逻辑和标记

视图不应当:

  • 包含复杂逻辑(这最好放到控制器中去)
  • 包含创建、存储或操作领域模型的逻辑

视图可以包含逻辑,但是应该尽量简单,并有节制地使用。如果不是将最简单的方法调用或者表达式放到视图中,而是将任何东西都放进视图中,会让整个应用程序变得更难以测试和维护

理解 RESTful 服务

在AngularJS应用中的领域模型逻辑通常被拆分为客户端和服务端两部分。服务器端包含持久化存储,典型的就是数据库,还包含管理这些存储的逻辑(操作数据库)
我们并不希望客户端代码直接访问数据存储——这样会在客户端和数据存储之间产生紧耦合,使得单元测试复杂化,也使得在不修改客户端代码的情况下对数据存储的修改变得困难
通过服务器端作为中介来访问数据存储,我们可以消除紧耦合。客户端的逻辑负责从服务器端存取数据,而无须知道数据在后台是如何存储或访问的细节
有许多种在客户端和服务端之间传递数据的方法。最常见的一种是使用Ajax请求来调用服务器端的代码,让服务器发送JSON并使用HTML表单来修改数据
这种方法可以很好地工作,也是RESTful Web服务的基础,利用了HTTP请求的天然特性来执行对数据的CRUD操作

注意:
REST是一种API风格,而不是一个定义完善的规范,到底什么是Web服务的REST化是有争议的

HTTP方法所对应的常用操作

方法 描述
GET 获取URL所指定的数据对象
PUT 更新URL所指定的数据对象
POST 创建一个新的数据对象,通常使用表单数据值作为数据域
DELETE 删除URL所指定的数据对象

你不一定必须按照表中所描述的HTTP方法来执行操作。一个常见的变体是POST方法通常具有双重职责,如果某个对象存在的话将会对其进行更新,如果不存在的话则创建一个,也就是说不必使用PUT方法

幂等的HTTP方法
虽然我推荐你尽可能地贴近表中所描述的习惯用法,你仍然可以在HTTP方法与对数据存储的操作之间实现任何映射
如果你另辟蹊径,请确保利用了HTTP规范中所定义的HTTP方法的特性:
GET方法是具有无为性的也就是说对于该方法的响应中所做的操作应该只是读取数据而不会修改它。一个浏览器应当期望能够重复地发出GET请求,而不会改变服务器端的状态
PUT和DELETE方法是幂等的,也就是说多次发送同一个请求应该和只发送一次该请求具有同样的效果
POST方法既不具有无为性也不是幂等的
只有当你在实现自己的RESTful的Web服务时,所有这些才变得非常重要。如果你在编写一个消费RESTful服务的客户端,那么你只需要知道每个HTTP方法对应于哪些数据操作就可以了

常见的设计陷阱

在本节,我将介绍AngularJS项目中遇到的三个最常见的设计陷阱。这不是代码编写的错误,但却是整个Web应用范围内的问题,会妨碍项目团队享受AngularJS和MVC模式所带来的益处

将逻辑放到错误的地方
最常见的问题是把逻辑放到了错误的位置,破坏了MVC关注点的分离。以下是这种问题的三个最常见的种类:

  • 将业务逻辑放到视图中,而不是控制器中
  • 将领域逻辑放到控制器中,而不是模型中
  • 在使用RESTful的服务时将数据存储逻辑放到客户端模型中

提示:
找到逻辑应该放到哪儿的感觉需要经验,但是如果使用单元测试你将更早地找到问题,因为覆盖该逻辑所需写的测试将无法很好地与MVC模式相融合

当你积累了更多的AngularJS开发经验之后,知道应该把逻辑放到哪儿就会渐渐成为一种习惯,但是还得遵循以下这三条规则:

  • 视图逻辑应该仅为显示准备数据,并且永远都不应该修改模型
  • 控制器逻辑永远都不应该直接创建、更新或删除模型中的数据
  • 客户端永远都不应该直接访问数据存储

如果你在开发时牢记这些,将会避免最常见的大多数问题

采用数据存储所依赖的数据格式
下一个问题将会出现在当开发团队搭建的应用程序依赖于服务端数据存储的某些特性时。最近我与一个项目团队一起工作,他们所搭建的客户端利用了他们的服务端SQL Server的某些特殊数据格式。他们所遇到的问题时他们需要升级到一个更健壮的数据库,而该数据库对于关键数据类型使用的是不同的表示方法
在一个设计良好的从RESTful服务获取数据的AngularJS应用程序中,服务端的职责应当是隐藏数据存储的实现细节,并向客户端以一种合适的数据格式表达数据,使得客户端使用起来简洁易用。例如,要决定客户端需要如何表示日期,并确保使用的是数据存储中的格式——那么如果数据存储本身无法再支持这种格式,就应当是服务器端的职责来执行各种转换

墨守成规
AngularJS最强大的特性之一是它是在jQuery基础上搭建的,特别是对于指令特性。然而,这所带来的问题是,理论上会使得在项目上使用AngularJS变得简单,但实际上以在幕后使用jQuery而告终
这看起来也许不像是设计问题,但是往往会使应用程序的轮廓发生变形,因为使用jQuery不易分离MVC的各个组件,并使得你所创建的Web应用难以测试、优化和维护。如果在一个AngularJS应用中你在直接使用jQuery操作DOM,那么你就遇到问题了
正如我在本章中前面介绍的,AngularJS并不是包揽任何功能的万能利器,在项目开发阶段决定打算使用哪些工具是很重要的。如果你打算使用AngularJS,那么你需要确保不要回退到依赖于那些jQuery的隐藏捷径,这最后将会导致无尽的问题

第 4 章 HTML 和 Bootstrap CSS 入门

了解 HTML

参见HTML5 权威指南

了解 Bootstrap

为了便于演示基本的Bootstrap特性,在angularjs文件夹下创建了一个名为bootstrap.html的HTML文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">

<head>
<title>Bootstrap Examples</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
</head>

<body>
<div class="panel">
<h3 class="panel-heading">Button Styles</h3>
<button class="btn">Basic Button</button>
<button class="btn btn-primary">Primary</button>
<button class="btn btn-success">Success</button>
<button class="btn btn-warning">Warning</button>
<button class="btn btn-info">Info</button>
<button class="btn btn-danger">Danger</button>
</div>
<div class="well">
<h3 class="panel-heading">Button Sizes</h3>
<button class="btn btn-large btn-success">Large Success</button>
<button class="btn btn-warning">Standard Warning</button>
<button class="btn btn-small btn-danger">Small Danger</button>
</div>
<div class="well">
<h3 class="panel-heading">Block Buttons</h3>
<button class="btn btn-block btn-large btn-success">Large Block Success</button>
<button class="btn btn-block btn-warning">Standard Block Warning</button>
<button class="btn btn-block btn-small btn-info">Small Block Info</button>
</div>
</body>

</html>

使用基本的Bootstrap类
Bootstrap样式是通过class属性使用的,用于关联相关的元素

提示:
并不是所有的Bootstrap样式都需要显式地使用class属性。标题h1-h6无论何时都可以自动地应用样式

例子中用到的基本Bootstrap类

Bootstrap类 描述
panel 表示一个具有圆形边框的面板,一个面板可以有页眉和页脚
panel-heading 为面板创建标题
btn 创建一个按钮
well 使用插图效果将元素分组

1.修改样式上下文
Bootstrap定义了一组可应用到元素上的样式上下文类,用来表示其目的。这些类的指定方式是,将基础Bootstrap类的名字(比如btn),一个连字符和primary、success、warning、info或danger这些词之一联合在一起

1
2
3
...
<button class="btn btn-primary">Primary</button>
...

上下文类必须和基础类一起使用,这也是为什么button元素同时具有btn和btn-primary类

2.修改大小
你可用通过使用大小修改类来改变某些元素被渲染样式的方式。这些类的指定方式是,将一个基础类的名字,一个连字符和lg或sm之一联合在一起

1
2
3
...
<button class="btn btn-lg btn-success">Large Success</button>
...

对于button元素,你可以使用btn-block类来创建一个能够填满合适的横向空间的按钮

1
2
3
...
<button class="btn btn-block btn-lg btn-success">Large Block Success</button>
...

用Bootstrap对表格使用样式
Bootstrap也包括对表格元素样式的支持

用于表格的Bootstrap CSS类

Bootstrap类 描述
table 对table元素及其内容使用一般样式
table-striped 对table的主体部分使用各行条纹式的样式
table-bordered 对所有行和列使用边框
table-hover 当鼠标滑过表格中的一行时显示不同的样式
table-condensed 减少表格中的空白以创建更精简的布局

所有这些类都可以直接用于table元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">

<head>
<title>Bootstrap Examples</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
</head>

<body>
<div class="panel">
<h3 class="panel-heading">Standard Table with Context</h3>
<table class="table">
<thead>
<tr>
<th>Country</th>
<th>Capital City</th>
</tr>
</thead>
<tr class="success">
<td>United Kingdom</td>
<td>London</td>
</tr>
<tr class="danger">
<td>France</td>
<td>Paris</td>
</tr>
<tr>
<td>Spain</td>
<td class="warning">Madrid</td>
</tr>
</table>
</div>
<div class="panel">
<h3 class="panel-heading">Striped, Bordered and Highlighted Table</h3>
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>Country</th>
<th>Capital City</th>
</tr>
</thead>
<tr>
<td>United Kingdom</td>
<td>London</td>
</tr>
<tr>
<td>France</td>
<td>Paris</td>
</tr>
<tr>
<td>Spain</td>
<td>Madrid</td>
</tr>
</table>
</div>
</body>

</html>

确保表格结构正确
注意,在示例中定义表格时我使用了thead元素。如果没有使用thead元素,那么浏览器将会自动地将table元素下的任何直接的tr子元素添加到一个tbody元素下。如果在使用Bootstrap时依赖于这一行为,你会得到一些奇怪的结果

使用Bootstrap创建表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">

<head>
<title>Bootstrap Examples</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
</head>

<body>
<div class="panel">
<h3 class="panel-header">
Form Elements
</h3>
<div class="form-group">
<label>Name:</label>
<input name="name" class="form-control" />
</div>
<div class="form-group">
<label>Email:</label>
<input name="email" class="form-control" />
</div>

<div class="radio">
<label>
<input type="radio" name="junkmail" value="yes" checked /> Yes, send me endless junk mail
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="junkmail" value="no" /> No, I never want to hear from you again
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" /> I agree to the terms and conditions.
</label>
</div>
<input type="button" class="btn btn-primary" value="Subscribe" />
</div>
</body>

</html>

对包含了一个label和一个input元素的div元素使用form-group类,可以应用可用于表单的基本样式

1
2
3
4
5
6
...
<div class="form-group">
<label>Email:</label>
<input name="email" class="form-control" />
</div>
...

对于其他元素有不同的类,在示例中我使用了checkbox类,也应用到了div元素上,对于那些type被设置为checkbox的input元素

1
2
3
4
5
6
7
...
<div class="checkbox">
<label>
<input type="checkbox" /> I agree to the terms and conditions.
</label>
</div>
...

使用Bootstrap创建网格
Bootstrap提供了可用于创建不同种类的网格布局的样式类,可以含有1到12列,并提供对响应式布局的支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">

<head>
<title>Bootstrap Examples</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<style>
#gridContainer {
padding: 20px;
}

.grid-row>div {
border: 1px solid lightgrey;
padding: 10px;
background-color: aliceblue;
margin: 5px 0;
}
</style>
</head>

<body>
<div class="panel">
<h3 class="panel-header">
Grid Layout
</h3>
<div id="gridContainer">
<div class="row grid-row">
<div class="col-xs-1">1</div>
<div class="col-xs-1">1</div>
<div class="col-xs-2">2</div>
<div class="col-xs-2">2</div>
<div class="col-xs-6">6</div>
</div>
<div class="row grid-row">
<div class="col-xs-3">3</div>
<div class="col-xs-4">4</div>
<div class="col-xs-5">5</div>
</div>
<div class="row grid-row">
<div class="col-xs-6">6</div>
<div class="col-xs-6">6</div>
</div>
<div class="row grid-row">
<div class="col-xs-11">11</div>
<div class="col-xs-1">1</div>
</div>
<div class="row grid-row">
<div class="col-xs-12">12</div>
</div>
</div>
</div>
</body>

</html>

表格与网格
table元素用于表示表格式的数据,却常用于在网格中展示内容。一般来说你应该使用CSS在网格中展示内容,因为使用表格会与内容和展现形式分离的原则相悖。CSS3将网格布局纳入为规范的一部分,但却没有在主流浏览器上得到一致的实现。因此最好的选项就是使用类似Bootstrap这样的CSS框架
我一直坚持遵守的另一种模式是,知道遇到了一个需要解决的问题。在Web应用需要运行在不支持CSS3布局的设备上时,我使用table元素创建网格布局。像以前那样,我的设备仍然遵循元素类型与布局相分离的模式,但是当你找不到更好的代替品时,不要害怕使用table元素作为网格的方式

Bootstrap网格布局是易于使用的。你只需要对一个div元素使用row类,就可以指定某一列,其效果是为div元素包含的内容设置为网格布局
每一行定义了12列,你可以给予子元素使用形如col-xs加列数的类名,来指定每个子元素占多少列
Bootstrap并不对一行中的元素使用任何样式

创建响应式网格
响应式网格可以根据浏览器窗口的大小调整自身布局。响应式网格的主要用途是允许移动设备和桌面设备都可以显示同样的内容,无论有多大的可用屏幕空间都可以利用上

用于响应式表格的Bootstrap CSS类

Bootstrap类 描述
col-xs-*
col-sm-* 当屏幕宽度大于768像素时水平显示网格单元
col-md-* 当屏幕宽度大于940像素时水平显示网格单元
col-lg-* 当屏幕宽度大于1170像素时水平显示网格单元

当屏幕宽度小于该类所支持的像素时,表格中的单元将以垂直形式排列,而不是水平形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">

<head>
<title>Bootstrap Examples</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<style>
#gridContainer {
padding: 20px;
}

.grid-row>div {
border: 1px solid lightgrey;
padding: 10px;
background-color: aliceblue;
margin: 5px 0;
}
</style>
</head>

<body>
<div class="panel">
<h3 class="panel-header">
Grid Layout
</h3>
<div id="gridContainer">
<div class="row grid-row">
<div class="col-sm-3">3</div>
<div class="col-sm-4">4</div>
<div class="col-sm-5">5</div>
</div>
<div class="row grid-row">
<div class="col-sm-6">6</div>
<div class="col-sm-6">6</div>
</div>
<div class="row grid-row">
<div class="col-sm-11">11</div>
<div class="col-sm-1">1</div>
</div>
</div>
</div>
</body>

</html>

第 5 章 JavaScript 基础

参见HTML5 权威指南

准备示例项目

理解 script 元素

使用语句

定义并使用函数

在JavaScript中可以传递对象,所以让你知道对象是否为函数十分有用。为此AngularJS提供了angular.isFunction方法

注意:
所有的AngularJS工具方法都可以通过全局的angular对象访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html>

<head>
<title>Example</title>
<script src="angular.js"></script>
<script type="text/javascript">
function printMessage(unknownObject) {
if (angular.isFunction(unknownObject)) {
unknownObject();
} else {
console.log(unknownObject);
}
}
var variable1 = function sayHello() {
console.log("Hello!");
};
var variable2 = "Goodbye!";
printMessage(variable1);
printMessage(variable2);
</script>
</head>

<body>
This is a simple example
</body>

</html>

使用变量及类型

使用基本类型

处理字符串的AngularJS方法

名称 描述
angular.isString(object) 如果参数是字符串返回true,否则返回false
angular.lowercase(string) 将参数转换为小写
angular.uppercase(string) 将参数转换为大写

创建对象

1.使用对象字面量

2.使用函数作为方法

3.扩展对象
AngularJS通过angular.extend方法,使从一个对象往另一个对象复制方法和属性变得容易

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html>

<head>
<title>Example</title>
<script src="angular.js"></script>
<script type="text/javascript">
var myData = {
name: "Adam",
weather: "sunny",
printMessages: function () {
console.log("Hello " + this.name + ". ");
console.log("Today is " + this.weather + ".");
}
};
var myExtendedObject = {
city: "London"
};
angular.extend(myExtendedObject, myData);
console.log(myExtendedObject.name);
console.log(myExtendedObject.city);

</script>
</head>

<body>
This is a simple example
</body>

</html>

在本例中,我创建了带有city属性的对象,并将它赋值给了变量myExtendedObject。然后我使用angular.extend方法从myData对象上复制所有属性和函数到myExtendedObject上去

提示:
angular.extend方法保留目标对象上的所有属性和方法。如果你想无保留地创建对象的副本,可以使用angular.copy方法代替

使用对象

1.检查对象
AngularJS提供angular.isObject方法,如果参数是对象则返回true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>

<head>
<title>Example</title>
<script src="angular.js"></script>
<script type="text/javascript">
var myObject = {
name: "Adam",
weather: "sunny",
};
var myName = "Adam";
var myNumber = 23;
console.log("myObject: " + angular.isObject(myObject));
console.log("myName: " + angular.isObject(myName));
console.log("myNumber: " + angular.isObject(myNumber));
</script>
</head>

<body>
This is a simple example
</body>

</html>

2.读取和修改属性的值

3.枚举属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html>

<head>
<title>Example</title>
<script src="angular.js"></script>
<script type="text/javascript">
var myData = {
name: "Adam",
weather: "sunny",
printMessages: function () {
console.log("Hello " + this.name + ". ");
console.log("Today is " + this.weather + ".");
}
};
for (var prop in myData) {
console.log("Name: " + prop + " Value: " + myData[prop]);
}
console.log("---");
angular.forEach(myData, function (value, key) {
console.log("Name: " + key + " Value: " + value);
});
</script>
</head>

<body>
This is a simple example
</body>

</html>

JavaScript的for…in
AngularJS提供的angular.forEach方法是另一个选择,它要的是一个对象和一个将为其每个属性执行的函数。通过value和key参数将当前属性值及其名称传给函数

4.添加和删除属性和方法

使用 JavaScript 运算符

使用条件语句

对比等于运算符和全等运算符

提示:
AngularJS用angular.equals方法扩展了内置的对比较的支持,它拿两个对象或值做参数,如果它们通过全等比较或者两个参数的对象并且它们的所有属性都通过全等比较,那就返回true,我不倾向于使用该方法

显式转换类型

使用数组

比较 undefined 和 null 值

你也可以使用AngularJS的angular.isDefined和angular.isUndefined方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>

<head>
<title>Example</title>
<script src="angular.js"></script>
<script type="text/javascript">
var myData = {
name: "Adam",
city: null
};

console.log("name: " + angular.isDefined(myData.name));
console.log("city: " + angular.isDefined(myData.city));
console.log("country: " + angular.isDefined(myData.country));
</script>
</head>

<body>
This is a simple example
</body>

</html>

这些方法仅检查值是否已被定义,但不检查是否为null,也不能用于区别null和undefined值

使用承诺

承诺是一种表述方式,它表明某项工作会以异步方式执行并在未来某个点被完成。最常遇到的方式是产生Ajax请求,当请求被完成时,浏览器会暗地里发出HTTP请求通知你的应用程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html ng-app="demo">

<head>
<title>Example</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script type="text/javascript">
var myApp = angular.module("demo", []);

myApp.controller("demoCtrl", function ($scope, $http) {
var promise = $http.get("todo.json");
promise.then(function (data) {
$scope.todos = data.data;
});
});
</script>
</head>

<body ng-controller="demoCtrl">
<div class="panel">
<h1>To Do</h1>
<table class="table">
<tr>
<td>Action</td>
<td>Done</td>
</tr>
<tr ng-repeat="item in todos">
<td>{{item.action}}</td>
<td>{{item.done}}</td>
</tr>
</table>
</div>
</body>

</html>

示例的重要部分在这:

1
2
3
4
5
6
7
8
...
myApp.controller("demoCtrl", function ($scope, $http) {
var promise = $http.get("todo.json");
promise.then(function (data) {
$scope.todos = data.data;
});
});
...

$http服务用于产生Ajax请求,然后get方法取到你想从服务器获取的文件的URL
Ajax请求是被异步执行的,当请求发出时,浏览器继续运行我的简单应用程序。$http.get方法返回承诺对象,我可以用它接收关于Ajax请求的通知。then方法是承诺对象所定义的三个中的一个

承诺对象定义的方法

名称 描述
error(callback) 请求成功完成时调用指定的函数
success(callback) 请求未成功完成时调用指定的函数
then(success, error) 注册成功或失败时调用的函数

三个方法都用函数作为参数,并根据承诺的结果而调用。回调函数success会被传入从服务器拿到的数据,而回调函数error接收遭遇到问题的详情

三个承诺方法都返回承诺对象,让异步任务可以按顺序链接在一起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html ng-app="demo">

<head>
<title>Example</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script type="text/javascript">
var myApp = angular.module("demo", []);

myApp.controller("demoCtrl", function ($scope, $http) {
$http.get("todo.json").then(function (response) {
$scope.todos = response.data;
}, function () {
$scope.todos = [{ action: "Error" }];
}).then(function () {
$scope.todos.push({ action: "Request Complete" });
});
});
</script>
</head>

<body ng-controller="demoCtrl">
<div class="panel">
<h1>To Do</h1>
<table class="table">
<tr>
<td>Action</td>
<td>Done</td>
</tr>
<tr ng-repeat="item in todos">
<td>{{item.action}}</td>
<td>{{item.done}}</td>
</tr>
</table>
</div>
</body>

</html>

第一,我调用get方法创建了Ajax请求
第二,我使用then方法提供函数,它在Ajax请求完成时被调用。第一个在请求成功时调用,第二个在请求失败时调用
第三,我使用then方法再次添加了函数,这一次我仅仅为then放传入一个函数,意味着如果有问题我也不想要通知。最后的函数不顾之前被调用过的函数,向数据模型添加了一项

使用 JSON

JSON支持一些基本的数据类型,与JavaScript巧妙地结合在了一起:Number、String、Boolean、Array、Object和特殊类型null
JSON数据看起来和JavaScript用来声明数组和对象的字面量类似。唯一不同的是对象的属性名被放到了引号中
AngularJS使得使用JSON很简单。当你通过Ajax请求JSON数据时,响应会被自动解析成JavaScript对象并传给success函数,如上一示例中我使用$http.get方法从Web服务器获取JSON文件时所演示的那样
AngularJS补充了两个显式编码和解码JSON的方法:angular.fromJson和angular.toJson

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html ng-app="demo">

<head>
<title>Example</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script type="text/javascript">
var myApp = angular.module("demo", []);

myApp.controller("demoCtrl", function ($scope, $http) {
$http.get("todo.json").then(function (data) {
var jsonString = angular.toJson(data.data);
console.log(jsonString);
$scope.todos = angular.fromJson(jsonString);
});
});
</script>
</head>

<body ng-controller="demoCtrl">
<div class="panel">
<h1>To Do</h1>
<table class="table">
<tr>
<td>Action</td>
<td>Done</td>
</tr>
<tr ng-repeat="item in todos">
<td>{{item.action}}</td>
<td>{{item.done}}</td>
</tr>
</table>
</div>
</body>

</html>

angular.fromJson方法将JSON转换成对象
angular.toJson方法将对象转换成JSON

提示:
许多最常见的需要JSON数据的AngularJS功能都将自定编码和解码数据,所以你不会经常需要使用这些方法

第 6 章 SportsStore:一个真正的应用程序

开始

准备数据
第一步是创建新的Deployd应用程序,在和angularjs文件夹同级的位置创建一个名为deployd的文件夹,来存储生成的文件
切换到新创建的deployd目录,输入以下命令:

1
dpd create sportsstore

输入以下命令,以启动服务器

1
dpd –p 5500 sportsstore\app.dpd dashboard

打开Deployd控制面板,在浏览器中访问:http://localhost:5500/dashboard/

1.创建数据结构
2.添加数据
3.测试数据服务

准备应用程序
1.创建目录结构
在angularjs文件夹中创建以下目录

SportsStore应用程序必需的文件夹

名称 描述
components 包括独立的自定义AngularJS组件
controllers 包括应用程序的控制器
filters 包括自定义过滤器
ngmodules 包括可选AngularJS模块
views 包括应用程序的局部视图

2.安装AngularJS和Bootstrap
将angularjs的主要JavaScript文件和Bootstrap的CSS文件放入angularjs主目录
并不是所有的功能都在angularjs.js文件中。为了SportsStore应用程序,我将需要一些可选模块中的附加功能,下载以下文件,并将它们放在angularjs/ngmodules文件夹中

名称 描述
angular-route.js 添加URL路由的支持
angular-resource.js 添加使用RESTful的API的支持

3.构建基本大纲
创建app.html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!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的特有格式。第一个是在script元素中调用angular.module方法

1
2
3
4
5
...
<script>
angular.module("sportsStore", []);
</script>
...

模块是在一个AngularJS应用程序中的顶级构建块,调用该方法创建了叫做sportsStore的新模块。这是马上构建模块的唯一做法,我会在之后使用它定义应用程序的功能
第二个方面是我在html元素上应用了ng-app指令

1
2
3
...
<html ng-app="sportsStore">
...

ng-app指令使定义在sportsStore模块中的功能在HTML中也可以使用。我喜欢在html元素上应用ng-app指令,但你也可以更明确,通常也可以在body元素上应用它

显示伪造的产品数据

创建控制器
我需要控制器作为开端,定义逻辑和需要的数据,以支持其作用域中的视图。我要创建的控制器将被用于整个应用程序。创建新的controllers/sportsStore.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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文件中调用的方法一样,定义SportsStore应用程序的主模块。不同的是当我定义模块时,提供了额外的参数
第二个参数是数组,目前是空的,它列出模块依赖于SportsStore的哪些模块,并让AngularJS找到并提供这些模块所提供的功能。如果你试图创建已经存在的模块,AngularJS会有错误报告,所以你需要确保你的模块名是唯一的
相比之下,在sportsStore.js文件中调用angular.module方法时没有第二个参数
第二个参数的缺失会告诉AngularJS,我想找到已经定义的模块。这种情况下,如果指定的模块不存在,AngularJS会报告错误,所以你需要确保模块已经被创建
两种angular.module方法的使用都返回module对象,它可用于应用程序功能的定义。我使用controller方法定义控制器

注意:
我通常不会向这样在HTML文件中调用它以创建主要的应用程序模块,因为在JavaScript文件中做每件事都更容易。我分开表述的原因是由于angular.module的两用性会导致混淆

顶级控制器在SportsStore应用程序中的主要角色是,定义将被用于应用程序显示的不同视图。AngularJS可以将多个控制器放在一个层级中,它们可以从上层的控制器继承数据和逻辑,而且通过在顶级控制器中定义数据,可以让稍后将定义的控制器更易于使用
我已经定义的数据是一个数组对象,和存储在Deployd的数据具有相同的属性,它让我可以立马开始,而不用等到产生Ajax请求获取真实产品信息后

注意:
我在控制器的作用域上定义数据时,我在数组中定义了数据,并将其赋值到data对象的products属性上,它会依次附着在作用域上。在定义你要继承的数据时必须要小心,不要直接将属性赋值到作用域上(即 $scope.products = [data])),因为这样其他控制器便可以读取数据,除非数据在频繁的被修改

显示产品详情
在app.html文件中添加一些HTML标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!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元素从controllers文件夹中引入sportsStore.js文件。它包含sportsStoreCtrl控制器

1
2
3
4
5
6
...
<script>
angular.module("sportsStore", []);
</script>
<script src="controllers/sportsStore.js"></script>
...

我使用ng-controller指令为其视图使用控制器

1
2
3
...
<body ng-controller="sportsStoreCtrl">
...

最后我创建了用于显示数据的元素,并为item.price应用了内置的currency过滤器

1
2
3
4
5
6
7
8
9
10
11
...
<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>
....

显示分类列表

创建分类列表
我想以产品数据对象动态生成分类元素,而不是写死HTML元素。可以使用一个自定义过滤器实现这个功能。在filters目录中创建文件customFilters.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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的新模块,以包含我的过滤器。这样我就可以向你展示如何在一个应用程序中定义和连接多个模块
给filter方法的参数是过滤器的名称,还有用来执行过滤的工厂函数。AngularJS在其需要创建过滤器实例时调用该函数
所有过滤函数都会被传入需要格式化的数据,但我的过滤器定义了一个额外的参数propertyName,我使用它指定将被用于生成唯一值列表的对象属性

提示:
我可以把过滤器写死去寻找category属性,但那样就会存在局限性。我将属性名作为参数而创建的过滤器可以用于生成任何属性的唯一值列表

提示:
改动过滤器使之只对用户显示的数据有效,不要修改作用域上的原数据

生成分类导航链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!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模块的依赖,它包含unique过滤器

1
2
3
4
5
...
<script>
angular.module("sportsStore", ["customFilters"]);
</script>
...

这就是所谓的声明依赖。在这个例子中,我声明sportsStore模块依赖于customFilters模块中的功能。这使得AngularJS找到customFilters模块并使之可用,这样我就可以引用它所包含的组件,这一过程称为解析依赖
我还必须添加script元素以载入包含customFilters模块的文件

1
2
3
4
5
6
7
...
<script>
angular.module("sportsStore", ["customFilters"]);
</script>
<script src="controllers/sportsStore.js"></script>
<script src="filters/customFilters.js"></script>
...

注意我可以在创建sportsStore模块并声明依赖customFilters模块之后,再引入customFilters.js文件。在你扩展模块时模块必须先存在,但是在你声明依赖或定义新模块时就没有这个限制了

1.生成导航元素

1
2
3
4
5
6
...
<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>
...

提示:
我指定属性名称时将其放在了单引号之间,AngularJS默认将表达式中的名称解析为定义在作用域上的变量。要使用静态值,必须使用字符串字面量

提示:
我对调过滤器会有相同的效果。不同的是orderBy过滤器可能操作的是字符串数组而不是产品对象,因为unique过滤器返回的是字符串数组。过滤器orderBy被设计用于操作对象,但是你可以使用:orderBy:’toString()’来排序字符串。别忘了引号,否则AngularJS会将toString视为作用域属性,而不是调用toString()方法

2.处理单击事件
我在元素上使用ng-click指令指定了当click事件出发时AngularJS应该做什么

选择分类
在浏览器中单击分类按钮现在不会有任何效果,因为a元素上的ng-click指令被设置要调用的控制器行为还没有被定义

1.定义控制器
为了响应用户单击分类按钮,我需要定义控制器行为selectCategory。我不想在顶级控制器上添加行为,我要保留整个应用程序所需的行为和数据。作为替代,我会创建新控制器,它将仅被用于产品列表和分类视图。创建controllers/productListControllers.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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文件中的sportsStore模块上的controller方法创建了一个名为productListCtrl的控制器,在其中定义了selectedCategory行为,它将在每次点击分类时更新selectedCategory变量的值。控制器还定义了categoryFilterFn,它将过滤产品对象

提示:
注意变量selectedCategory没有定义在作用域上。它只是一个常规的JavaScript变量,说明它不能视图上的指令或数据绑定访问

2.应用控制器并过滤产品
我必须使用ng-controller指令向视图应用控制器,以使得ng-click指令可以调用selectCategory行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!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>

我添加了对productListControllers.js文件的引用,productListCtrl控制器的ng-controller指令放在了sportsStoreCtrl控制器的作用域之中,意味着我们可以利用控制器作用域继承

高亮显示选择的分类
先在控制器上添加行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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名,所以我使用Model对象上的constant方法定义常量productListActiveClass,在控制器中访问该值,我必须声明常量名称作为依赖

1
2
3
...
.controller("productListCtrl", function ($scope, $filter, productListActiveClass) {
...

在app.html文件中使用ng-class指令

1
2
3
4
5
6
7
8
9
...
<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)">

</a>
</div>
...

添加分页
1.更新控制器
我更新了productListCtrl控制器来支持分页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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,我声明其作为控制器的依赖。在控制器中我定义了两个作用域上的变量来暴露页数和当前被选中的页。同时还定义了getPageClass,它用来高亮显示被选中的页面

2.实现过滤器
我在customFilters.js中创建了两个新的过滤器支持分页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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;
}
}
})
.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从数组中返回一系列元素。过滤器接收参数有当前被选页面和页面尺寸。其中使用了内置过滤器limitTo,它从从数组返回指定数量的条目
pageCount过滤器是“脏”的。目的是通过生成的数组执行ng-repeat指令以创建分页元素

3.更新视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!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"
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>

第 7 章 SportsStore:导航和结账

准备实例项目

使用真实项目数据

1
2
3
4
5
6
7
8
9
10
11
12
13
angular.module("sportsStore")
.constant("dataUrl", "http://localhost:5500/products")
.controller("sportsStoreCtrl", function ($scope, $http, dataUrl) {

$scope.data = {};

$http.get(dataUrl)
.then(function (response) {
$scope.data.products = response.data;
}, function (error) {
$scope.data.error = error;
});
});

注意:
原文中使用success和error,这两个方法已被弃用,现用then方法实现其功能

处理Ajax错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!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="alert alert-danger" ng-show="data.error">
Error ({{data.error.status}}). The product data was not loaded.
<a href="/app.html" class="alert-link">Click here to try again</a>
</div>
<div class="panel panel-default row" ng-controller="productListCtrl" ng-hide="data.error">
<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>
<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>

创建局部视图

在app.html文件中的HTML错综复杂,没法一下子弄清楚每个元素都在做什么。我可以拆分标签成独立的文件,让后使用ng-include指令在运行时引入那些文件。为了这个目的我创建了views/productList.html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<div class="panel panel-default row" ng-controller="productListCtrl" ng-hide="data.error">
<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>
<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>

从app.html文件中删除这些元素,并用ng-include指令替换它们

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!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="alert alert-danger" ng-show="data.error">
Error ({{data.error.status}}). The product data was not loaded.
<a href="/app.html" class="alert-link">Click here to try again</a>
</div>

<ng-include src="'views/productList.html'"></ng-include>

</body>

</html>

提示:
使用局部视图有三个好处。第一是将应用程序拆成可管理的块,正如我在这做的。第二是创建在一个应用程序中可复用的HTML片段。第三是使其更易于为用户显示不同的功能区域

提示:
在使用ng-include指令时,我将文件名写成单引号的字面量。如果我不这么做,那指令会在作用域属性上寻找文件

创建购物车

定义购物车模块和服务
我首先创建components/cart文件夹并向其添加cart.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
angular.module("cart", [])
.factory("cart", function () {

var cartData = [];

return {
addProduct: function (id, name, price) {
var addedToExistingItem = false;
for (var i = 0; i < cartData.length; i++) {
if (cartData[i].id == id) {
cartData[i].count++;
addedToExistingItem = true;
break;
}
}
if (!addedToExistingItem) {
cartData.push({
count: 1, id: id, price: price, name: name
});
}
},

removeProduct: function (id) {
for (var i = 0; i < cartData.length; i++) {
if (cartData[i].id == id) {
cartData.splice(i, 1);
break;
}
}
},

getProducts: function () {
return cartData;
}
}
});

我在这里使用了module.factory方法,传入服务名称和工厂函数
我的cart服务工厂函数返回对象,该对象有三个方法

创建购物车部件
向cart.js添加指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
angular.module("cart", [])
.factory("cart", function () {
var cartData = [];
return {
// ...service statements omitted for brevity...
}
})
.directive("cartSummary", function (cart) {
return {
restrict: "E",
templateUrl: "components/cart/cartSummary.html",
controller: function ($scope) {
var cartData = cart.getProducts();
$scope.total = function () {
var total = 0;
for (var i = 0; i < cartData.length; i++) {
total += (cartData[i].price * cartData[i].count);
}
return total;
}
$scope.itemCount = function () {
var total = 0;
for (var i = 0; i < cartData.length; i++) {
total += cartData[i].count;
}
return total;
}
}
};
});

指令由AngularJS模块上的directive方法创建,传入指令名和返回指令定义的工厂函数。指令定义中定义的属性告诉AngularJS你的指令做什么和如何做。我在cartSummary中定义了三个属性:

restrict,指定指令如何应用。E值说明该指令只能作为元素应用。EA表示该指令可以作为元素或属性应用
templateUrl,指定将被插入指令的元素内容的局部视图
controller,指定向局部视图提供数据和行为的控制器

简单来说,我的指令定义了控制器,告诉AngularJS使用components/cart/cartSummary.html视图,还约束了指令让其仅作为元素而被使用。注意控制器声明了对cart服务的依赖,它定义在相同的模块中。控制器定义的行为可用于局部视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<style>
.navbar-right {
float: right !important;
margin-right: 5px;
}

.navbar-text {
margin-right: 10px;
}
</style>
<div class="navbar-right">
<div class="navbar-text">
<b>Your cart:</b>
{{itemCount()}} item(s), {{total() | currency}}
</div>
<a class="btn btn-default navbar-btn">Checkout</a>
</div>

提示:
该布局视图包含style元素。我通常不喜欢在局部视图中嵌入style元素,但当这些更改仅影响该视图,并且存在少量CSS时我会这样使用。在其他情况下,我将定义独立的CSS文件并将它引入应用程序的主HTML文件

应用购物车部件
在应用程序中应用购物车部件需要三个步骤:添加添加script元素将JavaScript文件的内容引入,添加对cart模块的依赖,还有在标签上添加指令内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!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", "cart"]);
</script>
<script src="controllers/sportsStore.js"></script>
<script src="filters/customFilters.js"></script>
<script src="controllers/productListControllers.js"></script>
<script src="components/cart/cart.js"></script>
</head>

<body ng-controller="sportsStoreCtrl">
<div class="navbar navbar-inverse">
<a class="navbar-brand" href="#">SPORTS STORE</a>

<cart-summary />

</div>
<div class="alert alert-danger" ng-show="data.error">
Error ({{data.error.status}}). The product data was not loaded.
<a href="/app.html" class="alert-link">Click here to try again</a>
</div>

<ng-include src="'views/productList.html'"></ng-include>

</body>

</html>

注意,我定义指令时使用了名称cartSummary,但我在app.html中添加的元素是cart-summary。AngularJS在两种格式之间映射以正常化组件名

添加产品选择按钮
首先向productListCtrl控制器添加添加产品的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
angular.module("sportsStore")
.constant("productListActiveClass", "btn-primary")
.constant("productListPageCount", 3)
.controller("productListCtrl", function ($scope, $filter, productListActiveClass, productListPageCount, cart) {

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

$scope.addProductToCart = function (product) {
cart.addProduct(product.id, product.name, product.price);
}
});

我声明了对cart的依赖,以调用cart.addProduct将产品添加到购物车中

接着向局部视图productList.html添加加入到购物车按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<div class="panel panel-default row" ng-controller="productListCtrl" ng-hide="data.error">
<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>
<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>
<button ng-click="addProductToCart(item)" class="btn btn-success pull-right">
Add to cart
</button>
<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>

添加 URL 导航

在添加结账功能的支持之前,我想先添加URL路由的支持
首先我要创建在用户开始结账时要显示的视图views/checkoutSummary.html

1
2
3
4
<div class="lead">
This is the checkout summary view
</div>
<a href="#!/products" class="btn btn-primary">Back</a>

注意:
原文中使用#/形式的路由已被弃用,现用#!/形式的新路由

定义URL路由
我将从定义我需要的路由开始,它映射指定URL于该URL应该显示的视图。首先/product和/checkout将分别映射到productList.html和checkoutSummary.html视图。其他路由都将默认显示productList.html视图。我在app.html中对路由做了改动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!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", "cart", "ngRoute"])
.config(function ($routeProvider) {

$routeProvider.when("/checkout", {
templateUrl: "/views/checkoutSummary.html"
});

$routeProvider.when("/products", {
templateUrl: "/views/productList.html"
});

$routeProvider.otherwise({
templateUrl: "/views/productList.html"
});
});
</script>
<script src="controllers/sportsStore.js"></script>
<script src="filters/customFilters.js"></script>
<script src="controllers/productListControllers.js"></script>
<script src="components/cart/cart.js"></script>
<script src="ngmodules/angular-route.js"></script>
</head>

<body ng-controller="sportsStoreCtrl">
<div class="navbar navbar-inverse">
<a class="navbar-brand" href="#">SPORTS STORE</a>

<cart-summary />

</div>
<div class="alert alert-danger" ng-show="data.error">
Error ({{data.error.status}}). The product data was not loaded.
<a href="/app.html" class="alert-link">Click here to try again</a>
</div>

<ng-view />

</body>

</html>

我添加了script元素将angular-route.js文件引入应用程序。该文件提供的功能是在ngRoute模块中定义的,我生命了它作为sportsStore模块的依赖
我调用模块上的config方法设置我的路由。config方法获取函数作为其参数,它在模块被载入而应用程序还未执行之前执行,提供一次性任意配置任务的机会
我传入config方法的函数声明依赖于提供器。创建AngularJS服务有不同的方式,其中之一就是创建可通过提供器对象配置的服务,它的名字是服务名与Provider连接而成的。我声明依赖的$routeProvider就是$route服务的提供器,它用于在应用程序中设置URL路由

我使用$routeProvider对象定义的两个方法设置我需要的路由,when方法让我能将URL匹配视图。我还使用otherwise定义了不匹配when方法定义的任意一个路由时应该被使用的视图

显示路由视图
路由策略定义了根据拿到的URL路径而应该被显示的视图是哪一个,但它没有告诉AngularJS它们在哪显示。为此我需要ng-view指令。不用进行配置,只是添加指令,告诉AngularJS它应该将目前被选则的视图的内容插入到哪里

使用URL路由导航

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<style>
.navbar-right {
float: right !important;
margin-right: 5px;
}

.navbar-text {
margin-right: 10px;
}
</style>
<div class="navbar-right">
<div class="navbar-text">
<b>Your cart:</b>
{{itemCount()}} item(s), {{total() | currency}}
</div>
<a href="#!/checkout" class="btn btn-default navbar-btn">Checkout</a>
</div>

使用URL路由的主要好处是组件可以改变ng-wiew指令所显示的布局,而不需要预先了解将被显示的视图的任何信息。这使其易于扩展为复杂的应用程序,也使其只改变URL路由的配置就可以改变应用程序的行为

开始结账流程

第一个任务是定义新的控制器controllers/checkoutControllers.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
angular.module("sportsStore")
.controller("cartSummaryController", function ($scope, cart) {
$scope.cartData = cart.getProducts();
$scope.total = function () {
var total = 0;
for (var i = 0; i < $scope.cartData.length; i++) {
total += ($scope.cartData[i].price * $scope.cartData[i].count);
}
return total;
}
$scope.remove = function (id) {
cart.removeProduct(id);
}
});

新控制器被添加到sportsStore模块上并依赖于cart服务,它通过作用域属性cartData暴露购物车,并定义计算购物车中计算产品的总值,以及从购物车中删除产品的行为。使用控制器创建的特性,我可以完善checkoutSummary.html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<h2>Your cart</h2>
<div ng-controller="cartSummaryController">
<div class="alert alert-warning" ng-show="cartData.length == 0">
There are no products in your shopping cart.
<a href="#!/products" class="alert-link">Click here to return to the catalogue</a>
</div>
<div ng-hide="cartData.length == 0">
<table class="table">
<thead>
<tr>
<th>Quantity</th>
<th>Item</th>
<th class="text-right">Price</th>
<th class="text-right">Subtotal</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in cartData">
<td class="text-center">{{item.count}}</td>
<td class="text-left">{{item.name}}</td>
<td class="text-right">{{item.price | currency}}</td>
<td class="text-right">{{ (item.price * item.count) | currency}}</td>
<td>
<button ng-click="remove(item.id)" class="btn btn-sm btn-warning">Remove</button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-right">Total:</td>
<td class="text-right">
{{total() | currency}}
</td>
</tr>
</tfoot>
</table>
<div class="text-center">
<a class="btn btn-primary" href="#!/products">Continue shopping</a>
<a class="btn btn-primary" href="#!/placeorder">Place order now</a>
</div>
</div>
</div>

应用结账总览
向app.html添加script元素,并定义我完成结账流程将会需要的额外的路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<!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", "cart", "ngRoute"])
.config(function ($routeProvider) {

$routeProvider.when("/complete", {
templateUrl: "/views/thankYou.html"
});

$routeProvider.when("/placeorder", {
templateUrl: "/views/placeOrder.html"
});

$routeProvider.when("/checkout", {
templateUrl: "/views/checkoutSummary.html"
});

$routeProvider.when("/products", {
templateUrl: "/views/productList.html"
});

$routeProvider.otherwise({
templateUrl: "/views/productList.html"
});
});
</script>
<script src="controllers/sportsStore.js"></script>
<script src="filters/customFilters.js"></script>
<script src="controllers/productListControllers.js"></script>
<script src="components/cart/cart.js"></script>
<script src="ngmodules/angular-route.js"></script>
<script src="controllers/checkoutControllers.js"></script>
</head>

<body ng-controller="sportsStoreCtrl">
<div class="navbar navbar-inverse">
<a class="navbar-brand" href="#">SPORTS STORE</a>

<cart-summary />

</div>
<div class="alert alert-danger" ng-show="data.error">
Error ({{data.error.status}}). The product data was not loaded.
<a href="/app.html" class="alert-link">Click here to try again</a>
</div>

<ng-view />

</body>

</html>

新的路由对应的视图将在下一章创建

第 8 章 SportsStore:订单和管理

准备示例程序

获取运输详情

我创建了views/placeOrder.html文件捕获用户的运输详情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<h2>Check out now</h2>
<p>Please enter your details, and we'll ship your goods right away!</p>

<div class="well">
<h3>Ship to</h3>
<div class="form-group">
<label>Name</label>
<input class="form-control" ng-model="data.shipping.name" />
</div>

<h3>Address</h3>

<div class="form-group">
<label>Street Address</label>
<input class="form-control" ng-model="data.shipping.street" />
</div>

<div class="text-center">
<button class="btn btn-primary">Complete order</button>
</div>
</div>

关于该视图,首先要注意的是我没有使用ng-controller指令指定控制器。这意味着视图将被顶级控制器sportsStoreCrtl支持,它管理包括ng-view指令在内的视图。我指出这点是因为你不是非得为局部视图添加控制器,当视图不需要任何附加行为时这样比较方便
在示例中最重要的AngularJS特性是在input元素上对ng-model指令的使用
ng-model指令设有双向数据绑定

提示:
我并非必须更新控制器,让它定义其作用域上的data.shipping对象或单独的name和street属性。AngularJS作用域非常灵活,如果你预先没定义好,假定你想动态定义属性也是可以的

添加表单验证
AngularJS支持表单验证,它能检查数值的适用性
AngularJS表单验证基于表单元素上标准的HTML属性,比如type和required。表单验证自动执行,但仍需要一些工作来将验证的反馈呈现给用户,并在应用程序中整合整体验证的结果

1.验证的准备
设置表单验证的第一步是在视图上添加form元素并在input元素上添加验证属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<h2>Check out now</h2>
<p>Please enter your details, and we'll ship your goods right away!</p>

<form name="shippingForm" novalidate>
<div class="well">
<h3>Ship to</h3>
<div class="form-group">
<label>Name</label>
<input class="form-control" ng-model="data.shipping.name" required />
</div>

<h3>Address</h3>
<div class="form-group">
<label>Street Address</label>
<input class="form-control" ng-model="data.shipping.street" required />
</div>

<div class="text-center">
<button class="btn btn-primary">Complete order</button>
</div>
</div>
</form>

form元素有三个目的,哪怕我没在应用程序中使用浏览器内置支持的表单验证
第一个目的是启用验证。在自定义指令中AngularJS重新定义了一些HTML元素以使用特殊特性,其中一个元素就是form。没有form元素,AngularJS就无法验证诸如input、select、textarea等元素的内容
表单元素的第二个目的是禁用任何浏览器可能会执行的验证,它可以通过novalidate属性的使用来禁用浏览器验证。它能确保只有AngularJS检查用户提供的数据,避免重复验证
最后一个表单元素的目的是定义一个变量,用来报告表单的有效性。它通过name属性实现
除此之外,我在input元素上使用了required属性。这是一个AngularJS可以识别的验证属性

2.显示验证反馈
一旦form元素和验证属性被放好,AngularJS就会开始验证用户所提供的数据,但我必须拿到用户所有的反馈。有两种反馈形式供我使用:我可以利用AngularJS赋给form元素的通过验证和未通过验证的class来定义CSS样式,我还可以使用作用域变量控制相应元素的反馈信息的可见性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<style>
.ng-invalid {
background-color: lightpink;
}

.ng-valid {
background-color: lightgreen;
}

span.error {
color: red;
font-weight: bold;
}
</style>

<h2>Check out now</h2>
<p>Please enter your details, and we'll ship your goods right away!</p>

<form name="shippingForm" novalidate>
<div class="well">
<h3>Ship to</h3>
<div class="form-group">
<label>Name</label>
<input name="name" class="form-control" ng-model="data.shipping.name" required />
<span class="error" ng-show="shippingForm.name.$error.required">
Please enter a name
</span>
</div>

<h3>Address</h3>
<div class="form-group">
<label>Street Address</label>
<input name="street" class="form-control" ng-model="data.shipping.street" required />
<span class="error" ng-show="shippingForm.street.$error.required">
Please enter a street address
</span>
</div>

<div class="text-center">
<button class="btn btn-primary">Complete order</button>
</div>
</div>
</form>

AngularJS赋予form元素ng-valid和ng-invalid样式类,所以我定义了style元素
CSS样式有指示input元素有问题的效果,但无法提示问题是什么。为此,我必须为每个元素添加name属性,并使用AngularJS添加到作用域的验证数据来控制错误信息的可见性

1
2
3
4
5
6
...
<input name="street" class="form-control" ng-model="data.shipping.street" required />
<span class="error" ng-show="shippingForm.street.$error.required">
Please enter a street address
</span>
...

3.连接按钮来验证
在大多数的Web应用程序中,所有表单数据都被提供并验证前,都不允许用户到下一步。为了这一目的,我想在未通过表单验证时禁用“Complete order”按钮,并在适当的完成表单之后启用它

1
2
3
4
5
...
<div class="text-center">
<button ng-disabled="shippingForm.$invalid" class="btn btn-primary">Complete order</button>
</div>
...

添加剩下的表单字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<style>
.ng-invalid {
background-color: lightpink;
}

.ng-valid {
background-color: lightgreen;
}

span.error {
color: red;
font-weight: bold;
}
</style>

<h2>Check out now</h2>
<p>Please enter your details, and we'll ship your goods right away!</p>

<form name="shippingForm" novalidate>
<div class="well">
<h3>Ship to</h3>
<div class="form-group">
<label>Name</label>
<input name="name" class="form-control" ng-model="data.shipping.name" required />
<span class="error" ng-show="shippingForm.name.$error.required">
Please enter a name
</span>
</div>

<h3>Address</h3>
<div class="form-group">
<label>Street Address</label>
<input name="street" class="form-control" ng-model="data.shipping.street" required />
<span class="error" ng-show="shippingForm.street.$error.required">
Please enter a street address
</span>
</div>

<div class="form-group">
<label>City</label>
<input name="city" class="form-control" ng-model="data.shipping.city" required />
<span class="error" ng-show="shippingForm.city.$error.required">
Please enter a city
</span>
</div>

<div class="form-group">
<label>State</label>
<input name="state" class="form-control" ng-model="data.shipping.state" required />
<span class="error" ng-show="shippingForm.state.$error.required">
Please enter a state
</span>
</div>

<div class="form-group">
<label>Zip</label>
<input name="zip" class="form-control" ng-model="data.shipping.zip" required />
<span class="error" ng-show="shippingForm.zip.$error.required">
Please enter a zip code
</span>
</div>

<div class="form-group">
<label>Country</label>
<input name="country" class="form-control" ng-model="data.shipping.country" required />
<span class="error" ng-show="shippingForm.country.$error.required">
Please enter a country
</span>
</div>

<h3>Options</h3>
<div class="checkbox">
<label>
<input name="giftwrap" type="checkbox" ng-model="data.shipping.giftwrap" /> Gift wrap these items
</label>
</div>

<div class="text-center">
<button ng-disabled="shippingForm.$invalid" class="btn btn-primary">Complete order</button>
</div>
</div>
</form>

下单

扩展Deployd服务
添加新的集合

定义控制器行为
向顶级控制器sportsStore中添加发送订单数据到Deployd服务器的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
angular.module("sportsStore")
.constant("dataUrl", "http://localhost:5500/products")
.constant("orderUrl", "http://localhost:5500/orders")
.controller("sportsStoreCtrl", function ($scope, $http, $location,
dataUrl, orderUrl, cart) {

$scope.data = {};

$http.get(dataUrl)
.then(function (response) {
$scope.data.products = response.data;
}, function (error) {
$scope.data.error = error;
});

$scope.sendOrder = function (shippingDetails) {
var order = angular.copy(shippingDetails);
order.products = cart.getProducts();
$http.post(orderUrl, order)
.then(function (data) {
$scope.data.orderId = data.id;
cart.getProducts().length = 0;
}, function (error) {
$scope.data.orderError = error;
})
.finally(function () {
$location.path("/complete");
});
}
});

调用控制器行为

1
2
3
4
5
6
7
...
<div class="text-center">
<button ng-disabled="shippingForm.$invalid" ng-click="sendOrder(data.shipping)" class="btn btn-primary">
Complete order
</button>
</div>
...

定义视图
请求完成后,我所指定的URL路径是/complete,URL路由配置会映射它到/view/thankYou.html

1
2
3
4
5
6
7
8
<div class="alert alert-danger" ng-show="data.orderError">
Error (). The order could not be placed.
<a href="#/placeorder" class="alert-link">Click here to try again</a>
</div>
<div class="well" ng-hide="data.orderError">
<h2>Thanks!</h2>
Thanks for placing your order. We'll ship your goods as soon as possible. If you need to contact us, use reference .
</div>

改进

首先,当你在浏览器中载入app.html文件,你可能注意到在视图被显示和产品及分类元素被生成之间有一点延迟。这是因为Ajax请求获取数据发生在后端,当等待服务器返回数据时,AngularJS继续执行应用程序并显示视图,当数据到达再更新它们
然后,为了导航和分页特性,我处理产品数据并筛出分类。在实际项目中,我会考虑在产品数据首次到达时只生成该信息一次,之后就复用它
最后,我会使用$animate服务显示短的、突出的动画,在URL路径改变时从一个视图过渡到另一个视图

管理产品类别

准备Deployd
添加用户集合

巩固集合
我喜爱的Deployd的特性之一是它定义了可用于实现服务端功能的简单JavaScript API,当对集合进行操作时可以触发一系列的事件。在控制台中单击products集合,然后单击Events,你将看到一系列不同事件的选项卡:On Get、On Validate、On Post、On Put和On Delete。这些事件为整个集合定义。你能做许多事情。比如使用JavaScript加强验证策略。在On Put和On Delete标签中输入以下JavaScript

1
2
3
if (me === undefined || me.username != "admin") {
cancel("No authorization", 401);
}

在Deployd的API中,变量me代表当前用户,而cacel函数使用指定的消息和HTTP状态码终止请求

在orders集合中为所有的事件重复这一过程,除了On Post和On Validate

创建管理应用程序
向angularjs文件夹添加新文件admin.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html ng-app="sportsStoreAdmin">

<head>
<title>Administration</title>
<script src="angular.js"></script>
<script src="ngmodules/angular-route.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("sportsStoreAdmin", ["ngRoute"])
.config(function ($routeProvider) {

$routeProvider.when("/login", {
templateUrl: "/views/adminLogin.html"
});

$routeProvider.when("/main", {
templateUrl: "/views/adminMain.html"
});

$routeProvider.otherwise({
redirectTo: "/login"
});
});
</script>
</head>

<body>
<ng-view />
</body>

</html>

为了定义路由的otherwise方法,我使用了redirectTo,它将重定向路由到其它路径

添加占位视图
创建验证成功后显示的视图/views/adminMain.html

1
2
3
<div class="well">
This is the main view
</div>

实现验证
Deployd验证用户使用标准HTTP请求。应用程序发送POST请求到/users/login并提供username和password作为参数。如果成功服务器响应状态码200,如果验证失败则返回401.
为实现验证我先定义产生Ajax调用的控制器controllers/adminControllers.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
angular.module("sportsStoreAdmin")
.constant("authUrl", "http://localhost:5500/users/login")
.controller("authCtrl", function ($scope, $http, $location, authUrl) {

$scope.authenticate = function (user, pass) {
$http.post(authUrl, {
username: user,
password: pass
}, {
withCredentials: true
}).then(function (data) {
$location.path("/main");
}, function (error) {
$scope.authenticationError = error;
});
}
});

我使用angular.module方法扩展了sportsStoreAdmin模块,它是在admin.html文件中创建的。我使用constant方法指定将被用于验证请求的URL,以及创建authCtrl控制器,定义行为authenticate去接收username和password值作为参数,并使用$http.post方法向Deployd服务器发出Ajax请求,请求成功时我使用$location服务改变浏览器路径

提示:
我向$http.post提供了可选配置对象,它设置withCredentials为true。这会启用跨域请求的支持,允许Ajax请求使用cookie处理验证。不启用该项,浏览器将忽略Deployd响应的cookie

我需要在admin.html文件中引入包含控制器的JavaScript文件,注意确保它在定义了被扩展模块的script元素之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html ng-app="sportsStoreAdmin">

<head>
<title>Administration</title>
<script src="angular.js"></script>
<script src="ngmodules/angular-route.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("sportsStoreAdmin", ["ngRoute"])
.config(function ($routeProvider) {

$routeProvider.when("/login", {
templateUrl: "/views/adminLogin.html"
});

$routeProvider.when("/main", {
templateUrl: "/views/adminMain.html"
});

$routeProvider.otherwise({
redirectTo: "/login"
});
});
</script>
<script src="controllers/adminControllers.js"></script>
</head>

<body>
<ng-view />
</body>

</html>

定义验证视图
创建views/adminLogin.html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div class="well" ng-controller="authCtrl">

<div class="alert alert-info" ng-hide="authenticationError">
Enter your username and password and click Log In to authenticate
</div>
<div class="alert alert-danger" ng-show="authenticationError">
Authentication Failed ({{authenticationError.status}}). Try again.
</div>

<form name="authForm" novalidate>
<div class="form-group">
<label>Username</label>
<input name="username" class="form-control" ng-model="username" required />
</div>
<div class="form-group">
<label>Password</label>
<input name="password" type="password" class="form-control" ng-model="password" required />
</div>
<div class="text-center">
<button ng-click="authenticate(username, password)" ng-disabled="authForm.$invalid" class="btn btn-primary">
Log In
</button>
</div>
</form>
</div>

定义主视图和控制器
定义用于显示产品和订单列表的占位内容。首先,我创建views/adminProducts.html文件

1
2
3
<div class="well">
This is the product view
</div

然后,创建views/adminOrders.html文件

1
2
3
<div class="well">
This is the order view
</div

我需要占位,这样才能演示管理应用程序中的视图流。URL路由特性有一系列限制:你不能嵌套多个ng-view指令。这使得在ng-view中难以安排不同视图的显示。我将演示如何使用ng-include指令来处理它,作为不太优雅的替代品。我在adminControllers.js文件中定义新的控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
angular.module("sportsStoreAdmin")
.constant("authUrl", "http://localhost:5500/users/login")
.controller("authCtrl", function ($scope, $http, $location, authUrl) {

$scope.authenticate = function (user, pass) {
$http.post(authUrl, {
username: user,
password: pass
}, {
withCredentials: true
}).then(function (data) {
$location.path("/main");
}, function (error) {
$scope.authenticationError = error;
});
}
})
.controller("mainCtrl", function ($scope) {

$scope.screens = ["Products", "Orders"];
$scope.current = $scope.screens[0];

$scope.setScreen = function (index) {
$scope.current = $scope.screens[index];
};

$scope.getScreen = function () {
return $scope.current == "Products"
? "/views/adminProducts.html" : "/views/adminOrders.html";
};
});

新控制器叫做mainCtrl,它提供我使用ng-include指令管理视图所需要的行为和数据,这和生成切换视图的导航按钮一样,行为setScreen用于改变显示的视图,行为getScreen用于获取要显示的视图

修改adminMain.html文件

1
2
3
4
5
6
7
8
9
10
<div class="panel panel-default row" ng-controller="mainCtrl">
<div class="col-xs-3 panel-body">
<a ng-repeat="item in screens" class="btn btn-block btn-default" ng-class="{'btn-primary': item == current }" ng-click="setScreen($index)">
{{item}}
</a>
</div>
<div class="col-xs-8 panel-body">
<div ng-include="getScreen()" />
</div>
</div>

该视图使用ng-repeat指令为screens数组生成元素,ng-repeat指令定义了一些指定的变量,这些变量可以在其生成的元素内引用,其中之一就是$index。我在ng-click指令上用这个值来调用控制器行为setScreen
视图最重要的地方是使用ng-include指令,ng-include指令可以传入行为

实现订单特性
添加新的控制器到adminControllers.js文件,使用$http服务向Deployd发送Ajax的GET请求获取订单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
angular.module("sportsStoreAdmin")
.constant("authUrl", "http://localhost:5500/users/login")
.constant("ordersUrl", "http://localhost:5500/orders")
.controller("authCtrl", function ($scope, $http, $location, authUrl) {

// ...controller statements omitted for brevity...

})
.controller("mainCtrl", function ($scope) {

// ...controller statements omitted for brevity...

})
.controller("ordersCtrl", function ($scope, $http, ordersUrl) {

$http.get(ordersUrl, { withCredentials: true })
.then(function (response) {
$scope.orders = response.data;
}, function (error) {
$scope.error = error;
});

$scope.selectedOrder;

$scope.selectOrder = function (order) {
$scope.selectedOrder = order;
};

$scope.calcTotal = function (order) {
var total = 0;
for (var i = 0; i < order.products.length; i++) {
total +=
order.products[i].count * order.products[i].price;
}
return total;
}
});

我定义了URL常量。控制器函数产生到该URL的Ajax请求并将数据对象赋给作用域上的orders属性,或者赋给error对象。注意在调用$http.get方法时我设置了withCredentials配置项,和执行验证时的一样。这可以确保浏览器将用于安全验证的cookie发送给Deployd以验证请求
selectOrder被调用以设置selectedOrder属性,我将使用它获取订单详情。行为calcTotal算出订单中产品金额的总和
有了ordersCtrl控制器,我对adminOrders.html文件做了更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<div ng-controller="ordersCtrl">

<table class="table table-striped table-bordered">
<tr>
<th>Name</th>
<th>City</th>
<th>Value</th>
<th></th>
</tr>
<tr ng-repeat="order in orders">
<td>{{order.name}}</td>
<td>{{order.city}}</td>
<td>{{calcTotal(order) | currency}}</td>
<td>
<button ng-click="selectOrder(order)" class="btn btn-xs btn-primary">
Details
</button>
</td>
</tr>
</table>

<div ng-show="selectedOrder">
<h3>Order Details</h3>
<table class="table table-striped table-bordered">
<tr>
<th>Name</th>
<th>Count</th>
<th>Price</th>
</tr>
<tr ng-repeat="item in selectedOrder.products">
<td>{{item.name}}</td>
<td>{{item.count}}</td>
<td>{{item.price| currency}} </td>
</tr>
</table>
</div>
</div>

视图有两个table元素组成。第一个table显示订单摘要,连带一个button元素,它调用行为selectOrder以突出显示订单。第二个table只在订单被选中时可见,它显示订单详情

实现产品特性
你可以使用$http服务来做RESTful的API,但这么做意味着你必须暴露整组执行贯穿应用程序的操作的URL。你可以这样定义为你执行操作的服务,但更优雅的替代品是使用可选模块ngResource中的resource服务,它也有漂亮的方式处理用于发送请求到服务器的URL的定义

1.定义RESTful控制器
创建controllers/adminProductController.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
angular.module("sportsStoreAdmin")
.constant("productUrl", "http://localhost:5500/products/")
.config(function ($httpProvider) {
$httpProvider.defaults.withCredentials = true;
})
.controller("productCtrl", function ($scope, $resource, productUrl) {

$scope.productsResource = $resource(productUrl + ":id", { id: "@id" });

$scope.listProducts = function () {
$scope.products = $scope.productsResource.query();
}

$scope.deleteProduct = function (product) {
product.$delete().then(function () {
$scope.products.splice($scope.products.indexOf(product), 1);
});
}

$scope.createProduct = function (product) {
new $scope.productsResource(product).$save().then(function (newProduct) {
$scope.products.push(newProduct);
$scope.editedProduct = null;
});
}

$scope.updateProduct = function (product) {
product.$save();
$scope.editedProduct = null;
}

$scope.startEdit = function (product) {
$scope.editedProduct = product;
}

$scope.cancelEdit = function () {
$scope.editedProduct = null;
}

$scope.listProducts();
});

首先,$resource服务是建立在$http服务所提供的特性基础上的。这意味着我需要启用withCredentials选项做点适当的工作,我之前获取验证使用过它。我没有权限访问$http服务产生的请求,但我可以通过调用模块上的config方法改变所有Ajax请求的默认设置,并声明依赖于$http服务的提供器

1
2
3
4
5
...
.config(function ($httpProvider) {
$httpProvider.defaults.withCredentials = true;
})
...

本示例中最重要的一段是:

1
2
3
...
$scope.productsResource = $resource(productUrl + ":id", { id: "@id" });
...

传入$resource的第一个参数用来定义将用于产生擦洗的URL格式。“:id”部分与第二个参数的映射对象一致,告诉AngularJS如果数据对象中有id属性,那它应该被添加到用于Ajax请求的URL中
用于访问RESTful的API的URL和HTTP方法是由这两个参数推断出来的,这说明我并非一定要使用$http服务产生独立的Ajax调用
访问对象是使用$resource服务的结果,有query、get、delete、remove和save方法,可用来获取或操作服务器来的数据。调用这些方法触发执行必要操作的Ajax请求

提示:
由访问对象定义的方法与Deplooyd所定义的API并不完全一致,不过Deployd很灵活,足以接收$resource服务产生的请求

控制器中的大部分代码都以可用的方式为视图提供了它们的方法,但在$resource实现中这些方式有一点问题。由query方法返回的数据对象不会在对象被创建或删除时被自动更新,所以我必须包含用于保持本地集合与远端改变同步

2.定义视图
更新views/adminProducts.html视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<style>
#productTable {
width: auto;
}

#productTable td {
max-width: 150px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}

#productTable td input {
max-width: 125px;
}
</style>

<div ng-controller="productCtrl">
<table id="productTable" class="table table-striped table-bordered">
<tr>
<th>Name</th>
<th>Description</th>
<th>Category</th>
<th>Price</th>
<th></th>
</tr>
<tr ng-repeat="item in products" ng-hide="item.id == editedProduct.id">
<td>{{item.name}}</td>
<td class="description">{{item.description}}</td>
<td>{{item.category}}</td>
<td>{{item.price | currency}}</td>
<td>
<button ng-click="startEdit(item)" class="btn btn-xs btn-primary">
Edit
</button>
<button ng-click="deleteProduct(item)" class="btn btn-xs btn-primary">
Delete
</button>
</td>
</tr>
<tr ng-class="{danger: editedProduct}">
<td>
<input ng-model="editedProduct.name" required />
</td>
<td>
<input ng-model="editedProduct.description" required />
</td>
<td>
<input ng-model="editedProduct.category" required />
</td>
<td>
<input ng-model="editedProduct.price" required />
</td>
<td>
<button ng-hide="editedProduct.id" ng-click="createProduct(editedProduct)" class="btn btn-xs btn-primary">
Create
</button>
<button ng-show="editedProduct.id" ng-click="updateProduct(editedProduct)" class="btn btn-xs btn-primary">
Save
</button>
<button ng-show="editedProduct" ng-click="cancelEdit()" class="btn btn-xs btn-primary">
Cancel
</button>
</td>
</tr>
</table>
</div>

3.添加HTML文件的引用
在admin.html文件中添加script元素来引入新模块和新控制器并更新主应用程序模块,好让它声明依赖于ngResource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html ng-app="sportsStoreAdmin">

<head>
<title>Administration</title>
<script src="angular.js"></script>
<script src="ngmodules/angular-route.js"></script>
<script src="ngmodules/angular-resource.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("sportsStoreAdmin", ["ngRoute", "ngResource"])
.config(function ($routeProvider) {

$routeProvider.when("/login", {
templateUrl: "/views/adminLogin.html"
});

$routeProvider.when("/main", {
templateUrl: "/views/adminMain.html"
});

$routeProvider.otherwise({
redirectTo: "/login"
});
});
</script>
<script src="controllers/adminControllers.js"></script>
<script src="controllers/adminProductController.js"></script>
</head>

<body>
<ng-view />
</body>

</html>

第 9 章 AngularJS 应用剖析

AngularJS应用程序遵循的是第三章所描述的MVC模式,但是开发过程本身依赖于一系列更广泛的构件。当然,存在一些最主要的构件,如模型、视图和控制器,但是在AngularJS应用中还有许多其他可供灵活使用的部件,包括模块、指令、过滤器、工厂和服务

准备示例项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>AngularJS Demo</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script src="angular.js"></script>
<script>
var myApp = angular.module("exampleApp", []);
myApp.controller("dayCtrl", function ($scope) {
// controller statements will go here
});
</script>
</head>

<body>
<div class="panel" ng-controller="dayCtrl">
<div class="page-header">
<h3>AngularJS App</h3>
</div>
<h4>Today is {{day || "(unknown)"}}</h4>
</div>
</body>

</html>

使用模块工作

模块是AngularJS应用程序中的顶层组件。实际上无需引入模块也能搭建出简单的AngularJS应用,但是不推荐这么做,应为简单的应用程序随着时间推移会变复杂,当变得无法管理时最后的结果只能是重写整个程序。使用模块工作相当简单,而且需要设置和管理模块时仅需写少量额外的JavaScript语句,这是值得的投资。在一个AngularJS应用中模块具有三种主要角色:

  • 将AngularJS应用程序与HTML文档的区域相关联
  • 充当关键AngularJS框架功能的门户
  • 帮助组织AngularJS应用程序中的代码和组件

设置AngularJS应用程序的边界
在创建一个AngularJS应用程序时的第一步是定义一个模块并将其和HTML文档中的一部分区域关联起来。模块是通过AngularJS.module方法定义的

1
2
3
...
var myApp = angular.module("exampleApp", []);
...

module方法支持三个参数,但是通常只使用前两个
当创建一个将会与HTML文档相关联的模块时,惯例是给模块一个名为App的后缀名。这个习惯的好处在于更为清晰地体现出模块代表的是代码结构中的顶层AngularJS应用程序——这在会包含多个模块的复杂应用程序中将很有用

angular.module方法所接受的参数

名称 描述
name 新模块的名称
requires 该模块所依赖的模块集合
config 该模块的配置,等效于module.config方法

在JavaScript中定义模块只是整个过程的一部分,模块还必须通过ng-app属性应用到HTML内容中。当AngularJS是唯一被使用的框架时,惯例是将ng-app属性应用到html元素上

1
2
3
...
<html ng-app="exampleApp">
...

避免落入模块创建/查找陷阱
当创建一个模块时,你必须指定name和requires参数,即使你的模块并不存在依赖
如果忽略了requires参数,AngularJS就会试图查找一个之前创建过的名为name的模块,而不是创建一个。这可能会导致错误

使用模块定义AngularJS组件

angular.module方法返回一个module对象,也用于使用AngularJS所提供的一些重要特性

module对象的成员方法

名称 描述
animation(name, factory) 支持动画特性
config(callback) 注册一个在模块加载时对该模块进行配置的函数
constant(key, value) 定义一个返回常量的服务
controller(name, constructor) 创建一个控制器
directive(name, factory) 创建一个指令,对标准的HTML词汇进行扩展
factory(name, provider) 创建一个服务
filter(name, factory) 创建一个对显示给用户的数据进行格式化的过滤器
provider(name, type) 创建一个服务
name 返回模块名称
run(callback) 注册一个在AngularJS加载完毕后对所有模块进行配置的函数
service(name, constructor) 创建一个服务
value(name, value) 定义一个返回常量的服务

提示:
constant和value方法都将创建服务,只是这些服务的可用范围是有限的。这并不影响你使用这些方法,但我认为深刻理解AngularJS是如何广泛地使用服务还是不错的

module对象定义的方法分为三大类:为AngularJS应用程序定义组件的方法,使创建这些构建块更容易的方法,以及有助于管理AngularJS生命周期的方法

定义控制器
控制室是使用module.controller方法来定义的,该方法接受两个参数:控制器名称和一个工厂函数,该函数用于设置控制器并使其就绪

1
2
3
4
5
...
myApp.controller("dayCtrl", function ($scope) {
// controller statements will go here
});
...

控制器名称的习惯是使用Ctrl后缀。传给Model.controller的函数用于声明控制器的依赖,即控制器所需的AngularJS组件。AngularJS提供了一些以$符号开头命名的内置服务与特性。$scope请求AngularJS为控制器提供作用域
这是一个依赖注入的例子,AngularJS会检查函数的参数,并查找相应的组件。AngularJS将会在函数被调用时自动传入作用域对象

理解依赖注入
一个AngularJS应用程序中的一些组件将会依赖于其他组件
依赖注入简化了在组件之间处理依赖的过程。没有依赖注入就不得不以某种方式自己查找依赖项,很可能使用全局变量。这虽然也能够工作,但是不如AngularJS的依赖注入技术这么简单
AngularJS应用程序中的一个组件通过在工厂函数的参数上声明依赖,声明的名称要与所依赖的组件相匹配
换言之,依赖注入改变了函数参数的用途,没有依赖注入,参数将会被用于接收调用者想传入的任何对象,但有了依赖注入后,函数使用参数来提出需求,告诉AngularJS它需要什么样的构件
AngularJS中的依赖注入工作方式所带来的有趣的副作用之一是,参数的顺序总是得与声明依赖的顺序相匹配

1
2
3
...
myApp.controller("dayCtrl", function ($scope, $filter) {
...

传给函数的第一个参数将是$scope组件,第二个将是$filter服务对象

1
2
3
...
myApp.controller("dayCtrl", function ($filter, $scope) {
...

传给函数的第一个参数是$filter服务对象,第二个是$scope组件。简而言之,你声明依赖注入参数的顺序无关紧要。这一点不同与JavaScript通常的工作方式
在开发中使用依赖注入的好处是AngularJS负责管理组件并在需要时提供给相应的函数。依赖注入还能为测试带来好处,因为它允许你能够使用假的或者模拟的对象来代替真实构件,从而让你专注于程序的特定部分

1.将控制器用于视图
定义控制器只是整个过程的一部分——还必须将其应用于HTML元素才能让AngularJS知道HTML文档的哪一部分构成了给定的控制器的视图。这是通过ng-controller属性实现的

1
2
3
4
5
6
7
8
9
10
...
<body>
<div class="panel" ng-controller="dayCtrl">
<div class="page-header">
<h3>AngularJS App</h3>
</div>
<h4>Today is {{day || "(unknown)"}}</h4>
</div>
</body>
...

ng-controller被属性应用到元素及该元素包括的内容
在创建控制器时指定给参数的$scope参数是用于向视图提供数据的,而且只有通过$scope配置的数据才能用于表达式和数据绑定中。目前,当你在浏览器中查看example.html文件时,数据绑定会生成空“unknown”,因为我使用了“||”操作符来合并空值

1
2
3
...
<h4>Today is {{day || "(unknown)"}}</h4>
...

AngularJS的数据绑定的一个极好的特性是可以使用JavaScript表达式。要对day属性提供一个值,必须在控制器设置函数中将它赋给$scope

1
2
3
4
5
6
7
8
9
...
<script>
var myApp = angular.module("exampleApp", []);
myApp.controller("dayCtrl", function ($scope) {
var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Staturday"];
$scope.day = dayNames[new Date().getDay()]
});
</script>
...

2.创建多个视图
每个控制器可以支持多个视图,这允许同一份数据以多种不同形式展现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>AngularJS Demo</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script src="angular.js"></script>
<script>
var myApp = angular.module("exampleApp", []);
myApp.controller("dayCtrl", function ($scope) {
var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Staturday"];
$scope.day = dayNames[new Date().getDay()];
$scope.tomorrow = dayNames[(new Date().getDay() + 1) % 7];
});
</script>
</head>

<body>
<div class="panel">
<div class="page-header">
<h3>AngularJS App</h3>
</div>
<h4 ng-controller="dayCtrl">Today is {{day || "(unknown)"}}</h4>
<h4 ng-controller="dayCtrl">Tomorrow is {{tomorrow || "(unknown)"}}</h4>
</div>
</body>

</html>

我将ng-controller属性改变了位置,以使得能够在HTML文档中创建两个并存的简单视图
当然,只是用一个视图也能达到同样的效果,但这里我想演示的是控制器和视图可供使用的不同方式

3.创建多个控制器
除了最简单的应用程序之外,所有的程序几乎都包含多个控制器,每个控制器负责程序功能中的不同方面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>AngularJS Demo</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script src="angular.js"></script>
<script>
var myApp = angular.module("exampleApp", []);
myApp.controller("dayCtrl", function ($scope) {
var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Staturday"];
$scope.day = dayNames[new Date().getDay()];
});

myApp.controller("tomorrowCtrl", function ($scope) {
var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"];
$scope.day = dayNames[(new Date().getDay() + 1) % 7];
});
</script>
</head>

<body>
<div class="panel">
<div class="page-header">
<h3>AngularJS App</h3>
</div>
<h4 ng-controller="dayCtrl">Today is {{day || "(unknown)"}}</h4>
<h4 ng-controller="tomorrowCtrl">Tomorrow is {{day || "(unknown)"}}</h4>
</div>
</body>

</html>

我增加了一个名为tomorrowCtrl的控制器,用于计算出明天的星期名称。还修改了HTML以使每个控制器都有自己的视图

提示:
注意,在这两个视图中都能够使用day属性却互不影响。每个控制器都具有自己的作用域,dayCtrl与tomorrowCtrl控制器中的day属性是相互隔离的

使用Fluent API
module对象定义的方法返回的结果仍然是module对象本身。这听起来有点奇怪但却是一个简洁的技巧,使得能够使用Fluent API,即多个方法调用可以链式连接在一起。给出一个简单的例子,可以对上述示例进行重写,而不再需要定义myApp变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
<script>
angular.module("exampleApp", [])
.controller("dayCtrl", function ($scope) {
var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Staturday"];
$scope.day = dayNames[new Date().getDay()];
})
.controller("tomorrowCtrl", function ($scope) {
var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"];
$scope.day = dayNames[(new Date().getDay() + 1) % 7];
});
</script>
...

我调用了angular.module方法并得到了module对象作为返回结果,在这个对象上直接调用controller方法来建立dayCtrl控制器。从controller方法得到的结果与调用angular.module方法得到的结果是同一个对象,所以我可以再次使用它调用controller方法来建立tomorrowCtrl

定义指令
指令是最强大的AngularJS特性了,因为通过它们能够扩展并增强HTML,从而创建丰富的Web应用程序。AngularJS包含许多内置指令,通过module.directive方法可以创建自己定义指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>AngularJS Demo</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script src="angular.js"></script>
<script>
angular.module("exampleApp", [])
.controller("dayCtrl", function ($scope) {
var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Staturday"];
$scope.day = dayNames[new Date().getDay()];
})
.controller("tomorrowCtrl", function ($scope) {
var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"];
$scope.day = dayNames[(new Date().getDay() + 1) % 7];
})
.directive("highlight", function () {
return function (scope, element, attrs) {
if (scope.day == attrs["highlight"]) {
element.css("color", "red");
}
}
});
;
</script>
</head>

<body>
<div class="panel">
<div class="page-header">
<h3>AngularJS App</h3>
</div>
<h4 ng-controller="dayCtrl" highlight="Monday">
Today is {{day || "(unknown)"}}
</h4>
<h4 ng-controller="tomorrowCtrl">Tomorrow is {{day || "(unknown)"}}</h4>
</div>
</body>

</html>

工厂函数和工人函数
所有的可用于创建AngularJS构件的module方法哦都可以接收函数作为参数。这些函数通常被称为工厂函数,之所以这么叫是因为它们创建那些将被AngularJS用来执行工作的对象。工厂函数通常会返回一个工人函数,也就是说将被AngularJS用来执行工作的对象也是一个函数。再上述示例中传给directive方法的第二个参数是一个工厂函数,工厂函数中的return语句返回的是另一个函数,每次使用这个指令是AngularJS就会调用它,这就是工人函数

将指令应用于HTML元素

1
2
3
...
<h4 ng-controller="dayCtrl" highlight="Monday">
...

定义过滤器
过滤器被使用再视图中,用于格式化展现给用户的数据。一旦定义过滤器之后,就可以在整个模块中全面应用,也就意味着可以用来保证跨多个控制器和视图之间的数据展示的一致性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>AngularJS Demo</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script src="angular.js"></script>
<script>
var myApp = angular.module("exampleApp", []);

myApp.controller("dayCtrl", function ($scope) {
$scope.day = new Date().getDay();
});
myApp.controller("tomorrowCtrl", function ($scope) {
$scope.day = new Date().getDay() + 1;
});
myApp.directive("highlight", function () {
return function (scope, element, attrs) {
if (scope.day == attrs["highlight"]) {
element.css("color", "red");
}
}
});

myApp.filter("dayName", function () {
var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"];
return function (input) {
return angular.isNumber(input) ? dayNames[input] : input;
};
});
</script>
</head>

<body>
<div class="panel">
<div class="page-header">
<h3>AngularJS App</h3>
</div>
<h4 ng-controller="dayCtrl" highlight="Monday">
Today is {{day || "(unknown)" | dayName}}
</h4>
<h4 ng-controller="tomorrowCtrl">
Tomorrow is {{day || "(unknown)" | dayName}}
</h4>
</div>
</body>

</html>

filter方法用于定义一个过滤器,其参数是新过滤器的名称,以及一个在调用时会创建过滤器的工厂函数

1.使用过滤器
过滤器应用在视图里所包含的模板表达式中。数据绑定或者表达式后紧跟一个竖线“|”以及过滤器的名称

1
2
3
4
5
...
<h4 ng-controller="dayCtrl" highlight="Monday">
Today is {{day || "(unknown)" | dayName}}
</h4>
...

2.修复指令
你可能已经注意到了,过滤器破坏了之前创建的指令的功能。这是因为控制器里现在向作用域里添加了一个数值形式的变量来代表当天,而不是经过格式化的字符串名称。有许多方法解决这个问题,比如修改指令的值以使用数值型——但这里想演示一个稍微复杂一些的方法

1
2
3
4
5
6
7
8
9
10
11
...
myApp.directive("highlight", function ($filter) {
var dayFilter = $filter("dayName");

return function (scope, element, attrs) {
if (dayFilter(scope.day) == attrs["highlight"]) {
element.css("color", "red");
}
}
});
...

我想使用这个例子说明的是,在AngularJS应用程序中创建的构件并不仅限于在HTML元素上使用。在你的JavaScript代码里也可以使用
在本例中,向指令的工厂函数添加了$filter参数,告诉AngularJS当我的函数被调用是要接收过滤器服务对象。$filter服务允许我访问所有已定义的过滤器,包括前例中的自定义过滤器,通过名称我将获取到我的过滤器

1
2
3
..
var dayFilter = $filter("dayName");
...

这样我就能得到工厂函数所创建的过滤器函数,然后可以调用该函数将数值类型转换为一个名称

1
2
3
...
if (dayFilter(scope.day) == attrs["highlight"]) {
...

定义服务
服务是提供在整个应用程序中所使用的任何功能的单例对象。对于普通任务,AngularJS自带了一些有用的内置服务,例如创建HTTP请求。一些关键的方法也被AngularJS作为服务,包括前面例子中使用的$scope和$filter对象。你也可以创建自己的服务
module对象所定义的方法中有三个是用来以不同的方式创建服务的:service、factory和provider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>AngularJS Demo</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script src="angular.js"></script>
<script>
var myApp = angular.module("exampleApp", []);

myApp.controller("dayCtrl", function ($scope, days) {
$scope.day = days.today;
});

myApp.controller("tomorrowCtrl", function ($scope, days) {
$scope.day = days.tomorrow;
});

myApp.directive("highlight", function ($filter) {
var dayFilter = $filter("dayName");

return function (scope, element, attrs) {
if (dayFilter(scope.day) == attrs["highlight"]) {
element.css("color", "red");
}
}
});

myApp.filter("dayName", function () {
var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"];
return function (input) {
return angular.isNumber(input) ? dayNames[input] : input;
};
});

myApp.service("days", function () {
this.today = new Date().getDay();
this.tomorrow = this.today + 1;
});
</script>
</head>

<body>
<div class="panel">
<div class="page-header">
<h3>AngularJS App</h3>
</div>
<h4 ng-controller="dayCtrl" highlight="Monday">
Today is {{day || "(unknown)" | dayName}}
</h4>
<h4 ng-controller="tomorrowCtrl">
Tomorrow is {{day || "(unknown)" | dayName}}
</h4>
</div>
</body>

</html>

service方法具有两个参数:服务名和调用后用来创建服务对象的工厂函数。当AngularJS调用工厂函数时,会分配一个可通过this关键字访问的新对象,我可以使用这个对象来定义属性。这是一个简单的服务,但是意味着我可以在应用程序的任意位置通过我的服务访问其中定义的属性,这有助于简化开发过程

提示:
即使在controller方法之后才调用service方法定义服务,也可以在控制器中使用服务

通过声明对服务的依赖可以访问定义的服务

1
2
3
...
myApp.controller("tomorrowCtrl", function ($scope, days) {
...

AngularJS使用依赖注入来查找days服务并将其作为参数传递给工厂函数,这意味着我们可以直接获取到服务中定义的属性的值

1
2
3
4
5
...
myApp.controller("tomorrowCtrl", function ($scope, days) {
$scope.day = days.tomorrow;
});
...

定义值
module.value用于创建返回固定值和对象的服务。这可能看起来有点奇怪,但是却意味着你可以为任何值或对象使用依赖注入,而不仅仅是使用module方法创建的那些对象。这使得开发体验更为一致化,使单元测试更为简化,并允许你使用一些高级特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
var myApp = angular.module("exampleApp", []);

// ...statements omitted for brevity...

var now = new Date();
myApp.value("nowValue", now);


myApp.service("days", function (nowValue) {
this.today = nowValue.getDay();
this.tomorrow = this.today + 1;
});
</script>

在不使用value的情况下使用对象
使用值服务看起来好像增加了不必要的复杂性,而且如果说仅仅是为了单元测试方便而增加参数的话,是无法令人信服的。即使这样,你仍将发现创建AngularJS值服务比不适用值服务要简单,因为AngularJS假设工厂函数的任意参数都 声明了需要解析的依赖。AngularJS新手经常会试图写如下代码,不使用值服务:

1
2
3
4
5
6
7
8
...
var now = new Date();

myApp.service("days", function (now) {
this.today = now.getDay();
this.tomorrow = this.today + 1;
});
...

如果你试图运行这段代码,你将会在浏览器JavaScript控制台中看到类似这样的报错:

Error: [$injector:unpr] Unknown provider: nowProvider <- now <- days

这里的问题在于,当调用工厂函数时,AngularJS不会为now参数使用那个局部变量,当引用now变量时它已经不存在与作用域中了
如果你坚决不想创建AngularJS值服务,那么可以依赖JavaScript的闭包特性,它允许你在定义内部函数时从函数里引用外部变量:

1
2
3
4
5
6
7
8
...
var now = new Date();

myApp.service("days", function () {
this.today = now.getDay();
this.tomorrow = this.today + 1;
});
...

我从工厂函数中移除了一些参数,就意味着AngularJS不会去试图查找要解析的依赖。这段代码可以工作,但却使得days服务更难测试了,我的建议是遵循AngularJS的创建值服务的方法

使用模块组织代码

在前面的示例中,演示了AngularJS在创建诸如控制器、指令、过滤器、和服务时,是如何结合工厂函数使用依赖注入的。我前面解释过angular.module方法的第二个参数是用于创建服务的,是一个该模块的依赖构成的数组

1
2
3
...
var myApp = angular.module("exampleApp", []);
...

任何AngularJS模块都可以依赖于在其他模块中定义的组件,在复杂应用程序中这是一个能够使组织代码更为容易的特性。为了演示这一特性我在angularjs文件夹下添加了一个名为controllers.js的文件

1
2
3
4
5
6
7
8
9
var controllersModule = angular.module("exampleApp.Controllers", [])

controllersModule.controller("dayCtrl", function ($scope, days) {
$scope.day = days.today;
});

controllersModule.controller("tomorrowCtrl", function ($scope, days) {
$scope.day = days.tomorrow;
});

在controllers.js文件中创建了一个名为exampleApp.Controllers的新模块,并使用它定义了前面例子中的两个控制器。一种通常的习惯用法是将你的应用程序组织成具有相同类型组件的模块,并通过使用主模块名加构件类型的命名方式清晰地表明该模块包含什么样的构件,这也是为什么这里起名为exampleApp.Controllers。类似地,我创建了第二个JavaScript文件名为filters.js

1
2
3
4
5
6
7
angular.module("exampleApp.Filters", []).filter("dayName", function () {
var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"];
return function (input) {
return angular.isNumber(input) ? dayNames[input] : input;
};
});

我创建了一个名为exampleApp.Filters的模块,并使用它定义了前面例子中曾是主模块一部分的过滤器。有一个变化,我使用了fluent API在module方法的返回结果上调用了filter方法

提示:
将模块放到自己的文件夹里或者基于所包含的构件来组织模块并不是必需的,但我通常更加偏好这种方式,对于正在寻找自己的AngularJS开发过程和偏好的你来说也是一个良好的起点

接下来可以看到我是如何添加脚本元素以引入controllers.js和filters.js文件,并将它们所包含的模块作为依赖添加到主模块exampleApp中的,在example.html文件中我还另外创建了两个模块,以强调模块并不是必须要定义在自己的独立文件里的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>AngularJS Demo</title>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script src="angular.js"></script>
<script src="controllers.js"></script>
<script src="filters.js"></script>
<script>
var myApp = angular.module("exampleApp",
["exampleApp.Controllers", "exampleApp.Filters",
"exampleApp.Services", "exampleApp.Directives"]);

angular.module("exampleApp.Directives", [])
.directive("highlight", function ($filter) {
var dayFilter = $filter("dayName");
return function (scope, element, attrs) {
if (dayFilter(scope.day) == attrs["highlight"]) {
element.css("color", "red");
}
}
});

var now = new Date();
myApp.value("nowValue", now);

angular.module("exampleApp.Services", [])
.service("days", function (nowValue) {
this.today = nowValue.getDay();
this.tomorrow = this.today + 1;
});
</script>
</head>

<body>
<div class="panel">
<div class="page-header">
<h3>AngularJS App</h3>
</div>
<h4 ng-controller="dayCtrl" highlight="Monday">
Today is {{day || "(unknown)" | dayName}}
</h4>
<h4 ng-controller="tomorrowCtrl">
Tomorrow is {{day || "(unknown)" | dayName}}
</h4>
</div>
</body>

</html>

为了对主模块声明依赖,我将每个模块的名称添加到一个数组中,并传递给主模块作为第二个参数

1
2
3
4
...
var myApp = angular.module("exampleApp",
["exampleApp.Controllers", "exampleApp.Filters", "exampleApp.Services", "exampleApp.Directives"]);
...

这些依赖并不是按照某种特定顺序定义的,而且你也可以按照任何顺序定义模块。例如,我先定义了exampleApp模块之后才定义exampleApp.Services模块,虽然exampleApp模块依赖于exampleApp.Services模块
AngularJS会加载定义在程序中的所有模块并解析依赖,将每个模块所包含的构件进行合并。这个合并是很重要的,因为它使得无缝地使用来自其他模块的功能成为可能。例如,在exampleApp.Services模块模块中的days服务依赖于来自exampleApp模块中的nowValue值服务,以及exampleApp.Directives模块中的指令依赖于exampleApp.Filters模块中的过滤器
你可以根据喜好在其他模块中放入尽可能多或尽可能少的功能,在本例中我定义了四个模块,但却将值服务留在主模块定义。我也可以为值服务专门创建一个模块,一个混合了服务和值服务的模块,或者一个适宜我的开发风格的其他混合形式的模块

使用模块生命周期进行工作
module.config和module.run方法注册了那些在AngularJS应用的生命周期的关键时刻所调用的函数。传给config方法的函数在当前模块被加载后调用,传给run方法的函数在所有模块被加载后调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
...
<script>
var myApp = angular.module("exampleApp",
["exampleApp.Controllers", "exampleApp.Filters",
"exampleApp.Services", "exampleApp.Directives"]);

myApp.constant("startTime", new Date().toLocaleTimeString());
myApp.config(function (startTime) {
console.log("Main module config: " + startTime);
});
myApp.run(function (startTime) {
console.log("Main module run: " + startTime);
});

angular.module("exampleApp.Directives", [])
.directive("highlight", function ($filter) {
var dayFilter = $filter("dayName");
return function (scope, element, attrs) {
if (dayFilter(scope.day) == attrs["highlight"]) {
element.css("color", "red");
}
}
});

var now = new Date();
myApp.value("nowValue", now);

angular.module("exampleApp.Services", [])
.service("days", function (nowValue) {
this.today = nowValue.getDay();
this.tomorrow = this.today + 1;
})
.config(function () {
console.log("Services module config: (no time)");
})
.run(function (startTime) {
console.log("Services module run: " + startTime);
});
</script>
...

在本示例的第一处修改时使用了constant方法,这个方法与value方法类似,但是创建的服务能够作为config方法所声明的依赖使用,value服务却做不到这点
config方法接收一个函数,该函数在调用方法的模块被加载后调用。config方法通常通过注入来自其他服务的值(比如连接的详细信息或者用户凭证)的方式用于配置模块
run方法也可以接收一个函数,但是函数只会在所有模块加载完后以及解析完它们的依赖后才会被调用
以下是这些回调函数的调用顺序

  1. exampleApp.Services模块的config回调函数
  2. exampleApp模块的config回调函数
  3. exampleApp.Services模块的run回调函数
  4. exampleApp模块的run回调函数

AngularJS做了一些聪明的事情,保证那些具有依赖的模块首先调用其依赖的回调函数。可以看到exampleApp.Services模块的回调优先于exampleApp的回调被调用。这允许了模块在被用于解析模块依赖之前对自己进行配置,如果运行这个例子,可以从JavaScript控制台看到以下输出:

Services module config: (no time)
Main module config: 16:57:28
Services module run: 16:57:28
Main module run: 16:57:28

第 10 章 使用绑定和模板指令

为什么以及何时使用指令

为什么以及何时使用指令

为什么使用 什么时候使用
指令暴露了AngularJS的核心功能,如事件处理、表单验证和模板。你可以使用自定义指令在视图中使用你的程序功能 指令在AngularJS程序中的各个部分都能使用

准备示例项目

在angularjs文件夹中创建directives.html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Directives</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.todos = [
{ action: "Get groceries", complete: false },
{ action: "Call plumber", complete: false },
{ action: "Buy running shoes", complete: true },
{ action: "Buy flowers", complete: false },
{ action: "Call family", complete: false }];
});
</script>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>
Data items will go here...
</div>
</body>

</html>

使用数据绑定指令

内置指令的第一类是负责执行数据绑定的。数据绑定使用模型中的值并将其插入到HTML文档中

数据绑定指令

指令 用作 描述
ng-bind 属性、类 绑定HTML元素的innerText属性
ng-bind-html 属性、类 绑定HTML元素的innerHTML属性。浏览器将把内容解释为HTML
ng-bind-template 属性、类 与ng-bind指令类似,但是允许在属性值中指定多个模板表达式
ng-model 属性、类 创建一个双向数据绑定
ng-non-bindable 属性、类 声明一块不会执行数据绑定的区域

属性:HTML属性
类:HTML样式类

使用指令
所有的数据绑定指令都可以当作一个属性或者类使用。一般来说使用指令的方式只是一种风格偏好的问题,我一般更喜欢将指令用作属性

1
2
3
...
There are <span ng-bind="todos.length"></span> items
...

指令被指定为属性名,关于指令的配置则被设置为属性值
如果你不能或者不想使用这种方式,你也可以将指令配置配置为用作标准class属性的方式

1
2
3
...
There are <span class="ng-bind: todos.length"></span> items
...

并不是所有的指令都可以以任意一种方式使用,有些指令只能被当作自定义元素使用

执行和禁止单向绑定
ng-bind指令负责创建单项数据绑定,但是很少直接使用它,因为AngularJS在HTML文档中遇到双大括号时,也会创建这种绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Directives</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.todos = [
{ action: "Get groceries", complete: false },
{ action: "Call plumber", complete: false },
{ action: "Buy running shoes", complete: true },
{ action: "Buy flowers", complete: false },
{ action: "Call family", complete: false }];
});
</script>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>

<div>There are {{todos.length}} items</div>

<div>
There are
<span ng-bind="todos.length"></span> items
</div>

<div ng-bind-template="First: {{todos[0].action}}. Second: {{todos[1].action}}"></div>

<div ng-non-bindable>
AngularJS uses {{ and }} characters for templates
</div>
</div>
</body>

</html>

提示:
AngularJS不是唯一使用双大括号的JavaScript包,因此如果同时使用多个库时可能会遇到问题。AngularJS允许修改用于内联绑定的字符

组织内联数据绑定
内联绑定的缺点是AngularJS将寻找并处理内容中的每一对双大括号。这有可能成文问题,特别是在混用JavaScript工具包并想在HTML的某部分上使用一些其他模板系统时(或者只是想以文本方式输出双大括号时)。解决方案是使用ng-non-bindable指令

1
2
3
4
5
...
<div ng-non-bindable>
AngularJS uses {{ and }} characters for templates
</div>
...

如果我没有使用这个指令,AngularJS将处理div元素的内容并试图绑定到名为and的模型属性。在请求一个不存在的模型属性时,AngularJS也不会报错,它假定这个属性将会在之后创建。相应的是,他根本不会插入任何内容

创建双向数据绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>

<div class="well">
<div>The first item is: {{todos[0].action}}</div>
</div>

<div class="form-group well">
<label for="firstItem">Set First Item:</label>
<input name="firstItem" class="form-control" ng-model="todos[0].action" />
</div>
</div>
</body>
...

双向数据绑定仅能应用于那些允许用户输入数据值的元素上

提示:
ng-model指令对使用表单进行工作甚至对于创建自定义表单指令提供了附加的特性

数据模型上的变化被传播到所有相关绑定上,以保证在整个应用程序中保持同步

提示:
在本例中,使用了通过控制器工厂函数中的$scope服务显示地添加到数据模型中的属性。数据绑定的一个很好的特性是AngularJS将在需要时动态地创建模型属性,也就是说无需费力地定义所有要使用的属性,就可以和视图关联到一起

使用模板指令

Web应用程序往往都在相似的数据对象集合上进行操作,并且使展示给用户的视图随着不同的数据值而变化
AngularJS包含了一组可使用模板生成HTML元素的指令,使得使用数据集合进行工作,以及向响应数据状态的模板中添加基本逻辑变得更为简单

模板指令

指令 用作 描述
ng-cloak 属性、类 应用隐藏内联绑定表达式的CSS样式,在文档首次加载时可以短暂显示
ng-include 元素、属性、类 将HTML片段加载,处理并插入到文档对象模型中
ng-repeat 属性、类 为对象中的数组或属性中的每个对象生成单个元素及其内容的新副本
ng-repeat-start 属性、类 表示具有多个顶级元素的重复节的开始
ng-repeat-end 属性、类 表示具有多个顶级元素的重复节的结束
ng-switch 元素、属性 根据数据绑定的值更改文档对象模型中的元素

属性:HTML属性
类:HTML样式类

这些指令使你不用写任何JavaScript代码就可以向视图中添加简单的逻辑

重复生成元素
通过ng-repeat指令完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>

<table class="table">
<thead>
<tr>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in todos">
<td>{{item.action}}</td>
<td>{{item.complete}}</td>
</tr>
</tbody>
</table>
</div>
</body>

这是使用ng-repeat指令最简单而常见的方式:使用一个对象集合为table元素生成若干行。使用ng-repeat指令的方法可以分为两部分。第一部分是指定数据源以及在模板中要处理的对象被引用的名称

1
2
3
...
<tr ng-repeat="item in todos">
...

ng-repeat指令属性值的基本形式是<variable> in <source>其中source是被控制器的$scope所定义的一个对象或者数组。该指令遍历数组中的对象,创建元素及其内容的一个新实例,并且处理所包含的模板。在指令属性值中赋给<variable>的名称可以用于引用当前数据对象

1
2
3
4
5
6
...
<tr ng-repeat="item in todos">
<td>{{item.action}}</td>
<td>{{item.complete}}</td>
</tr>
...

1.重复操作对象属性
前一例子中使用ng-repeat指令遍历数组中的对象,但是你也可以遍历一个对象中的属性。ng-repeat指令还可以被嵌套使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
<table class="table">
<thead>
<tr>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in todos">
<td ng-repeat="prop in item">{{prop}}</td>
</tr>
</tbody>
</table>
...

外层的ng-repeat指令为todos数组中的每一个对象生成一个tr元素,而且每个对象被赋给变量item。内层的ng-repeat指令为item对象的每个属性生成一个td元素并将属性值赋给变量prop。最后prop用一个单项数据绑定,作为td元素的内容

2.使用数据对象的键值进行工作
对于ng-repeat指令的配置有一个可供替代的语法选项,允许你从被处理的每个属性或者数据对象中接收一个键值

1
2
3
4
5
6
7
...
<tr ng-repeat="item in todos">
<td ng-repeat="(key, value) in item">
{{key}}={{value}}
</td>
</tr>
...

与单个变量名不同的是,我指定了被一对圆括号包括并以逗号分隔的两个名称。对ng-repeat指令所遍历的每个对象或者属性来说,第二个变量将被赋以数据对象或者属性的值。第一个变量的使用方式则依赖于数据源。对于对象key是当前属性名,而对于集合key则是当前对象所处的位置

3.使用内置变量工作
ng-repeat指令将当前对象或者属性赋给你所指定的变量,但是还有一组内置变量可用于提供被处理数据的上下文信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos">
<td>{{$index + 1}}</td>
<td ng-repeat="prop in item">
{{prop}}
</td>
</tr>
</table>
...

这里使用到了ng-repeat指令提供的$index变量来显示数组中每一项的位置

内置的ng-repeat变量

变量 描述
$index 返回当前对象或属性的位置
$first 在当前对象为集合中的第一个对象时返回true
$middle 在当前对象不是集合中的第一个也不是最后一个对象时返回true
$last 在当前对象为集合中的最后一个对象时返回true
$even 对于集合中偶数编号的对象返回true
$odd 对于集合中奇数编号的对象返回true

可以使用这些变量来控制生成的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Directives</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.todos = [
{ action: "Get groceries", complete: false },
{ action: "Call plumber", complete: false },
{ action: "Buy running shoes", complete: true },
{ action: "Buy flowers", complete: false },
{ action: "Call family", complete: false }];
});
</script>
<style>
.odd {
background-color: lightcoral
}

.even {
background-color: lavenderblush
}
</style>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>

<table class="table">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos" ng-class="$odd ? 'odd' : 'even'">
<td>{{$index + 1}}</td>
<td ng-repeat="prop in item">
{{prop}}
</td>
</tr>
</table>
</div>
</body>

</html>

我使用ng-class指令来设置使用了数据绑定的元素的class属性

提示:
也可以直接使用ng-class-even和ng-class-odd指令

这些内置变量也可以和其他指令结合实现更复杂的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos" ng-class="$odd ? 'odd' : 'even'">
<td>{{$index + 1}}</td>
<td>{{item.action}}</td>
<td>
<span ng-if="$first || $last">{{item.complete}}</span>
</td>
</tr>
</table>
...

重复生成多个顶层元素
ng-repeat指令对所处理的对象或属性重复生成一个顶层元素及其内容。有时候需要对每个数据对象重复生成多个顶层元素。在需要对每个处理的数据项生成多个表格行时最常遇到这种问题——这很难用ng-repeat指令来解决,因为在tr元素及其父元素之间不允许使用任何中间元素。要解决这个问题,可以使用ng-repeat-start和ng-repeat-end指令

1
2
3
4
5
6
7
8
9
10
11
12
13
...
<table class="table">
<tr ng-repeat-start="item in todos">
<td>This is item {{$index}}</td>
</tr>
<tr>
<td>The action is: {{item.action}}</td>
</tr>
<tr ng-repeat-end>
<td>Item {{$index}} is {{$item.complete? '' : "not "}} complete</td>
</tr>
</table>
...

ng-repeat-start指令的配置方法类似于ng-repeat,但是将会重复生成所有顶层元素直到应用了ng-repeat-end属性的元素

使用局部视图工作
ng-include指令从服务器获取一段HTML片段,编译并处理其中包含的任何指令,并添加到DOM中。这些片段被称为局部视图。为了演示这是如何工作的,我在angularjs文件夹下添加了一个名为table.html的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos" ng-class="$odd ? 'odd' : 'even'">
<td>{{$index + 1}}</td>
<td ng-repeat="prop in item">{{prop}}</td>
</tr>
</table>

接着在directives.html文件中使用ng-include指令来加载、处理table.html文件

1
2
3
4
5
6
7
<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>

<ng-include src="'table.html'"></ng-include>
</div>
</body>

这是我们遇到的第一个既能够用作HTML元素,也能够用作属性或者类的内置指令
ng-include指令支持3个配置参数,当指令被当作元素使用时,这些参数作为属性使用

注意:
不要将ng-incude作为空元素使用。否则ng-include元素之后的内容会被从DOM中移除

ng-include指令的配置参数

名称 描述
src 指定要加载的内容的URL
onload 指定加载内容时要使用的表达式
autoscroll 指定在内容被加载时AngularJS是否应该滚动到这部分视图所在的区域

动态地选择局部视图
也许你已经注意到我指定ng-include指令应该从服务器请求哪个文件的方式有点奇怪

1
2
3
...
<ng-include src="'table.html'"></ng-include>
...

我将table.html指定为一个字符串,必须这样做是因为src属性是被当作JavaScript表达式进行计算的,要静态地定义一个文件,就得使用字符串字面量
ng-include指令的真正威力在于scr的设置可以通过计算得到。为了演示这是如何工作的,我在angularjs文件夹下添加了一个名为list.html的文件

1
2
3
4
5
6
<ol>
<li ng-repeat="item in todos">
{{item.action}}
<span ng-if="item.complete"> (Done)</span>
</li>
</ol>

接着更新directives.html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Directives</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.todos = [
{ action: "Get groceries", complete: false },
{ action: "Call plumber", complete: false },
{ action: "Buy running shoes", complete: true },
{ action: "Buy flowers", complete: false },
{ action: "Call family", complete: false }];

$scope.viewFile = function () {
return $scope.showList ? "list.html" : "table.html";
};
});
</script>
<style>
.odd {
background-color: lightcoral
}

.even {
background-color: lavenderblush
}
</style>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>

<div class="well">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="showList"> Use the list view
</label>
</div>
</div>

<ng-include src="viewFile()"></ng-include>
</div>
</body>

</html>

我在控制器中定义了一个名为viewFile的行为,并根据变量showList的值返回之前创建的两个html片段文件的名字。然后我添加了一个复选框并使用了双向绑定以改变showList的值。最后我修改了ng-include指令src属性的值,使其使用viewFile的返回值

将ng-include指令用作属性
首先为原有的ng-include指令添加onload属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Directives</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.todos = [
{ action: "Get groceries", complete: false },
{ action: "Call plumber", complete: false },
{ action: "Buy running shoes", complete: true },
{ action: "Buy flowers", complete: false },
{ action: "Call family", complete: false }];

$scope.viewFile = function () {
return $scope.showList ? "list.html" : "table.html";
};

$scope.reportChange = function () {
console.log("Displayed content: " + $scope.viewFile());
}
});
</script>
<style>
.odd {
background-color: lightcoral
}

.even {
background-color: lavenderblush
}
</style>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>

<div class="well">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="showList"> Use the list view
</label>
</div>
</div>

<ng-include src="viewFile()" onload="reportChange()"></ng-include>
</div>
</body>

</html>

假定我不想或者无法使用自定义元素,则可以将自定义元素ng-include使用以下语句替换

1
2
3
...
<div ng-include="viewFile()" onload="reportChange()"></div>
...

ng-include可以作为任何元素的属性,src参数将从该属性值中获得,其他的指令配置参数以单独的属性表示

有条件地交换元素
ng-include指令对于处理较重要的局部代码片段是极佳的,但是你经常会需要在已经存在于文档中的较小代码块之间进行切换——对于这种情况,AngularJS提供了ng-switch指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Directives</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {

$scope.data = {};

$scope.todos = [
{ action: "Get groceries", complete: false },
{ action: "Call plumber", complete: false },
{ action: "Buy running shoes", complete: true },
{ action: "Buy flowers", complete: false },
{ action: "Call family", complete: false }];
});
</script>
<style>
.odd {
background-color: lightcoral
}

.even {
background-color: lavenderblush
}
</style>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>

<div class="well">
<div class="radio" ng-repeat="button in ['None', 'Table', 'List']">
<label>
<input type="radio" ng-model="data.mode" value="{{button}}" ng-checked="$first" /> {{button}}
</label>
</div>
</div>

<div ng-switch on="data.mode">
<div ng-switch-when="Table">
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos" ng-class="$odd ? 'odd' : 'even'">
<td>{{$index + 1}}</td>
<td ng-repeat="prop in item">{{prop}}</td>
</tr>
</table>
</div>
<div ng-switch-when="List">
<ol>
<li ng-repeat="item in todos">
{{item.action}}
<span ng-if="item.complete"> (Done)</span>
</li>
</ol>
</div>
<div ng-switch-default>
Select another option to display a layout
</div>
</div>
</div>
</body>

</html>

这个例子首先使用ng-repeat指令生成一组单选按钮,这组单选按钮使用双向数据绑定来设置data.mode模型属性的值

提示:
我将作用域属性mode定义为一个名为data的对象的属性。这是必要的,因为AngularJS作用域的继承方式和一些指令创建自已作用域的方式决定了需要这样做

本例剩余的部分演示了ng-switch指令的使用

提示:
ng-switch指令可以被当作元素使用,但是ng-switch-when和ng-switch-default部分只能当作属性使用。正因为这样,为了保持一致而将ng-switch也作为属性使用

ng-switch指令中使用了on属性指定了一个表达式,用于计算并决定哪部分内容将被显示出来

1
2
3
...
<div ng-switch on="data.mode">
...

然后使用ng-switch-when指令表示与所指定的值相关联的一块内容

1
2
3
4
5
6
7
8
9
10
11
12
...
<div ng-switch-when="Table">
<table class="table">
<!-- elements omitted for brevity -->
</table>
</div>
<div ng-switch-when="List">
<ol>
<!-- elements omitted for brevity -->
</ol>
</div>
...

当属性值与on属性所指定的表达式相匹配时,AngularJS将会显示出ng-switch-when指令所应用到的元素。其他在ng-switch指令代码块里的元素将被移除。ng-switch-defaule指令用于指定没有任何一个ng-switch-when区域匹配时应当显示的内容

1
2
3
4
5
...
<div ng-switch-default>
Select another option to display a layout
</div>
...

ng-switch指令会在其数据绑定中的值发生变化时做出响应

在ng-include和ng-switch指令之间做选择
ng-include和ng-switch指令可以产生同样的效果,所以有时候难以决定该用其中哪个指令以达到最佳效果
当需要在较小而简单的内容块之间进行切换,而且在Web程序的正常执行过程中需要向用户展示大部分或所有这些内容块时,使用ng-switch指令。这是因为必须将ng-switch指令所需要的所有内容作为HTML文档的一部分交付,如果有内容不大可能会用到时这将会造成带宽的浪费,使加载时间变长
ng-include指令更适用于较复杂的内容或者在整个应用程序中需要分别独立使用的内容。当需要在不同的地方包含进相同的内容时,局部视图有助于减少项目中的重复内容,但是要记住局部视图是直到第一次被引用时才会被请求,这会在浏览器发出Ajax请求并从服务器接收响应时造成延迟
如果拿不准的话,就先使用ng-switch指令。它更简单而且易于使用,当内容变得太复杂而难以管理或者需要在同一应用中的其他地方使用相同内容时,可以再改为使用ng-include指令

隐藏未处理的内联模板绑定表达式
在较慢的设备上对复杂内容进行处理时,在AngularJS仍在解析HTML、处理指令和进行准备工作时,会有一个浏览器对HTML进行加载显示的时刻。在这一时刻,任何所定义的内联模板表达式将会对用户可见
虽然现在这种情况是十分罕见的,但是它确实能够发生。有两种方法解决这一问题。第一种方法是避免使用内联模板表达式,坚持使用ng-bind指令。但是这个指令相对于内联表达式而言是比较笨拙的
另一个更好的选择是使用ng-cloak指令,它能够在AngularJS结束对内容的处理之前先将其隐藏。ng-cloak指令使用CSS对被应用到的元素进行隐藏,当内容被处理后AngularJS库移除CSS样式,以保证用户永远不会看见内联表达式。你可以按照所需广泛地或者有选择地使用ng-cloak指令,一种通常地做法是对body元素使用该指令,但这意味着当AngularJS处理内容时用户只能看到一个空白的浏览器窗口。我更倾向于更加有选择地使用,将该指令只应用到那些具有内联表达式的文档部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
...
<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>

<div class="well">
<div class="radio" ng-repeat="button in ['None', 'Table', 'List']">
<label ng-cloak>
<input type="radio" ng-model="data.mode" value="{{button}}" ng-checked="$first" /> {{button}}
</label>
</div>
</div>

<div ng-switch on="data.mode" ng-cloak>
<div ng-switch-when="Table">
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos" ng-class="$odd ? 'odd' : 'even'">
<td>{{$index + 1}}</td>
<td ng-repeat="prop in item">{{prop}}</td>
</tr>
</table>
</div>
<div ng-switch-when="List">
<ol>
<li ng-repeat="item in todos">
{{item.action}}
<span ng-if="item.complete"> (Done)</span>
</li>
</ol>
</div>
<div ng-switch-default>
Select another option to display a layout
</div>
</div>
</div>
</body>
...

第 11 章 使用元素和事件指令

准备示例项目

对上一章使用过的directives.html文件做一些修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Directives</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.todos = [
{ action: "Get groceries", complete: false },
{ action: "Call plumber", complete: false },
{ action: "Buy running shoes", complete: true },
{ action: "Buy flowers", complete: false },
{ action: "Call family", complete: false }];
});
</script>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>

<table class="table">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos">
<td>{{$index + 1}}</td>
<td ng-repeat="prop in item">{{prop}}</td>
</tr>
</table>
</div>
</body>

</html>

使用元素指令

元素指令

指令 用作 描述
ng-if 属性 从DOM中添加和移除元素
ng-class 属性、类 为某个元素设置class属性
ng-class-even 属性、类 对有ng-repeat指令生成的偶数元素设置class属性
ng-class-odd 属性、类 对有ng-repeat指令生成的奇数元素设置class属性
ng-hide 属性、类 在DOM中显示和隐藏元素
ng-show 属性、类 在DOM中显示和隐藏元素
ng-style 属性、类 设置一个或多个CSS属性

属性:HTML属性
类:HTML样式类

显示、隐藏和移除元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Directives</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.todos = [
{ action: "Get groceries", complete: false },
{ action: "Call plumber", complete: false },
{ action: "Buy running shoes", complete: true },
{ action: "Buy flowers", complete: false },
{ action: "Call family", complete: false }];
});
</script>
<style>
td>*:first-child {
font-weight: bold
}
</style>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>

<div class="checkbox well">
<label>
<input type="checkbox" ng-model="todos[2].complete" /> Item 3 is complete
</label>
</div>

<table class="table">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos">
<td>{{$index + 1}}</td>
<td>{{item.action}}</td>
<td>
<span ng-hide="item.complete">(Incomplete)</span>
<span ng-show="item.complete">(Done)</span>
</td>
</tr>
</table>
</div>
</body>

</html>

我使用了ng-hide和ng-show指令来控制表格中span元素的可见性
ng-hide和ng-show指令通过添加和移除一个名为ng-hide的CSS类来控制元素的可见性。ng-hide和ng-show之间的区别在于,ng-hide在表达式为true时隐藏元素,而ng-show在表达式为true时显示元素

ng-hide和ng-show指令仍会将所操作的元素保留在DOM中,仅仅只是对用户隐藏。你可以使用ng-if指令从DOM中移除元素而不是隐藏

1
2
3
4
5
6
...
<td>
<span ng-if="!item.complete">(Incomplete)</span>
<span ng-if="item.complete">(Done)</span>
</td>
...

对于ng-if指令来说没有方便的逆指令,因此必须对被绑定的属性进行取反

解决表格的条纹化问题以及与ng-repeat的冲突
ng-hide、ng-show和ng-if指令在应用到组成表格的元素时都有一些问题
首先ng-hide与ng-show的工作方式意味着它们无法容易第在条纹化表格中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos" ng-hide="item.complete">
<td>{{$index + 1}}</td>
<td>{{item.action}}</td>
<td>{{item.complete}}</td>
</tr>
</table>
...

我对table元素使用了Bootstrap的table-striped CSS类以创建条纹效果
AngularJS将处理这些指令,但是因为元素是被隐藏而不是被移除的。所以结果会造成条纹显示的不一致
你可以结合ng-repeat和ng-if指令来解决这个问题

1
2
3
4
5
6
7
...
<tr ng-repeat="item in todos" ng-if="!item.complete">
<td>{{$index + 1}}</td>
<td>{{item.action}}</td>
<td>{{item.complete}}</td>
</tr>
...

或者你也可以使用过滤器

1
2
3
4
5
6
7
...
<tr ng-repeat="item in todos | filter: {complete: 'false'}">
<td>{{$index + 1}}</td>
<td>{{item.action}}</td>
<td>{{item.complete}}</td>
</tr>
...

管理class和CSS
AngularJS提供了一组可用于将元素添加到class中或者设置单个CSS属性的指令:ng-class和ng-css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Directives</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.todos = [
{ action: "Get groceries", complete: false },
{ action: "Call plumber", complete: false },
{ action: "Buy running shoes", complete: true },
{ action: "Buy flowers", complete: false },
{ action: "Call family", complete: false }];

$scope.buttonNames = ["Red", "Green", "Blue"];

$scope.settings = {
Rows: "Red",
Columns: "Green"
};
});
</script>
<style>
tr.Red {
background-color: lightcoral;
}

tr.Green {
background-color: lightgreen;
}

tr.Blue {
background-color: lightblue;
}
</style>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>

<div class="row well">
<div class="col-xs-6" ng-repeat="(key, val) in settings">
<h4>{{key}}</h4>
<div class="radio" ng-repeat="button in buttonNames">
<label>
<input type="radio" ng-model="settings[key]" value="{{button}}">{{button}}
</label>
</div>
</div>
</div>

<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos" ng-class="settings.Rows">
<td>{{$index + 1}}</td>
<td>{{item.action}}</td>
<td ng-style="{'background-color': settings.Columns}">
{{item.complete}}
</td>
</tr>
</table>
</div>
</body>

</html>

本例的核心是settings对象

1
2
3
4
5
6
...
$scope.settings = {
Rows: "Red",
Columns: "Green"
};
...

我将使用Rows属性来设置表格中tr元素的背景色,并使用Columns属性设置Done一列的背景色。为了能够改变这些值,我使用ng-repeat指令创建了两组单选按钮

使用ng-class指令为tr元素设置class

1
2
3
...
<tr ng-repeat="item in todos" ng-class="settings.Rows">
...

使用ng-style指令为td元素设置CSS属性

1
2
3
...
<td ng-style="{'background-color': settings.Columns}">
...

ng-style指令被配置为使用一个对象,该对象的属性为相应的应设置的CSS属性——在这个例子中是background-color属性

设置奇数行和偶数行的CSS类
ng-class指令的另一个变体是由ng-class-odd和ng-class-even指令提供的,在ng-repeat指令中使用这两个指令,并对奇数行或偶数行的元素应用CSS类。这和使用$oddh和$even是类似的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos" ng-class-even="settings.Rows" ng-class-odd="settings.Columns">
<td>{{$index + 1}}</td>
<td>{{item.action}}</td>
<td>
{{item.complete}}
</td>
</tr>
</table>
...

处理事件

事件指令

指令 用作 描述
ng-blur 属性、类 对blur事件指定自定义行为,在失去焦点时触发
ng-change 属性、类 为change事件指定自定义行为,在表单元素的内容状态发生变化时触发
ng-click 属性、类 为click事件指定自定义行为,在用户单击时触发
ng-copy
ng-cut
ng-paste
属性、类 为copy、cut和paste事件指定自定义行为
ng-dblclick 属性、类 为dbclick事件指定自定义行为,在用户双击时触发
ng-focus 属性、类 为focus事件指定自定义行为,在元素获得焦点时触发
ng-keydown
ng-keypress
ng-keyup
属性、类 为keydown、keypress和keyup事件指定自定义行为,在用户按下/释放某个键时被触发
ng-mousedown
ng-mouseenter
ng-mouseleave
ng-mousemove
ng-mouseover
ng-mouseup
属性、类 为mousedown、mouseenter、mouseleave、mousemove、mouseover和mouseup事件指定自定义行为,在用户使用鼠标与元素发生交互时触发
ng-submit 属性、类 为submit事件指定自定义行为,在表单被提交时触发

属性:HTML属性
类:HTML样式类

事件处理器指令可用于直接计算表达式或调用控制器中的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Directives</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.todos = [
{ action: "Get groceries", complete: false },
{ action: "Call plumber", complete: false },
{ action: "Buy running shoes", complete: true },
{ action: "Buy flowers", complete: false },
{ action: "Call family", complete: false }];

$scope.buttonNames = ["Red", "Green", "Blue"];

$scope.data = {
rowColor: "Blue",
columnColor: "Green"
};

$scope.handleEvent = function (e) {
console.log("Event type: " + e.type);
$scope.data.columnColor = e.type == "mouseover" ? "Green" : "Blue";
}
});
</script>
<style>
.Red {
background-color: lightcoral;
}

.Green {
background-color: lightgreen;
}

.Blue {
background-color: lightblue;
}
</style>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>

<div class="well">
<span ng-repeat="button in buttonNames">
<button class="btn btn-info" ng-click="data.rowColor = button">
{{button}}
</button>
</span>
</div>

<table class="table">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos" ng-class="data.rowColor" ng-mouseenter="handleEvent($event)" ng-mouseleave="handleEvent($event)">
<td>{{$index + 1}}</td>
<td>{{item.action}}</td>
<td ng-class="data.columnColor">{{item.complete}}</td>
</tr>
</table>
</div>
</body>

</html>

我对一组由ng-repeat指令产生的按钮元素使用了ng-click指令。当这些按钮中的任意一个被单击时,所指定的内联表达式将会被计算,直接更新数据模型中的值

1
2
3
4
5
...
<button class="btn btn-info" ng-click="data.rowColor = button">
{{button}}
</button>
...

如果你对使用内联表达式感到不习惯,或者如果你需要执行复杂的逻辑,那么你可以在控制中定义一个行为并在事件指令中调用它

1
2
3
...
<tr ng-repeat="item in todos" ng-class="data.rowColor" ng-mouseenter="handleEvent($event)" ng-mouseleave="handleEvent($event)">
...

我对tr元素使用了ng-mouseenter和ng-mouseleave指令,指定了应该调用handleEvent行为。这与JavaScript传统的事件处理方式是类似的,为了访问Event对象,特别使用了$event变量,所有的事件指令都定义了该变量
在行为中处理事件时必须小心,因为AngularJS为指令名所用的事件名称和底层事件的type属性值之间存在不匹配的情况。在本例中我添加了处理ng-mouseenter和ng-mouseleave事件的指令,但是在行为函数中能够收到其他不同的事件

1
2
3
4
5
6
...
$scope.handleEvent = function (e) {
console.log("Event type: " + e.type);
$scope.data.rowColor = e.type == "mouseover" ? "Green" : "Blue";
}
...

找出在行为中将收到哪些事件的最安全的方法是在函数中使用console.log在控制台输出type属性的值

理解AngularJS中的事件
虽然AngularJS提供了一组事件指令,但是你会发现能够创建的事件处理器仍然比通过jQuery所创建的要少。这是因为Web应用程序中的许多事件是在用户改变表单元素的状态时产生的,比如input和select。在AngularJS中你不需要通过事件来响应这些变化,因为你可以使用ng-model指令代替。事件处理器仍然在后台被AngularJS所使用,但是你不需要自己编写和管理它们
有的开发者对在元素上直接使用事件指令感到不适应,特别是当其包含内联表达式时。这种不适应来自两个原因:一是仅仅因为不习惯,另一个则有值得探讨的价值
常见的原因是Web开发者乐于频繁地使用不太引人注意的JavaScript来创建事件处理器,而不是直接在元素上添加代码。这并不是AngularJS所担心的事,它也用了jQuery在背后创建一些不引人注意的处理器。在元素上使用事件指令让人感觉有点奇怪,但是却不会带来那些背后的JavaScript所需要极力避免的维护问题
有探讨价值的事结合指令使用表达式的想法,而不是对控制器行为的依赖。我不喜欢在视图中看到除了最简单的逻辑以外的任何东西,而且更偏爱于使用控制器行为。为了防止滥用表达式,要知道在AngularJS视图中这要少得多,因为大量依赖了ng-repeat这样的指令来生成元素,但使用表达式仍然容易导致创建出不好测试和维护的代码。我的建议事尽量使用指令事件,但是将触发事件时所执行的逻辑放到控制器行为中

创建自定义事件指令
我将创建一个处理touchstart和touchend事件的指令,分别在用户单击和释放可触摸设备的屏幕时触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Directives</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope, $location) {

$scope.message = "Tap Me!";

}).directive("tap", function () {
return function (scope, elem, attrs) {
elem.on("touchstart touchend", function () {
scope.$apply(attrs["tap"]);
});
}
});
</script>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<div class="well" tap="message = 'Tapped!'">
{{message}}
</div>
</div>
</body>

</html>

我使用了module.directive方法创建指令,指令名为tab,返回一个工厂函数,并依次创建一个工人函数来处理指令所应用到的元素。传给工人函数的参数是指令所操作的作用域,指令所应用到的元素的jqLite或jQuery表示形式,以及应用到的元素的属性的集合
我是使用jqLite的on方法为touchstart和touchend事件注册一个处理器函数。我的处理器函数调用scope.$apply方法来计算指令属性值所定义的任何表达式,该属性值是从集合中取到的。我对div元素使用了该指令,就像使用其他那些指令一样,此处的表达式修改了message模型属性

1
2
3
...
<div class="well" tap="message = 'Tapped!'">
...

管理特殊属性

大多数情况下,AngularJS能够巧妙地与HTML进行工作,与标准元素和属性无缝地结合。尽管如此,HTML中某些属性的奇怪的工作方式会导致AngularJS产生某些问题,并需要使用指令解决

管理布尔属性
大多数HTML属性的意义是由赋给属性的值所决定的,但是某些HTML属性仅当元素上存在该属性就可产生效果,不论值是什么。这类属性被称为布尔属性

布尔属性指令

指令 用作 描述
ng-checked 属性 管理checked属性(在input元素上使用)
ng-disabled 属性 管理disabled属性(在input和button元素上使用)
ng-open 属性 管理open属性(在details元素上使用)
ng-readonly 属性 管理readonly属性(在input元素上使用)
ng-selected 属性 管理selected属性(在option元素上使用)

属性:HTML属性
类:HTML样式类

以ng-disabled 指令为例演示如何使用这些指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Directives</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.dataValue = false;
});
</script>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<h3 class="panel-header">To Do List</h3>
<div class="checkbox well">
<label>
<input type="checkbox" ng-model="dataValue"> Set the Data Value
</label>
</div>

<button class="btn btn-success" ng-disabled="dataValue">My Button</button>
</div>
</body>

</html>

管理其他属性
有3个指令常用于对AngularJS无法直接操作的其他属性进行工作

布尔属性指令

指令 用作 描述
ng-href 属性 在a元素上设置href属性
ng-src 属性 在img元素上设置src属性
ng-srcset 属性 在img元素上设置srcset属性。srcset属性是扩展HTML5的草案标准,允许为不同的显示大小和像素密度指定多个图像。我写这篇文章时,浏览器支持有限

在使用ng-href指令时,会在AngularJS处理完元素之前防止用户通过单击链接跳转到错误的目标位置

属性:HTML属性
类:HTML样式类

第 12 章 使用表单

准备示例项目

创建一个名为forms.html的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Forms</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.todos = [
{ action: "Get groceries", complete: false },
{ action: "Call plumber", complete: false },
{ action: "Buy running shoes", complete: true },
{ action: "Buy flowers", complete: false },
{ action: "Call family", complete: false }];
});
</script>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">

<h3 class="panel-header">
To Do List
<span class="label label-info">
{{(todos | filter: {complete: 'false'}).length}}
</span>
</h3>

<table class="table">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos">
<td>{{$index + 1}}</td>
<td>{{item.action}}</td>
<td>{{item.complete}}</td>
</tr>
</table>
</div>
</body>

</html>

对表单元素使用双向数据绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos">
<td>{{$index + 1}}</td>
<td>{{item.action}}</td>
<td>
<input type="checkbox" ng-model="item.complete">
</td>
</tr>
</table>
...

隐式地创建模型属性
前例中,在创建控制器时对显示定义的模型属性进行了操作,但其实也可以通过使用双向数据绑定来隐式地在数据模型中创建属性——当你在使用表单元素收集用户输入数据以便在数据模型中创建一个新的对象或属性时,这是一个非常有用的特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Forms</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.todos = [
{ action: "Get groceries", complete: false },
{ action: "Call plumber", complete: false },
{ action: "Buy running shoes", complete: true },
{ action: "Buy flowers", complete: false },
{ action: "Call family", complete: false }];

$scope.addNewItem = function (newItem) {
$scope.todos.push({
action: newItem.action + " (" + newItem.location + ")",
complete: false
});
};
});
</script>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">

<h3 class="panel-header">
To Do List
<span class="label label-info">
{{ (todos | filter: {complete: 'false'}).length}}
</span>
</h3>

<div class="row">
<div class="col-xs-6">
<div class="well">
<div class="form-group row">
<label for="actionText">Action:</label>
<input id="actionText" class="form-control" ng-model="newTodo.action">
</div>
<div class="form-group row">
<label for="actionLocation">Location:</label>
<select id="actionLocation" class="form-control" ng-model="newTodo.location">
<option>Home</option>
<option>Office</option>
<option>Mall</option>
</select>
</div>
<button class="btn btn-primary btn-block" ng-click="addNewItem(newTodo)">
Add
</button>
</div>
</div>

<div class="col-xs-6">
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Action</th>
<th>Done</th>
</tr>
</thead>
<tr ng-repeat="item in todos">
<td>{{$index + 1}}</td>
<td>{{item.action}}</td>
<td>
<input type="checkbox" ng-model="item.complete">
</td>
</tr>
</table>
</div>
</div>
</div>
</body>

</html>

我们需要关注的是这个input元素

1
2
3
...
<input id="actionText" class="form-control" ng-model="newTodo.action">
...

以及这个select元素

1
2
3
4
5
6
7
...
<select id="actionLocation" class="form-control" ng-model="newTodo.location">
<option>Home</option>
<option>Office</option>
<option>Mall</option>
</select>
...

他们都使用了ng-model指令,用于更新未曾显示定义的模型属性值:newTodo.action和newTodo.location属性。这些属性并不是领域模型的一部分,但在用户单击按钮后调用的addNewItem方法里,却需要使用这些属性来得到用户输入值

1
2
3
4
5
...
<button class="btn btn-primary btn-block" ng-click="addNewItem(newTodo)">
Add
</button>
...
1
2
3
4
5
6
7
8
...
$scope.addNewItem = function (newItem) {
$scope.todos.push({
action: newItem.action + " (" + newItem.location + ")",
complete: false
});
};
...

提示:
也可以在方法中直接使用$scope.newTodo,而不必接收一个对象作为参数,但是接收对象参数的方法能够在视图中被多次复用,这在考虑控制器的继承关系时尤为重要

当页面被浏览器第一次加载时,这个newTodo对象及其action和location属性并不存在。在input元素或select元素改变时,AngularJS将自动创建出newTodo对象,并根据用户正在使用的具体是哪个元素,赋值给该对象的action或location属性。正因为拥有这样的灵活性,AngularJS可以较为自由地展示数据模型的状态。在取用不存在的对象或属性时并不会报错,而且当赋值给还不存在的对象或属性时,AngularJS将会简单创建一个出来——制造出一个所谓的隐式定义属性或对象

提示:
这里虽然使用了newTodo对象来将相关的属性聚合为一组,但是也可以在$scope对象上直接隐式地定义属性

检查所创建的数据模型对象
在访问属性时使用隐式定义的对象有一些好处,例如能够以简洁的方式调用处理数据的方法。但是也有一些缺点,例如如果刷新浏览器,然后在未编辑输入框内容、也未在下拉列表中选择项时,单击Add按钮后,会看到界面没有变化,却能够在JavaScript控制器中看到一个类似这样的错误提示信息

TypeError: Cannot read property ‘action’ of undefined

这个问题是由于控制器中的方法视图访问一个AngularJS尚未创建出的对象的属性所致,而该对象必须等到某个表单控件被修改之后,触发了ng-model指令后才会被创建出来。当程序依赖于隐式定义时,非常重要的一点就是要考虑到你所访问的对象或属性是否有可能还不存在

1
2
3
4
5
6
7
8
9
10
11
...
$scope.addNewItem = function (newItem) {
if (angular.isDefined(newItem) && angular.isDefined(newItem.action)
&& angular.isDefined(newItem.location)) {
$scope.todos.push({
action: newItem.action + " (" + newItem.location + ")",
complete: false
});
}
};
...

校验表单

前面所做的修改放置了JavaScript错误的产生,但是会导致应用在与用户进行交互时既不产生任何输出结果也不报错。虽然解决了JavaScript报错问题,却将用户搞得摸不着头脑
检查数据对象的存在性是非常必要的,因为这是一个普遍存在的问题,但是更深层次的问题在于,这个简单的示例程序中有一个隐含的约束,即在创建一个新的待办事项之前,必须从用户输入中得到anction和location参数。本例在代码中实现了强制约束,但是其实同时也应该告知用户——这便引入的关于表单验证的各种问题

用户愚蠢吗?
Web应用开发者奇怪为什么总能够碰到一些愚蠢的用户——那些在表单中输入无意的数据,并将他们的账户搞得一团糟的用户。确实是有愚蠢的用户的,但是大多数表单数据的问题在于更有危害性的原因:开发者本身。这里总结了为什么用户会输入错误的数据值的四点原因,而这些问题在一定程度上都是可以通过细致的设计和开发来避免的
第一点原因在于用户不理解要求输入的是什么,可能是由于提示不够明确或者仅仅是由于用户没有足够注意。例如,如果要开发一个需要输入信用卡详细信息的应用,可以看看那些错误的请求——其中许多是由于用户在应该输入姓名的地方输入了信用卡账号,反之亦然。用户能够看到两个长长的输入框,按照在其他应用程序中所培养起来的习惯,这两个输入框常常一个用来输入卡号,另一个用来输入姓名。心不在焉的用户虽然看到了提示,却没有花时间阅读每一个输入框上的标签,于是输入了错误的信息。许多表单的填写过程中都会有这样的现象,大部分是你无法控制的,但是相当一部分粗心大意发生在用户已经填写了他们感兴趣的部分,并正要填写你感兴趣的部分的时候
为减少用户的混淆和疏忽,有一些可采取的有效步骤。在整个过程中尽可能早地要求填写那些引起最多错误的信息。例如,在让用户填写冗长的配送地址表单前,先要求填写信用卡详情信息。也可以重新组织表单以减少混淆:例如,让标签语义更清晰些,以及遵循表单元素的惯用顺序
用户输入错误数据的第二点原因是他们并不像提供所要求输入的信息。这种情况下,用户会视图尽快完成表单的填写过程,他们会输入尽可能少的数据以便尽快结束。如果你曾碰到许多email地址为a@a.com的用户,就应该是遇到这类问题了。请想一想为什么用户不愿意提供精确信息——比如,是否因为要求输入了太多的过于私人的信息?
第三点原因:用户不具备所要求输入的信息。我生活在英国,就意味着在要求选择一个美国的州名的时候,我会碰到麻烦。因为英国并没有州,如果将该字段置为必选的,就意味着会收集到错误的数据,或者用户根本不会填完你在引导用户填写的表单。这也是为什么全国公共广播电台(NPR,National Public Radio)从来收不到来自我的捐款的原因,我喜欢《This American Life》节目,但却无法完成捐款过程
最后一个原因最简单:用户输错了。我打字很快,却并不精确,我经常将我的姓Freeman打成Freman,少一个e。除了尽可能减少用户输入的文本数量之外,很少有什么能够有效地处理这种错误的办法。只要有可能,就提供一个选项列表供用户选择。这里不再赘述如何设计表单,但需要说明的是,解决这一问题的最好方法是把重点放在用户的关注点上。当问题出现时,应该试着去观察导致用户出现问题的方式,并找出所需的解决方案。你的用户并不知道你是怎样构建系统的,他们也不关心业务流程是怎样的,他们只想完成这件事情。只关注用户要完成的任务,去除主流程之外的旁枝末节,是每个人都乐意看到的

执行基本的表单验证
AngularJS提供了基本的表单验证方法,实现了对标准HTML元素属性的支持,如type和required等,并增加了一些指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Forms</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.addUser = function (userDetails) {
$scope.message = userDetails.name
+ " (" + userDetails.email + ") (" + userDetails.agreed + ")";
}
$scope.message = "Ready";
});
</script>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<form name="myForm" novalidate ng-submit="addUser(newUser)">
<div class="well">
<div class="form-group">
<label>Name:</label>
<input name="userName" type="text" class="form-control" required ng-model="newUser.name">
</div>
<div class="form-group">
<label>Email:</label>
<input name="userEmail" type="email" class="form-control" required ng-model="newUser.email">
</div>
<div class="checkbox">
<label>
<input name="agreed" type="checkbox" ng-model="newUser.agreed" required> I agree to the terms and conditions
</label>
</div>
<button type="submit" class="btn btn-primary btn-block" ng-disabled="myForm.$invalid">
OK
</button>
</div>
<div class="well">
Message: {{message}}
<div>
Valid: {{myForm.$valid}}
</div>
</div>
</form>
</div>
</body>

</html>

1.增加表单元素
AngularJS对表单校验的支持主要是基于对标准HTML元素(如form和input)进行替换的

提示:
对表单元素使用指令时无需再做任何额外的工作,AngularJS在遇到诸如form、input、select和textarea元素时将自动地应用指令。指令将AngularJS的特性和表单元素无缝地结合在一起,并且还提供了一些额外的属性用于增强开发体验

当AngularJS遇到form元素时,就会自动设置好本章所描述的基本特性,并且向下遍历各个子元素,以便查找是否有其他子元素需要被处理

1
2
3
...
<form name="myForm" novalidate ng-submit="addUser(newUser)">
...

要想获得AngularJS的最佳校验效果,必须为表单设置一些属性。首要的便是name属性,替换表单元素的指令将会定义一些有用的变量,用于表示表单数据的有效性,并且通过name属性的值来访问这些变量值

提示:
如果想使用如双向模型绑定之类的功能时,form元素并不是必需的,form元素只在表单校验时需要用到

正如下一节将要演示的,AngularJS使用标准HTML元素来配置表单校验。这会成为一个问题,因为最新版本的主流浏览器也会使用那些属性,但是对于小范围的元素类型它们的行为并不一致。要禁用浏览器所支持的校验并启用AngularJS校验功能,需要在自己的表单元素上增添novalidate属性,该属性定义于HTML5规范之中,用于告诉浏览器不要自己校验表单,从而允许AngularJS不受干扰地工作
关于表单元素,最后要附加的说明是ng-submit指令,这个指令为表单的提交事件指定了一个自定义的响应行为,将在用户提交表单时触发

2.使用校验属性
下一步是将标准HTML校验属性应用于input元素上

1
2
3
4
5
6
...
<div class="form-group">
<label>Email:</label>
<input name="userEmail" type="email" class="form-control" required ng-model="newUser.email">
</div>
...

正如form元素那样,为各个想要校验的元素添加name属性是非常重要的,这样就可以访问到AngularJS所提供的各种特殊变量
另外需要强调其他几个属性,这些属性告诉AngularJS需要什么样的校验。type属性指定到了input元素将要接收的数据类型,在这个例子中是email类型。HTML5为input元素定义了type属性值的新集合,其中可以被AngularJS所校验的值如下:

input元素的type属性值

属性值 描述
checkbox 创建一个复选框
email 创建一个接收邮件地址作为值的文本输入框(HTML5中新增)
number 创建一个接收数据值作为值的文本输入框(HTML5中新增)
radio 创建一个单选框
text 创建一个接收任何值的标准文本输入框
url 创建一个接收URL作为值的文本输入框(HTML5中新增)

除了type属性所指定的格式之外,还可以通过混合使用html标准属性与AngularJS指令来实现更进一步的约束。在这个例子中使用了required属性,指定用户必须为待校验的表单提供一个输入值。当该属性与type属性值联合使用时,效果相当于告诉AngularJS用户必须提供一个输入值,并且该值必须为email类型

注意:
email和url的校验是格式检查,而不是检查email或url是否存在或在被使用

这里的每一个input元素都使用ng-model指令来设置隐式定义的newUser对象的一个属性,并且由于所有的元素都使用required属性,结果便是只有当用户输入了名字和格式正确的电子邮箱地址,并且勾选了复选框时,表单才是有效的

3.监控表单的有效性
angularjs中用来替换标准表单元素的指令定义了一些特殊变量,可以被用来检查表单中的各个元素或整个表单的有效性状态

表单指令所定义的校验变量

变量 描述
$pristine 如果用户没有与元素/表单产生交互,则返回true
$dirty 如果用户与元素/表单产生过交互,则返回true
$valid 当表单/元素的校验结果为有效时返回true
$invalid 当表单/元素的校验结果为无效时返回true
$error 提供校验错误的详情信息

正如本章稍后将要演示的那样,这些变量可以联合使用,以向用户展示校验错误的反馈信息。以当前例子为例,这里使用了这些特殊变量中的两个。第一处用法是通过内联的数据绑定

1
2
3
4
5
...
<div>
Valid: {{myForm.$valid}}
</div>
...

这个表达式将显示$valid变量的值,以展示整个表单元素的有效性。正如之前所解释的,对于待校验的元素使用name属性是非常重要的,这里可以看到为什么这样做:AngularJS通过与每个元素的name值同名的变量来访问表单指令所定义的校验变量。第二处用到的变量是$invalid

1
2
3
4
5
...
<button type="submit" class="btn btn-primary btn-block" ng-disabled="myForm.$invalid">
OK
</button>
...

如果表单中的任一元素校验结果不为有效时,$invalid属性将会返回true,那么ng-disabled指令所指定的表达式结果也为true,将会使按钮一直被禁用,直到表单校验结果为有效为止

提供表单校验反馈信息

使用CSS提供校验反馈信息
每次用户与被校验元素产生交互时,AngularJS就会检查其状态以查看其是否有效
AngularJS通过在类的集合中添加或移除被校验元素,来报告有效性检查的结果,这一机制可以与CSS联合使用

AngularJS检验中用到的class

变量 描述
ng-pristine 用户未曾交互过的元素被添加到这个类
ng-dirty 用户曾经交互过的元素被添加到这个类
ng-valid 校验结果为有效的元素被添加到这个类
ng-invalid 校验结果为无效的元素被添加到这个类

在每次用户交互之后,AngularJS会从这些类中添加或移除正在被校验的元素,也就是说可以使用这些类来向用户提供按键和单击的即时反馈,无论是整个表单还是单个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Forms</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.addUser = function (userDetails) {
$scope.message = userDetails.name
+ " (" + userDetails.email + ") (" + userDetails.agreed + ")";
}

$scope.message = "Ready";
});
</script>
<style>
form .ng-invalid.ng-dirty {
background-color: lightpink;
}

form .ng-valid.ng-dirty {
background-color: lightgreen;
}

span.summary.ng-invalid {
color: red;
font-weight: bold;
}

span.summary.ng-valid {
color: green;
}
</style>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<form name="myForm" novalidate ng-submit="addUser(newUser)">
<div class="well">
<div class="form-group">
<label>Name:</label>
<input name="userName" type="text" class="form-control" required ng-model="newUser.name">
</div>
<div class="form-group">
<label>Email:</label>
<input name="userEmail" type="email" class="form-control" required ng-model="newUser.email">
</div>
<div class="checkbox">
<label>
<input name="agreed" type="checkbox" ng-model="newUser.agreed" required> I agree to the terms and conditions
</label>
</div>
<button type="submit" class="btn btn-primary btn-block" ng-disabled="myForm.$invalid">OK</button>
</div>
<div class="well">
Message: {{message}}
<div>
Valid:
<span class="summary" ng-class="myForm.$valid ? 'ng-valid' : 'ng-invalid'">
{{myForm.$valid}}
</span>
</div>
</div>
</form>
</div>
</body>

</html>

为特定的校验约束提供反馈信息
除了对校验元素的整体提示信息外,AngularJS也会将元素添加到类中以给出关于应用到该元素的每一个校验的具体信息。所使用的类名是基于相应的元素的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
<style>
form .ng-invalid-required.ng-dirty {
background-color: lightpink;
}

form .ng-invalid-email.ng-dirty {
background-color: lightgoldenrodyellow;
}

form .ng-valid.ng-dirty {
background-color: lightgreen;
}

span.summary.ng-invalid {
color: red;
font-weight: bold;
}

span.summary.ng-valid {
color: green
}
</style>
...

这里将两个校验约束应用到input元素上:使用required属性要求必须输入一个值,使用email要求值必须是邮箱格式

使用特殊变量来提供反馈信息
正如前面所提到的AngularJS为表单验证提供了一系列特殊变量,你可以在视图中使用这些变量来检查单个元素或整个表单的校验状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Forms</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.addUser = function (userDetails) {
$scope.message = userDetails.name
+ " (" + userDetails.email + ") (" + userDetails.agreed + ")";
}

$scope.message = "Ready";
});
</script>
<style>
form .ng-invalid-required.ng-dirty {
background-color: lightpink;
}

form .ng-invalid-email.ng-dirty {
background-color: lightgoldenrodyellow;
}

form .ng-valid.ng-dirty {
background-color: lightgreen;
}

span.summary.ng-invalid {
color: red;
font-weight: bold;
}

span.summary.ng-valid {
color: green;
}

div.error {
color: red;
font-weight: bold;
}
</style>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<form name="myForm" novalidate ng-submit="addUser(newUser)">
<div class="well">
<div class="form-group">
<label>Email:</label>
<input name="userEmail" type="email" class="form-control" required ng-model="newUser.email">
<div class="error" ng-show="myForm.userEmail.$invalid && myForm.userEmail.$dirty">
<span ng-show="myForm.userEmail.$error.email">
Please enter a valid email address
</span>
<span ng-show="myForm.userEmail.$error.required">
Please enter a value
</span>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block" ng-disabled="myForm.$invalid">OK</button>
</div>
<div class="well">
Message: {{message}}
<div>
Valid:
<span class="summary" ng-class="myForm.$valid ? 'ng-valid' : 'ng-invalid'">
{{myForm.$valid}}
</span>
</div>
</div>
</form>
</div>
</body>

</html>

本例中增加了一个新的div元素用于给用户显示校验提示信息。div元素的可见性是受ng-show指令所控制的,将会在input元素被输入值后且输入值未通过校验时显示该元素

提示:
AngularJS的校验具有持续性,意味着一个空的、未和用户发生过交互的input元素如果定义了required属性,将会是无效状态,因为还未输入值。本例中不想在用户开始输入数据前显示错误信息,所以检查了$dirty是否为true,表示只有当用户与元素发生过交互后才会显示错误信息

这里为了访问特殊校验变量,是如何引用input元素的

1
2
3
...
<div class="error" ng-show="myForm.userEmail.$invalid && myForm.userEmail.$dirty">
...

这里通过联合使用form元素的name值和input元素的name值,来访问input元素。这就是之前强调要将name属性应用到被校验元素的原因
在div元素里,为input元素的两条校验约束各自定义了错误提示信息,使用span元素进行包含。通过使用$error变量来控制这些元素的可见性,该变量返回一个对象,该对象的各个属性代表各个校验约束的结果。$error对象所包含的属性对应了应用到某个元素的所有约束

减少反馈元素的数量
前面的例子中巧妙地演示了特殊校验变量和其他指令联合使用以增强用户体验的办法,但是这样有可能使得你的页面标记中产生一大堆存在冗余信息的元素。一种简单的处理办法是将这些信息统一合并到控制器行为中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.addUser = function (userDetails) {
$scope.message = userDetails.name
+ " (" + userDetails.email + ") (" + userDetails.agreed + ")";
}

$scope.message = "Ready";

$scope.getError = function (error) {
if (angular.isDefined(error)) {
if (error.required) {
return "Please enter a value";
} else if (error.email) {
return "Please enter a valid email address";
}
}
}
});
</script>
...

这里定义了一个叫做getError的方法,它可以接收校验元素中的$error对象作为参数,并根据其属性返回一个字符串。$error对象直到校验出问题时才会被定义,所以这里使用了angular.isDefined方法以避免从一个不存在的对象中读取属性。通过数据绑定使用该方法可以简化我们的代码

1
2
3
4
5
6
7
8
9
...
<div class="form-group">
<label>Email:</label>
<input name="userEmail" type="email" class="form-control" required ng-model="newUser.email">
<div class="error" ng-show="myForm.userEmail.$invalid && myForm.userEmail.$dirty">
{{getError(myForm.userEmail.$error)}}
</div>
</div>
...

延迟校验反馈
AngularJS表单校验是非常灵敏的,在每次与用户交互后都会更新每个元素的校验状态。有时这会过于灵敏了,它对用户显示错误信息的方式让人觉得有点过头,特别是与传统的表单校验方式相比,传统方式会直到用户尝试提交表单时才延迟显示错误信息
如果不喜欢AngularJS这样的默认行为,可以在基本特性的基础上实现延迟反馈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Forms</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.addUser = function (userDetails) {
if (myForm.$valid) {
$scope.message = userDetails.name
+ " (" + userDetails.email + ") ("
+ userDetails.agreed + ")";
} else {
$scope.showValidation = true;
}
}

$scope.message = "Ready";

$scope.getError = function (error) {
if (angular.isDefined(error)) {
if (error.required) {
return "Please enter a value";
} else if (error.email) {
return "Please enter a valid email address";
}
}
}
});
</script>
<style>
form.validate .ng-invalid-required.ng-dirty {
background-color: lightpink;
}

form.validate .ng-invalid-email.ng-dirty {
background-color: lightgoldenrodyellow;
}

div.error {
color: red;
font-weight: bold;
}
</style>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<form name="myForm" novalidate ng-submit="addUser(newUser)" ng-class="showValidation ? 'validate' : ''">
<div class="well">
<div class="form-group">
<label>Email:</label>
<input name="userEmail" type="email" class="form-control" required ng-model="newUser.email">
<div class="error" ng-show="showValidation">
{{getError(myForm.userEmail.$error)}}
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">OK</button>
</div>
</form>
</div>
</body>

</html>

这里修改了addUser方法,用于检查整个表单的有效性,并且在应当显示校验反馈信息时,将一个隐式定义的模型属性showValidation设置为true。addUser方法直到表单被提交时才会被调用,也就是说用户可以在输入控件中输入任何值,而不用立即收到校验信息
如果表单被提交后有校验错误,隐式定义的模型属性showValidation就会被设置为true,这将会使校验验证信息显示出来,这是通过控制表单上的一个类样式来实现的,可以通过CSS选择器定位该目标元素。为了简化视图逻辑,就对包含了反馈信息文字的div元素使用了同一个模型属性。现在得到的结果是直到表单第一次被提交时校验反馈信息才会显示给用户,在这之后才变为正常的实时校验反馈

使用表单指令属性

使用input元素
AngularJS使用的指令对input元素提供了一些额外属性,可以用于与数据模型更好地集成,这些属性仅在input元素没有使用type属性,或者type属性为text、url、email和number时适用

适用于input元素的属性

名称 描述
ng-model 用于指定双向绑定的模型
ng-change 用于指定一个表达式,该表达式在元素内容被改变时被计算
ng-minlength 设置一个合法元素的最小字符数
ng-maxlength 设置一个合法元素的最大字符数
ng-pattern 设置一个正则表达式,合法的元素内容必须匹配该正则表达式
ng-required 通过数据绑定设置required属性的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Forms</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.requireValue = true;
$scope.matchPattern = new RegExp("^[a-z]");
});
</script>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<form name="myForm" novalidate>
<div class="well">
<div class="form-group">
<label>Text:</label>
<input name="sample" class="form-control" ng-model="inputValue" ng-required="requireValue" ng-minlength="3" ng-maxlength="10"
ng-pattern="matchPattern">
</div>
</div>

<div class="well">
<p>Required Error: {{myForm.sample.$error.required}}</p>
<p>Min Length Error: {{myForm.sample.$error.minlength}}</p>
<p>Max Length Error: {{myForm.sample.$error.maxlength}}</p>
<p>Pattern Error: {{myForm.sample.$error.pattern}}</p>
<p>Element Valid: {{myForm.sample.$valid}}</p>
</div>
</form>
</div>
</body>

</html>

本例中在校验约束条件中使用了ng-required、ng-minlength、ng-maxlength和ng-pattern属性。这样做的效果是只有当用户输入了值,且该值是以小写字母开头并且长度在3至10个字符时,才是合法的

注意:
当type属性为email、url或number时,AngularJS将会自动设置ng-pattern属性为相应的正则表达式,并检查格式是否匹配

使用复选框

当type属性为checkbox时可适用于input元素的属性

名称 描述
ng-model 用于指定双向绑定的模型
ng-change 用于指定一个表达式,该表达式在元素内容被改变时被计算
ng-true-value 指定当元素被勾选中时所绑定的表达式的值
ng-false-value 指定当元素被取消勾选时所绑定的表达式的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Forms</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) { });
</script>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<form name="myForm" novalidate>
<div class="well">
<div class="checkbox">
<label>
<input name="sample" type="checkbox" ng-model="inputValue" ng-true-value="'Hurrah!'" ng-false-value="'Boo!'"> This is a checkbox
</label>
</div>
</div>

<div class="well">
<p>Model Value: {{inputValue}}</p>
</div>
</form>
</div>
</body>

</html>

ng-true-value和ng-false-value属性的值将被用于设置所绑定的表达式的值,但是只在当复选框的勾选状态被改变时生效。也就是说模型属性不会被自动创建,直到有用户与元素的交互产生时才会被创建

使用文本域

使用选择列表
AngularJS用于select元素的指令包括ng-required和ng-options属性。ng-options属性用于从数组和对象中生成option元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Forms</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.todos = [
{ id: 100, action: "Get groceries", complete: false },
{ id: 200, action: "Call plumber", complete: false },
{ id: 300, action: "Buy running shoes", complete: true }];
});
</script>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<form name="myForm" novalidate>
<div class="well">
<div class="form-group">
<label>Select an Action:</label>
<select ng-model="selectValue" ng-options="item.action for item in todos">
</select>
</div>
</div>

<div class="well">
<p>Selected: {{selectValue || 'None'}}</p>
</div>
</form>
</div>
</body>

</html>

在这个例子中,定义了一个包含三个项目的模型变量todos,每个项目包含三个属性
对于select元素定义的ng-options变量以使得能够从todos中生成出option元素

1
2
3
...
<select ng-model="selectValue" ng-options="item.action for item in todos">
...

这是ng-options表达式的基本形式,形如<标签> for <项目> in <数组>。AngularJS会为数组中的每一个对象生成一个option元素,并且将其值设置到标签中去。对于这个示例将会生成如下的HtML

1
2
3
4
5
6
7
8
...
<select ng-model="selectValue" ng-options="item.action for item in todos" class="ng-pristine ng-untouched ng-valid ng-empty">
<option value="?" selected="selected"></option>
<option label="Get groceries" value="object:3">Get groceries</option>
<option label="Call plumber" value="object:4">Call plumber</option>
<option label="Buy running shoes" value="object:5">Buy running shoes</option>
</select>
...

1.改变第一个选项元素
需要注意的是select元素的输出里包括了一个值为“?”且没有任何内容的option元素。AngularJS在ng-model属性所指定的变量值为undefined时会生成这样的元素。可以通过添加一个空的option元素来替代默认的option元素

1
2
3
4
5
...
<select ng-model="selectValue" ng-options="item.action for item in todos">
<option value="">(Pick One)</option>
</select>
...

这会生成如下HTML

1
2
3
4
5
6
7
8
...
<select ng-model="selectValue" ng-options="item.action for item in todos" class="ng-pristine ng-untouched ng-valid ng-empty">
<option value="" class="" selected="selected">(Pick One)</option>
<option label="Get groceries" value="object:3">Get groceries</option>
<option label="Call plumber" value="object:4">Call plumber</option>
<option label="Buy running shoes" value="object:5">Buy running shoes</option>
</select>
...

2.改变选项值
有时不想总是使用整个源对象来设置ng-model的值,也可以使用一个稍有不同的表达式来为ng-options属性指定对象中的一个属性作为option元素的值

1
2
3
4
5
...
<select ng-model="selectValue" ng-options="item.id as item.action for item in todos">
<option value="">(Pick One)</option>
</select>
...

表达式形如<所选属性> as <标签> for <项目> in <数组>

3.创建选项组元素
ng-options属性可以用来按照某个属性的值将各个选项进行分组,为每个选项值生成一组optgroup元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Forms</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("defaultCtrl", function ($scope) {
$scope.todos = [
{ id: 100, place: "Store", action: "Get groceries", complete: false },
{ id: 200, place: "Home", action: "Call plumber", complete: false },
{ id: 300, place: "Store", action: "Buy running shoes", complete: true }];
});
</script>
</head>

<body>
<div id="todoPanel" class="panel" ng-controller="defaultCtrl">
<form name="myForm" novalidate>
<div class="well">
<div class="form-group">
<label>Select an Action:</label>
<select ng-model="selectValue" ng-options="item.action group by item.place for item in todos">
<option value="">(Pick One)</option>
</select>
</div>
</div>

<div class="well">
<p>Selected: {{selectValue || 'None'}}</p>
</div>
</form>
</div>
</body>

</html>

用于将对象进行分组的属性是通过在ng-options表达式中通过grouy by来进行指定的。在本例中指定了使用place属性进行分组,这将产生如下输出

1
2
3
4
5
6
7
8
9
10
11
12
...
<select ng-model="selectValue" ng-options="item.id as item.action group by item.place for item in todos" class="ng-pristine ng-untouched ng-valid ng-empty">
<option value="" class="" selected="selected">(Pick One)</option>
<optgroup label="Store">
<option label="Get groceries" value="number:100">Get groceries</option>
<option label="Buy running shoes" value="number:300">Buy running shoes</option>
</optgroup>
<optgroup label="Home">
<option label="Call plumber" value="number:200">Call plumber</option>
</optgroup>
</select>
...

提示:
也可以联合使用选项和分组特性,例如:item.id as item.action group by item.place for item in todos

第 13 章 使用控制器和作用域

为什么以及何时使用控制器和作用域

控制器就像领域模型与视图之间的纽带,它给视图提供数据与服务,并且定义了所需的业务逻辑,从而将用户行为转换成模型上的变化
控制器通过作用域向视图提供数据和逻辑,这是前文所描述的数据绑定技术的基础

为什么以及何时使用控制器和作用域

为什么使用 什么时候使用
控制器是模型与视图之间的纽带。控制器使用作用域将模型中的数据公开给视图,并定义根据用户与视图的交互对模型进行更改所需的逻辑 控制器的使用遍布整个AngularJS程序,并为它支持的视图提供作用域

准备示例项目

在angularjs文件夹下创建一个名为controllers.html的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Controllers</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", []);
</script>
</head>

<body>
<div class="well">
Content will go here.
</div>
</body>

</html>

理解基本原理

创建和使用控制器
控制器是通过AngularJS的module对象所提供的controller方法而创建出来的。controller方法的参数是新建控制器的名字和一个将被用于创建控制器的工厂函数。工厂函数可以通过依赖注入特性来声明对AngularJS服务的依赖。几乎每个控制器都需要使用到$scope服务,用于向视图提供作用域,定义可被视图使用的数据和逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Controllers</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("simpleCtrl",function($scope){

});
</script>
</head>

<body>
<div class="well" ng-controller="simpleCtrl">
Content will go here.
</div>
</body>

</html>

你不仅需要创建控制器,还需要区别控制器所支持的视图,这一过程是通过ng-controller指令来完成的。该指令所指定的值必须与创建的控制器同名,在AngularJS的惯例中,经常使用后缀Ctrl来命名控制器,但并不是必需的

设置作用域
当控制器声明了对$scope服务的依赖时,就可以使得控制器通过其对应的作用域向视图提供各种能力。作用域不仅定义了控制器和视图之间的关系,而且对许多重要的AngularJS特性提供了运转机制
有两种方法通过控制器使用作用域。可以定义数据也可以定义行为,也就是说可以在视图的绑定表达式或指令中调用JavaScript函数
创建初始数据和设置行为很简单。只需在传递给控制器工厂函数的$scope对象上创建属性,并为它们分配数据值或函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Controllers</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("simpleCtrl", function ($scope) {

$scope.city = "London";

$scope.getCountry = function (city) {
switch (city) {
case "London":
return "UK";
case "New York":
return "USA";
}
}
});
</script>
</head>

<body>
<div class="well" ng-controller="simpleCtrl">
<p>The city is: {{city}}</p>
<p>The country is: {{getCountry(city) || "Unknown"}}</p>
</div>
</body>

</html>

在控制器的作用域中定义了一个名为city的变量,并将一个字符串值赋给它,同时定义了一个名为getCountry的行为,只是一个简单的函数,能够接收city作为参数并根据city的值返回country。然后通过数据绑定来使用这个变量值以及这个行为,可以通过变量名直接访问任何数据变量,以及通过方法名调用任何行为,就像调用常规的JavaScript桉树一样

向控制器行为中传递参数
在上述示例中创建的getCountry行为,能够接收city作为参数,然后经过处理生成相应的country。这么做也许会令你觉得有点奇怪,因为在数据绑定中是这样调用该行为的

1
2
3
...
<p>The country is: {{getCountry(city) || "Unknown"}}</p>
...

这里传递了作用域里的city属性值作为参数给该行为,而该行为本身也是在作用域中的一部分。于是可以这样重写该行为

1
2
3
4
5
6
7
8
9
10
...
$scope.getCountry = function () {
switch ($scope.city) {
case "London":
return "UK";
case "New York":
return "USA";
}
}
...

将city作为参数传入的原因有两个,一是因为这样意味着我们的行为能够被任何city值所使用,而不是仅仅能够被同一个作用域里定义的那个city值所使用。这在涉及控制器继承时尤为有用。另一个原因是接收参数能够使单元测试变得更简便一些,因为这样该行为就是自包含的

修改作用域
关于作用域最重要的一点是修改会传播下去,自动更新所有相依赖的数据值,即使是通过行为产生的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Controllers</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("simpleCtrl", function ($scope) {

$scope.cities = ["London", "New York", "Paris"];

$scope.getCountry = function (city) {
switch (city) {
case "London":
return "UK";
case "New York":
return "USA";
}
}
});
</script>
</head>

<body ng-controller="simpleCtrl">

<div class="well">
<label>Select a City:</label>
<select ng-options="city for city in cities" ng-model="city">
</select>
</div>

<div class="well">
<p>The city is: {{city}}</p>
<p>The country is: {{getCountry(city) || "Unknown"}}</p>
</div>
</body>

</html>

组织控制器

在程序中组织控制器有许多不同的方法

使用整体控制器
第一种途径是在html元素上使用ng-controller指令,使用控制器
这种方法有一些优点:简单,无需担心各个控制器之间的通信问题。当你使用整体控制器时,实际上你会对整个应用程序创建一个单独的视图

这种方法也有缺点:对于简单程序来说还不错,但是当为了交付程序功能而不断添加其所需的行为时,最终将得到一大堆代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Controllers</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("simpleCtrl", function ($scope) {

$scope.addresses = {};

$scope.setAddress = function (type, zip) {
console.log("Type: " + type + " " + zip);
$scope.addresses[type] = zip;
}

$scope.copyAddress = function () {
$scope.shippingZip = $scope.billingZip;
}
});
</script>
</head>

<body ng-controller="simpleCtrl">

<div class="well">
<h4>Billing Zip Code</h4>
<div class="form-group">
<input class="form-control" ng-model="billingZip">
</div>
<button class="btn btn-primary" ng-click="setAddress('billingZip', billingZip)">
Save Billing
</button>
</div>

<div class="well">
<h4>Shipping Zip Code</h4>
<div class="form-group">
<input class="form-control" ng-model="shippingZip">
</div>
<button class="btn btn-primary" ng-click="copyAddress()">
Use Billing
</button>
<button class="btn btn-primary" ng-click="setAddress('shippingZip', shippingZip)">
Save Shipping
</button>
</div>
</body>

</html>

当你对于AngularJS刚刚入门时,或者在创建一个简单的应用程序时,或是当开始开发时并没有很清晰的设计思路时,这是一种可以用于起步的控制器组织方式。你可以很快起步并上手,在不断推进时也可以采用所介绍的其他组织方式之一

复用控制器
你可以在同一个应用程序中创建多个视图并复用同一个控制器。AngularJS将会调用每个应用到控制器的工厂函数,结果是每个控制器实例将会拥有自己的作用域。这看起来也许会有点奇怪,但是这种方法能够简化控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html ng-app="exampleApp">

<head>
<title>Controllers</title>
<script src="angular.js"></script>
<link href="bootstrap.css" rel="stylesheet" />
<link href="bootstrap-theme.css" rel="stylesheet" />
<script>
angular.module("exampleApp", [])
.controller("simpleCtrl", function ($scope) {
$scope.setAddress = function (type, zip) {
console.log("Type: " + type + " " + zip);
}
$scope.copyAddress = function () {
$scope.shippingZip = $scope.billingZip;
}
});
</script>
</head>

<body>
<div class="well" ng-controller="simpleCtrl">
<h4>Billing Zip Code</h4>
<div class="form-group">
<input class="form-control" ng-model="zip">
</div>
<button class="btn btn-primary" ng-click="setAddress('billingZip', zip)">
Save Billing