├── Chapter1.markdown ├── Chapter2.markdown ├── Chapter3.markdown ├── Chapter4.markdown ├── Chapter5.markdown ├── Chapter6.markdown ├── Chapter7.markdown ├── Chapter8.markdown ├── README.markdown ├── examples ├── Chapter1 │ ├── hello.html │ ├── hello2.html │ └── shopping-cart.html ├── Chapter2 │ ├── aMail │ │ ├── README.markdown │ │ ├── controllers.js │ │ ├── detail.html │ │ ├── figure1.png │ │ ├── figure2.png │ │ ├── index.html │ │ └── list.html │ ├── custom-directive.html │ ├── death-ray.html │ ├── displaying-text.html │ ├── form-input1.html │ ├── form-validation.html │ ├── ng-class.html │ ├── selected-row.html │ ├── show-disabled-menu.html │ ├── startup-controller-reset.html │ ├── startup-controller.html │ ├── startup-controller2.html │ ├── student-list.html │ ├── table-index.html │ ├── talkToServer │ │ ├── README.markdown │ │ ├── figure.png │ │ ├── index.html │ │ └── server.php │ ├── titleCase.html │ ├── use-namespace.html │ ├── useModule.html │ ├── watch1.html │ ├── watch2.html │ └── watch3.html ├── Chapter6 │ ├── hello.html │ ├── hello2.html │ └── helloTemplate.html ├── css │ ├── bootstrap-responsive.css │ ├── bootstrap-responsive.min.css │ ├── bootstrap.css │ ├── bootstrap.min.css │ └── todc-bootstrap.css ├── img │ ├── glyphicons-halflings-white.png │ └── glyphicons-halflings.png └── js │ ├── angular.min.js │ ├── bootstrap.js │ └── bootstrap.min.js └── figure ├── 3-1.png ├── 3-2.png ├── 3-3.png ├── 3-4.png ├── 3-5.png ├── 4-1.png ├── 6-2.png ├── 6-3.png ├── accordion.png ├── angularjs-book.jpg ├── custom-directive.png ├── hello.png ├── hello2.png ├── hello3.png ├── shopping-cart.png ├── signup.png ├── tab.png ├── titleCase.png ├── useModule.png └── watch1.png /Chapter1.markdown: -------------------------------------------------------------------------------- 1 | # 第一章 AngularJS简介 2 | 3 | 我们创造惊人的基于Web的应用程序的能力是令人难以置信的,但是创建这些应用程序时所涉及的复杂性也是让人不可思议的。我们的 Angular 团队希望减轻我们在参与开发 AJAX 应用程序时的痛苦。在 Google,我们曾经在构建像Gmail、Maps 、Calendar 以及其他大型Web应用程序时经历了最痛苦的教训。我想我们也许能够利用这些经验来帮助其他开发人员。 4 | 5 | 我们希望在编写 Web 应用程序时感觉更像是第一次我们编写了几行代码然后站在后面惊讶于它所发生的事情。我们希望编码的过程感觉更像是创造而不是试图满足 Web 浏览器的奇怪的内部运行工作。 6 | 7 | 与此同时,我们还希望我们所面对的工作环境来帮助我们作出设计选择,使应用程序的创建变得很简单并且从一开始就让人们很容易理解它,并且希望伴随着应用程序的不断成长,正确的设计选择会让我们的应用程序易于测试, 扩展和维护。 8 | 9 | 我们试图在 Angular 这个框架中做到这些。我们也为我们已经取得的成果感到非常高兴。这很大程度上归功于 Angular 开源社区中每个成员的出色工作和相互帮助,同时也教会了我们很多东西。我们希望你也加入到我们的社区中来,并帮助我们一起努力让 Angular 变得更好。 10 | 11 | 你可以在我们的 [Github 主页](https://github.com/shyamseshadri/angularjs-book)的仓库中查看那些较大的或者较复杂的例子和代码片段,你也可以拉取分支以及自行研究这些代码。 12 | 13 | ## 目录 14 | 15 | - [概念](#概念) 16 | - [客户端模板](#客户端模板) 17 | - [模型/视图/控制器(MVC)](#模型视图控制器MVC) 18 | - [数据绑定](#数据绑定) 19 | - [依赖注入](#依赖注入) 20 | - [指令](#指令) 21 | - [示例:购物车](#示例购物车) 22 | - [小结](#小结) 23 | 24 | 25 | ## 概念 26 | 27 | 在你将使用的 Angular 构建应用的过程中有几个核心的概念。事实上,任何这些概念并不是我们新发明的。相反,我们从其他开发环境借鉴了大量成功的做法(经验),然后使用包含 HTML ,浏览器以及许多其他 Web 标准的方式实现了它。 28 | 29 | ### 客户端模板 30 | 31 | 多页面的 Web 应用程序都是通过装配和连接服务器上数据来创建 HTML ,然后将构建完成的 HTML 页面发送到浏览器中。大多数的单页应用程序--也就是我们所知道的 AJAX 应用程序--从某种程度上讲,它的这一点一直做的很好。然而 Angular 以不同的方式实现了将模板和数据推送到浏览器中来装配它们。然后服务器角色只是为模板提供静态资源以及为模板适当地提供数据。 32 | 33 | 让我们来看一个例子,在 Angular 中如何在浏览器中组装模板和数据。按照惯例我们使用一个 Hello,World 的例子,但是注意这里并不是编写一个 "Hello,World" 的单一字符串,而是将问候 "Hello" 作为我们稍候可能会改变的数据来构建。 34 | 35 | 针对这个例子,我们在 `hello.html` 中来创建我们的模板: 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 |

{{greeting.text}}, World

45 |
46 | 47 | 48 | 49 | 接下来我们将逻辑编写在 `controllers.js` 中: 50 | 51 | function HelloController($scope){ 52 | $scope.greeting = {text: 'Hello'}; 53 | } 54 | 55 | 最后将我们 `hello.html` 载入任意浏览器中,我们将看到如图1-1所示的信息: 56 | 57 | ![Hello](figure/hello.png) 58 | 59 | 图1-1 Hello World 60 | 61 | > 译注:你也可以自行修改 `controllers.js` 中的数据来查看效果。 62 | 63 | 与现在我们使用广泛的大多数方法相比,这里有一些有趣的事情需要注意: 64 | 65 | + HTML 并没有类(class 属性)或者 IDs 来标识在哪里添加事件监听器。 66 | 67 | + 当 `HelloController` 设置 `greeting.text` 为 `Hello` 时,我们并没有注册任何事件监听器或者编写任何回调函数。 68 | 69 | + `HelloController` 只是一个很普通的 JavaScript 类,并且它并没有继承任何 Angular 所提供的信息。 70 | 71 | + `HelloController` 获取了它所需要的 `$scope` 对象,我们无需创建它。 72 | 73 | + 我们并没有自己手动的调用 `HelloController` 的构造函数,在这里暂时也不打算弄清楚什么时候调用它。 74 | 75 | 接下来我们会看到更多与传统开发方式之间的差异,但是现在我们应该清楚:Angular 应用程序的结构与过去类似的应用程序是完全不同的。 76 | 77 | 那么为什么我们会做出这些设计选择以及 Angular 是如何工作的呢?接下来先让我们来看看 Angular 从其他地方借鉴的一些好的思想(概念/经验)。 78 | 79 | ### 模型/视图/控制器(MVC) 80 | 81 | MVC 应用程序结构是20世纪70年代作为 Smalltalk 的一部分引入的。从 Smalltalk 开始,MVC 在几乎每一个涉及用户界面的桌面应用程序开发环境中都变得流行起来。无论你是使用 C++ ,Java ,还是 Object-C ,都可以找到使用 MVC 的场景。然而,直到最近,MVC 的思想才开始在国外的 Web 开发中应用。 82 | 83 | MVC 背后的核心思想是你可以在你的代码中清晰的分离数据管理(模型),应用程序逻辑(控制器)以及给用户呈现数据(视图)。 84 | 85 | 视图会从模型中获取数据来显示给用户。当用户通过点击或者输入操作与应用程序进行交互时,控制器就通过修改数据模型来响应用户的操作。最后,被修改的模型会通知视图它已经发生了变化,因此视图可以更新它所显示的信息。 86 | 87 | 在 Angular 应用程序中,视图就是文档对象模型 (DOM) ,控制器是 JavaScript 类,最后模型中的数据便是存储在对象中的属性(属性值)。 88 | 89 | > JavaScript 并没有类的概念,这里的意思就是用构造函数的方式来处理控制器部分,其他地方所提及的 JavaScript 类的概念读者需要自行甄别。 90 | 91 | 我们认为 MVC 的灵活性主要主要表现在以下几方面。首先,它给你提供了一个只能的模型用于告诉在哪里存储什么样的数据,因此你不需要每次都重新构造它。当其他人参与到你的项目中合作开发时,便能够即时理解你已经编写好的部分,因为他们会知道你使用了 MVC 结构来组织你的代码。也许最重要的是,它给你提供了一个极大的好处,是你的程序更易于扩展,维护和测试。 92 | 93 | **译注** 94 | 95 | 1. MVC 是软件工程中的一种软件架构模式 - [MVC](http://zh.wikipedia.org/wiki/MVC)。 96 | 2. Smalltalk 是一门面向对象的程序设计语言 - [Smalltalk](http://zh.wikipedia.org/wiki/Smalltalk)。 97 | 98 | ### 数据绑定 99 | 100 | 之前常见的 AJAX 单页应用程序,像 Rails ,PHP 或者 JSP 平台都是在通过在将页面发送给用户显示之前将数据合并到 HTML 字符串中来帮助我们创建用户界面 (UI)。 101 | 102 | 像 jQuery 这样的库也是将模型扩展到客户端并让我们使用类似的风格,但是它只有单独更新部分 DOM 的能力,而不能更新整个页面。在 AngularJS 中,我们将数据合并到 HTML 模板的字符串中,然后通过在一个占位元素中设置 `innerHTML` 将返回的结果插入到我们的目标 DOM 元素中。 103 | 104 | 这一切都工作得很好,但是当你希望插入新的数据到用户界面 (UI) 中时,或者基于用户的输入改变数据,你需要做一些相当不平凡的工作以确保你的数据变为正确的状态,无论数据是在用户界面中 (UI) 还是 JavaScript 属性中。 105 | 106 | 但是如果我们可以不用编写代码就能处理好所有这些工作会怎样呢?如果我们可以只需声明用户界面的哪些部分对应哪些 JavaScript 属性并且让它们自动同步又会怎样呢?这种编程风格被称为数据绑定。由于 MVC 可以在我们编写视图和模型时减少代码,因此我们将它引入到了 Angular 中。大部分将数据从一处迁移到另一处的工作都会自动完成。 107 | 108 | 下面来看看这一行为,我们继续使用前面的 "Hello World" 的例子,但是我们会让它"动"起来。原来是一旦 HelloController 设置了其模型 `greeting.text` 的值,它便不再会改变。首先让我们通过添加一个根据用户输入改变 `greeting.text` 值的文本输入框来改变这个例子,让它能够"动"起来。 109 | 110 | 下面是新的模板: 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 |
119 | 120 |

{{greeting.text}}, World

121 |
122 | 123 | 124 | 125 | `HelloController` 控制器可以保持不变。 126 | 127 | 将它载入到浏览器中,我们将看到如图1-2所示屏幕截图: 128 | 129 | ![Hello with data binding](figure/hello2.png) 130 | 131 | 图1-2 应用程序的默认状态 132 | 133 | 如果我们使用 'Hi' 文本替换输入框中的 'Hello' ,我们将就会看到如图1-3所示截图: 134 | 135 | ![Hi](figure/hello3.png) 136 | 137 | 图1-3 改变文本框值之后的应用程序 138 | 139 | 我们并没有在输入字段上注册一个改变值的事件监听器,我们有一个将会自动更新的 UI。同样的情况也适用于来自服务器端的改变。在我们的控制器中,我们可以构造一个服务器端的请求,获取响应,然后设置 `$scope.greeting.text` 等于它返回的值。Angular 会自动更新文本输入框和双大括号中的 text 字段为该返回值。 140 | 141 | ### 依赖注入 142 | 143 | 之前我们提到过,在 `HelloController` 中有很多东西都可以重复,在这里我们并没有编写。例如,`$scope` 对象会将数据绑定自动传递给我们;我们不需要通过调用任何函数来创建它。我们只是通过将它放置在 `HelloController` 的构造器中来请求它。 144 | 145 | 正如我们将在后面的章节中会看到,`$scope` 并不是我们唯一可以访问的东西。如果我们希望将数据绑定到用户浏览器的 URL 地址中,我们可以通过将数据绑定植入我们控制器的 `$location` 中来访问管理数据。就像这样: 146 | 147 | function HelloController($scope, $location){ 148 | $scope.greeting = {text: 'Hello'}; 149 | //use $location for something good here... 150 | } 151 | 152 | 这个神奇的效果是通过 Angular 的依赖注入系统实现的。依赖注入让我们遵循这种开发风格,而不是创建依赖,我们的类只需要知道它需要什么。 153 | 154 | 这个效果遵循一个被称为得墨忒耳定律的设计模式,也被称作最少知识原则。由于我们的 `HelloController` 的工作只是设置 greeting 模型的初试状态,这种模式会告诉你无需担心其他的事情,例如 `$scope` 是如何创建的,或者在哪里可以找到它。 155 | 156 | 这个特性并不只是通过 Angular 框架创建的对象才有。你最终创建的任何对象和服务也可以以同样的方式注入。 157 | 158 | ### 指令 159 | 160 | Angular 最好的部分之一就是你可以编写你的模板如同 HTML 一样。之所以可以这样做,是因为在这个框架的核心部分我们已经包含了一个强大的 DOM 解析引擎,它允许你扩展 HTML 的语法。 161 | 162 | 我们已经在我们的模板中看到了一些不属于 HTML 规范的新属性。例如包含在双花括号中的数据绑定,用于指定哪个控制器对应哪部分视图的 `ng-controller` ,以及将输入框绑定到模型部分的 `ng-model` 。我们称之为 HTML 扩展指令。 163 | 164 | Angular 自带了许多指令以帮助你定义应用程序的视图。后面我们就会看到更多的指令。这些指令可以定义用来帮助我们定义常见的视图作为模板。它们可以用于声明设置你的应用程序如何工作或者创建可复用的组件。 165 | 166 | 你并不仅限于使用 Angular 自带的指令。你也可以编写你自己的扩展 HTML 模板来做你想做的任何事。 167 | 168 | ## 示例:购物车 169 | 170 | 接下来让我们来看一个较大的例子,它展示了更多的 Angular 的能力。想象一下,我们要创建一个购物应用程序。在应用程序的某个地方,我们需要展示用户的购物车并允许他编辑。接下来我们直接跳到那部分。 171 | 172 | 173 | 174 | Your Shopping Cart 175 | 176 | 177 |

Your Order

178 |
179 | {{item.title}} 180 | 181 | {{item.price | currency}} 182 | {{item.price * item.quantity | currency}} 183 | 184 |
185 | 186 | 199 | 200 | 201 | 202 | 最终用户界面截屏如图1-4所示: 203 | 204 | ![shopping-cart](figure/shopping-cart.png) 205 | 206 | 图1-4 购物车用户界面 207 | 208 | 下面是关于这里发生了什么的简短参考。本书其余的部分提供了更深入的讲解。 209 | 210 | 让我们从顶部开始: 211 | 212 | 213 | 214 | `ng-app` 属性告诉 Angular 它应该管理页面的哪一部分。由于我们把它放在 `` 元素中,这就会告诉 Angular 我们希望它管理整个页面。这往往也是你所希望的,但是如果你是在现有应用程序中集成 Angular 并使用其他方式管理页面,那么你可能希望把它放在应用程序中的某个 `
` 元素中。 215 | 216 | 217 | 218 | 在 Angular 中,你用于管理页面某个区域的 JavaScript 类被称为控制器。通过在 `body` 标签中包含一个控制器,那么说明我的这个 `CartController` 将会管理 `` 和 `` 之间的所有东西。 219 | 220 |
221 | 222 | `ng-repeat` 的意思就是给被称为 `items` 数组中的每个元素都复制一次当前 `
` 里面的 DOM 结构。在每一个复制的 div 副本中,我们都会给当前元素设置一个名为 `item` 的属性,这样我们就可以在模板中使用它。你可以看到,结果返回的三个 `
` 中的每一个都包含产品的标题,数量,单价,总价以及一个用于移除当前条目的按钮。 223 | 224 | {{item.title}} 225 | 226 | 227 | 正如我们在 "Hello, World" 例子中所示,数据绑定通过 `{{ }}` 让我们将一个变量的值插入到页面某部分中并保持数据同步。完整的表达式 `{{item.title}}` 会以迭代的方式检索当前 item ,然后将该 item 的 title 属性的内容插入到 DOM 中。 228 | 229 | 230 | 231 | `ng-model` 在输入字段和 `item.quantity` 的值之间定义并创建了数据绑定行为。 232 | 233 | `` 中的 `{{ }}` 设置了一个单项关联,它的意思就是"在这里插入一个值"。我们就是想要这个效果,但是应用程序也需要知道用户什么时候改变了商品数量,以便它可以改变选购商品的总价。 234 | 235 | 因此我们通过使用 `ng-model` 来同步模型中的变化。`ng-model` 声明了将 `item.quantity` 的值插入到文本域中,每当用户输入一个新的值时它也会自动更新 `item.quantity` 的值。 236 | 237 | {{item.price | currency}} 238 | {{item.price * item.quantity | currency}} 239 | 240 | 241 | 我们还希望单价和总价被格式化为美元形式。Angular 自带了一个被称为过滤器的特性来让我们转换文本,在这里我们捆绑了一个被称为 `currency` 的过滤器用于给我们处理这里的美元格式操作。在下一章我们将会看到更多的过滤器。 242 | 243 | 244 | 245 | 这允许用户通过点击产品旁边的 `remove` 按钮来从他们的购物车中移除所选择的商品条目。在这里我们设置它点击这个按钮时调用 `remove()` 函数。我们还给它传递了一个 `$index` 参数,这个参数包含了它在 `ng-repeat` 中的索引值,这样我们就会知道哪一项将会被移除。 246 | 247 | function CartController($scope) 248 | 249 | 这个 `CartContoller` 用于整个管理购物车应用的逻辑。这会告诉 Angular ,在这里它要给控制器传递一个叫做 `$scope` 的参数。这个 `$scope` 用于帮助我们在用户界面中将数据绑定到元素中。 250 | 251 | $scope.items = [ 252 | {title: 'Paint pots', quantity: 8, price: 3.95}, 253 | {title: 'Polka dots', quantity: 17, price: 12.95}, 254 | {title: 'Pebbles', quantity: 5, price: 6.95} 255 | ]; 256 | 257 | 通过定义 `$scope.items` ,我创建一个虚拟数据哈希表[数组]来表示用户的购物车。我们希望它可以用于 UI 中的数据绑定,因此将它添加到 `$scope` 中。 258 | 259 | 当然,真正的购物车应用不可能只是在内存中工作,它需要访问服务器中正确存储的数据。我们将在后面的章节中讨论这些。 260 | 261 | $scope.remove = function(index){ 262 | $scope.items.splice(index, 1); 263 | } 264 | 265 | 我们还希望将 `remove()` 函绑定在 UI 中使用,因此我们也将它添加到 `$scope` 中。对于这个内存版本的购物车,`remove()` 函数可以即时从数组中删除项目。由于 `
` 列表是通过 `ng-repeat` 创建的数据绑定,所以当项目消失时列表会自动收缩。记住,每当用户在 UI 界面上点击一个 Remove 按钮时,`remove()` 函数就会被调用。 266 | 267 | ##小结 268 | 269 | 我们已经看到了 Angular 最基本的用法以及一些非常简单的例子。本书后面的部分将专注于介绍这个框架所提供的更多功能。 270 | -------------------------------------------------------------------------------- /Chapter4.markdown: -------------------------------------------------------------------------------- 1 | #分析一个AngularJS应用程序 2 | 3 | 在第2章中, 我们已经讨论了一些AngularJS常用的功能, 然后在第3章讨论了该如何结构化开发应用程序. 现在, 我们不再继续深单个的技术点, 第4章将着眼于一个小的, 实际的应用程序进行讲解. 我们将从一个实际有效的应用程序中感受一下我们之前已经讨论过的(示例)所有的部分. 4 | 5 | 我们将每次介绍一部分, 然后讨论其中有趣和关联的部分, 而不是讨论完整应用程序的前端和核心, 最后在本章的后面我们会慢慢简历这个完整的应用程序. 6 | 7 | ## 目录 8 | 9 | - [应用程序](#应用程序) 10 | - [模型, 控制器和模板之间的关系](#模型-控制器和模板之间的关系) 11 | - [模型](#模型) 12 | - [控制器, 指令和服务](#控制器-指令和服务) 13 | - [服务](#服务) 14 | - [指令](#指令) 15 | - [控制器](#控制器) 16 | - [模板](#模板) 17 | - [测试](#测试) 18 | - [单元测试](#单元测试) 19 | - [脚本测试](#脚本测试) 20 | 21 | ##应用程序 22 | 23 | Guthub是一个简单的食谱管理应用, 我们设计它用于存储我们超级美味的食谱, 同时展示AngularJS应用程序的各个不同的部分. 这个应用程序包含以下内容: 24 | 25 | + 一个两栏的布局 26 | + 在左侧有一个导航栏 27 | + 允许你创建新的食谱 28 | + 允许你浏览现有的食谱列表 29 | 30 | 主视图在左侧, 其变化依赖于URL, 或者食谱列表, 或者单个食谱的详情, 或者可添加新食谱的编辑功能, 或者编辑现有的食谱. 我们可以在图4-1中看到这个应用的一个截图: 31 | 32 | ![Guthub](figure/4-1.png) 33 | 34 | Figure 4-1. Guthub: A simple recipe management application 35 | 36 | 这个完整的应用程序可以在我们的Github中的`chapter4/guthub`中得到. 37 | 38 | ##模型, 控制器和模板之间的关系 39 | 40 | 在我们深入应用程序之前, 让我们来花一两段文字来讨论以下如何将标题中的者三部分在应用程序中组织在一起工作, 同时来思考一下其中的每一部分. 41 | 42 | `model`(模型)是真理. 只需要重复这句话几次. 整个应用程序显示什么, 如何显示在视图中, 保存什么, 等等一切都会受模型的影响. 因此你要额外花一些时间来思考你的模型, 对象的属性将是什么, 以及你打算如何从服务器获取并保存它. 视图将通过数据绑定的方式自动更新, 所以我们的焦点应该集中在模型上. 43 | 44 | `controller`保存业务逻辑: 如何获取模型, 执行什么样的操作, 视图需要从模型中获取什么样的信息, 以及如何将模型转换为你所想要的. 验证职责, 使用调用服务器, 引导你的视图使用正确的数据, 大多数情况下所有的这些事情都属于控制器来处理. 45 | 46 | 最后, `template`代表你的模型将如何显示, 以及用户将如何与你的应用程序交互. 它主要约束以下几点: 47 | 48 | + 显示模型 49 | + 定义用户可以与你的应用程序交互的方式(点击, 文本输入等等) 50 | + 应用程序的样式, 并确定何时以及如何显示一些元素(显示或隐藏, hover等等) 51 | + 过滤和格式化数据(包括输入和输出) 52 | 53 | 要意识到在Angular中的模型-视图-控制器涉及模式中模板并不是必要的部分. 相关, 视图是模板获取执行被编译后的版本. 它是一个模板和模型的组合. 54 | 55 | 任何类型的业务逻辑和行为都不应该进入模板中; 这些信息应该被限制在控制器中. 保持模板的简单可以适当的分离关注点, 并且可以确保你只使用单元测试的情况下就能够测试大多数的代码. 而模板必须使用场景测试的方式来测试. 56 | 57 | 但是, 你可能会问, 在哪里操作DOM呢? DOM操作并不会真正进入到控制器和模板中. 它会存在于Angular的指令中(有时候也可以通过服务来处理, 这样可以避免重复的DOM操作代码). 我们会在我们的GutHub的示例文件中涵盖一个这样的例子. 58 | 59 | 废话少说, 让我们来深入探讨一下它们. 60 | 61 | ##模型 62 | 63 | 对于应用程序我们要保持模型非常简单. 这一有一个菜谱. 在整个完整的应用程序中, 它们是一个唯一的模型. 它是构建一切的基础. 64 | 65 | 每个菜谱都有下面的属性: 66 | 67 | + 一个用于保存到服务器的ID 68 | + 一个名称 69 | + 一个简短的描述 70 | + 一个烹饪说明 71 | + 是否是一个特色的菜谱 72 | + 一个成份数组, 每个成分的数量, 单位和名称 73 | 74 | 就是这样. 非常简单. 应用程序的中一切都基于这个简单的模型. 下面是一个让你食用的示例菜谱(如图4-1一样): 75 | ```js 76 | { 77 | 'id': '1', 78 | 'title': 'Cookies', 79 | 'description': 'Delicious. crisp on the outside, chewy' + 80 | ' on the outside, oozing with chocolatey goodness' + 81 | ' cookies. The best kind', 82 | 'ingredients': [ 83 | { 84 | 'amount': '1', 85 | 'amountUnits': 'packet', 86 | 'ingredientName': 'Chips Ahoy' 87 | } 88 | ], 89 | 'instructions': '1. Go buy a packet of Chips Ahoy\n'+ 90 | '2. Heat it up in an oven\n' + 91 | '3. Enjoy warm cookies\n' + 92 | '4. Learn how to bake cookies from somewhere else' 93 | } 94 | ``` 95 | 下面我们将会看到如何基于这个简单的模型构建更复杂的UI特性. 96 | 97 | ##控制器, 指令和服务 98 | 99 | 现在我们终于可以得到这个让我们牙齿都咬到肉里面去的美食应用程序了. 首先, 我们来看看代码中的指令和服务, 以及讨论以下它们都是做什么的, 然后我们我们会看看这个应用程序需要的多个控制器. 100 | 101 | ###服务 102 | ```js 103 | //this file is app/scripts/services/services.js 104 | 105 | var services = angular.module('guthub.services', ['ngResource']); 106 | 107 | services.factory('Recipe', ['$resource', function(){ 108 | return $resource('/recipes/:id', {id: '@id'}); 109 | }]); 110 | 111 | services.factory('MultiRecipeLoader', ['Recipe', '$q', function(Recipe, q){ 112 | return function(){ 113 | var delay = $q.defer(); 114 | Recipe.query(function(recipes){ 115 | delay.resolve(recipes); 116 | }, function(){ 117 | delay.reject('Unable to fetch recipes'); 118 | }); 119 | return delay.promise; 120 | }; 121 | }]); 122 | 123 | services.factory('RecipeLoader', ['Recipe', '$route', '$q', function(Recipe, $route, $q){ 124 | return function(){ 125 | var delay = $q.defer(); 126 | Recipe.get({id: $route.current.params.recipeId}, function(recipe){ 127 | delay.resolve(recipe); 128 | }, function(){ 129 | delay.reject('Unable to fetch recipe' + $route.current.params.recipeId); 130 | }); 131 | return delay.promise; 132 | }; 133 | }]); 134 | ``` 135 | 首先让我们来看看我们的服务. 在33页的"使用模块组织依赖"小节中已经涉及到了服务相关的知识. 这里, 我们将会更深一点挖掘服务相关的信息. 136 | 137 | 在这个文件中, 我们实例化了三个AngularJS服务. 138 | 139 | 有一个菜谱服务, 它返回我们所调用的Angular Resource. 这些是RESTful资源, 它指向一个RESTful服务器. Angular Resource封装了低层的`$http`服务, 因此你可以在你的代码中只处理对象. 140 | 141 | 注意单独的那行代码 - `return $resource` - (当然, 依赖于`guthub.services`模型), 现在我们可以将`recipe`作为参数传递给任意的控制器中, 它将会注入到控制器中. 此外, 每个菜谱对象都内置的有以下几个方法: 142 | 143 | + Recipe.get() 144 | + Recipe.save() 145 | + Recipe.query() 146 | + Recipe.remove() 147 | + Recipe.delete() 148 | 149 | > 如果你使用了`Recipe.delete`方法, 并且希望你的应用程序工作在IE中, 你应该像这样调用它: `Recipe[delete]()`. 这是因为在IE中`delete`是一个关键字. 150 | 151 | 对于上面的方法, 所有的查询众多都在一个单独的菜谱中进行; 默认情况下`query()`返回一个菜谱数组. 152 | 153 | `return $resource`这行代码用于声明资源 - 也给了我们一些好东西: 154 | 155 | 1. 注意: URL中的id是指定的RESTFul资源. 它基本上是说, 当你进行任何查询时(`Recipe.get()`), 如果你给它传递一个id字段, 那么这个字段的值将被添加早URL的尾部. 156 | 157 | 也就是说, 调用`Recipe.get{id: 15})将会请求/recipe/15. 158 | 159 | 2. 那第二个对象是什么呢? {id: @id}吗? 是的, 正如他们所说的, 一行代码可能需要一千行解释, 那么让我们举一个简单的例子. 160 | 161 | 比方说我们有一个recipe对象, 其中存储了必要的信息, 并且包含一个id. 162 | 163 | 然后, 我们只需要像下面这样做就可以保存它: 164 | ```js 165 | //Assuming existingRecipeObj has all the necessary fields, 166 | //including id(say 13) 167 | var recipe = new Recipe(existingRecipeObj); 168 | recipe.$save(); 169 | ``` 170 | 这将会触发一个POST请求到`/recipe/13`. 171 | 172 | `@id`用于告诉它, 这里的id字段取自它的对象中同时用于作为id参数. 这是一个附加的便利操作, 可以节省几行代码. 173 | 174 | 在`apps/scripts/services/services.js`中有两个其他的服务. 它们两个都是加载器(Loaders); 一个用于加载单独的食谱(RecipeLoader), 另一个用于加载所有的食谱(MultiRecipeLoader). 这在我们连接到我们的路由时使用. 在核心上, 它们两个表现得非常相似. 这两个服务如下: 175 | 176 | 1. 创建一个`$q`延迟(deferred)对象(它们是AngularJS的promises, 用于链接异步函数). 177 | 2. 创建一个服务器调用. 178 | 3. 在服务器返回值时resolve延迟对象. 179 | 4. 通过使用AngularJS的路由机制返回promise. 180 | 181 | > **AngularJS中的Promises** 182 | > 183 | > 一个promise就是一个在未来某个时刻处理返回对象或者填充它的接口(基本上都是异步行为). 从核心上讲, 一个promise就是一个带有`then()`函数(方法)的对象. 184 | > 185 | >让我们使用一个例子来展示它的优势, 假设我们需要获取一个用户的当前配置: 186 | 187 | ```js 188 | var currentProfile = null; 189 | var username = 'something'; 190 | 191 | fetchServerConfig(function(){ 192 | fetchUserProfiles(serverConfig.USER_PROFILES, username, 193 | function(profiles){ 194 | currentProfile = profiles.currentProfile; 195 | }); 196 | }); 197 | ``` 198 | > 对于这种做法这里有一些问题: 199 | > 200 | > 1. 对于最后产生的代码, 缩进是一个噩梦, 特别是如果你要链接多个调用时. 201 | > 202 | > 2. 在回调和函数之间错误报告的功能有丢失的倾向, 除非你在每一步中处理它们. 203 | > 204 | > 3. 对于你想使用`currentProfile`做什么, 你必须在内层回调中封装其逻辑, 无论是直接的方式还是使用一个单独分离的函数. 205 | > 206 | > Promises解决了这些问题. 在我们进入它是如何解决这些问题之前, 先让我们来看看一个使用promise对同一问题的实现. 207 | 208 | ```js 209 | var currentProfile = fetchServerConfig().then(function(serverConfig){ 210 | return fetchUserProfiles(serverConfig.USER_PROFILES, username); 211 | }).then(function{ 212 | return profiles.currentProfile; 213 | }, function(error){ 214 | // Handle errors in either fetchServerConfig or 215 | // fetchUserProfile here 216 | }); 217 | ``` 218 | > 注意其优势: 219 | > 220 | > 1. 你可以链接函数调用, 因此你不会产生缩进带来的噩梦. 221 | > 222 | > 2. 你可以放心前一个函数调用会在下一个函数调用之前完成. 223 | > 224 | > 3. 每一个`then()`调用都要两个参数(这两个参数都是函数). 第一个参数是成功的操作的回调函数, 第二个参数是错误处理的函数. 225 | > 4. 在链接中发生错误的情况下, 错误信息会通过错误处理器传播到应用程序的其他部分. 因此, 任何回调函数的错误都可以在尾部被处理. 226 | > 227 | > 你会问, 那什么是`resolve`和`reject`呢? 是的, `deferred`在AngularJS中是一种创建promises的方式. 调用`resolve`来满足promise(调用成功时的处理函数), 同时调用`reject`来处理promise在调用错误处理器时的事情. 228 | 229 | 当我们链接到路由时, 我们会再次回到这里. 230 | 231 | ###指令 232 | 233 | 我们现在可以转移到即将用在我们应用程序的指令上来. 在这个应用程序中将有两个指令: 234 | 235 | `butterbar` 236 | 237 | 这个指令将在路由发生改变并且页面仍然还在加载信息时处理显示和隐藏任务. 它将连接路由变化机制, 基于页面的状态来自动控制显示或者隐藏是否使用哪个标签. 238 | 239 | `focus` 240 | 241 | 这个`focus`指令用于确保指定的文本域(或者元素)拥有焦点. 242 | 243 | 让我们来看一下代码: 244 | ```js 245 | // This file is app/scripts/directives/directives.js 246 | 247 | var directives = angular.module('guthub.directives', []); 248 | 249 | directives.directive('butterbar', ['$rootScope', function($rootScope){ 250 | return { 251 | link: function(scope, element attrs){ 252 | element.addClass('hide'); 253 | 254 | $rootScope.$on('$routeChangeStart', function(){ 255 | element.removeClass('hide'); 256 | }); 257 | 258 | $rootScope.$on('$routeChangeSuccess', function(){ 259 | element.addClass('hide'); 260 | }); 261 | } 262 | }; 263 | }]); 264 | 265 | directives.dirctive('focus',function(){ 266 | return { 267 | link: function(scope, element, attrs){ 268 | element[0].focus(); 269 | } 270 | }; 271 | }); 272 | ``` 273 | 上面所述的指令返回一个对象带有一个单一的属性, link. 我们将在第六章深入讨论你可以如何创建你自己的指令, 但是现在, 你应该知道下面的所有事情: 274 | 275 | 1. 指令通过两个步骤处理. 在第一步中(编译阶段), 所有的指令都被附加到一个被查找到的DOM元素上, 然后处理它. 任何DOM操作否发生在编译阶段(步骤中). 在这个阶段结束时, 生成一个连接函数. 276 | 277 | 2. 在第二步中, 连接阶段(我们之前使用的阶段), 生成前面的DOM模板并连接到作用域. 同样的, 任何观察者或者监听器都要根据需要添加, 在作用域和元素之前返回一个活动(双向)绑定. 因此, 任何关联到作用域的事情都发生在连接阶段. 278 | 279 | 那么在我们指令中发生了什么呢? 让我们去看一看, 好吗? 280 | 281 | `butterbar`指令可以像下面这样使用: 282 | 283 |
My loading text...
284 | 285 | 它基于前面隐藏的元素, 然后添加两个监听器到根作用域中. 当每次一个路由开始改变时, 它就显示该元素(通过改变它的class[className]), 每次路由成功改变并完成时, 它再一次隐藏`butterbar`. 286 | 287 | 另一个有趣的事情是注意我们是如何注入`$rootScope`到指令中的. 所有的指令都直接挂接到AngularJS的依赖注入系统, 因此你可以注入你的服务和其他任何需要的东西到其中. 288 | 289 | 最后需要注意的是处理元素的API. 使用jQuery的人会很高兴, 因为他直到使用的是一个类似jQuery的语法(addClass, removeClass). AngularJS实现了一个调用jQuery的自己, 因此, 对于任何AngularJS项目来说, jQuery都是一个可选的依赖项. 如果你最终在你的项目中使用完整的jQuery库, 你应该直到它使用的是它自己内置的jQlite实现. 290 | 291 | 第二个指令(focus)简单得多. 它只是在当前元素上调用`focus()`方法. 你可以用过在任何input元素上添加`focus`属性来调用它, 就像这样: 292 | 293 | 294 | 295 | 当页面加载时, 元素将立即获得焦点. 296 | 297 | ###控制器 298 | 299 | 随着指令和服务的覆盖, 我们终于可以进入控制器部分了, 我们有五个控制器. 所有的这些控制器都在一个单独的文件中(`app/scripts/controllers/controllers.js`), 但是我们会一个个来了解它们. 让我们来看第一个控制器, 这是一个列表控制器, 负责显示系统中所有的食谱列表. 300 | ```js 301 | app.controller('ListCtrl', ['scope', 'recipes', function($scope, recipes){ 302 | $scope.recipes = recipes; 303 | }]); 304 | ``` 305 | 注意列表控制器中最重要的一个事情: 在这个控制器中, 它并没有连接到服务器和获取是食谱. 相反, 它只是使用已经取得的食谱列表. 你可能不知道它是如何工作的. 你可能会使用路由一节来回答, 因为它有一个我们之前看到`MultiRecipeLoader`. 你只需要在脑海里记住它. 306 | 307 | 在我们提到的列表控制器下, 其他的控制器都与之非常相似, 但我们仍然会逐步指出它们有趣的地方: 308 | ```js 309 | app.controller('ViewCtrl', ['$scope', '$location', 'recipe', function($scope, $location, recipe){ 310 | $scope.recipe = recipe; 311 | 312 | $scope.edit = function(){ 313 | $location.path('/edit/' + recipe.id); 314 | }; 315 | }]); 316 | ``` 317 | 这个视图控制器中有趣的部分是其编辑函数公开在作用域中. 而不是显示和隐藏字段或者其他类似的东西, 这个控制器依赖于AngularJS来处理繁重的任务(你应该这么做)! 这个编辑函数简单的改变URL并跳转到编辑食谱的部分, 你可以看见, AngularJS并没有处理剩下的工作. AngularJS识别已经改变的URL并加载响应的视图(这是与我们编辑模式中相同的食谱部分). 来看一看! 318 | 319 | 接下来, 让我们来看看编辑控制器: 320 | ```js 321 | app.controller('EditCtrl', ['$scope', '$location', 'recipe', function($scope, $location, recipe){ 322 | $scope.recipe = recipe; 323 | 324 | $scope.save = function(){ 325 | $scope.recipe.$save(function(recipe){ 326 | $location.path('/view/' + recipe.id); 327 | }); 328 | }; 329 | 330 | $scope.remove = function(){ 331 | delete $scope.recipe; 332 | $location.path('/'); 333 | }; 334 | }]); 335 | ``` 336 | 那么在这个暴露在作用域中的编辑控制器中新的`save`和`remove`方法有什么. 337 | 338 | 那么你希望作用域内的`save`函数做什么. 它保存当前食谱, 并且一旦保存好, 它就在屏幕中将用户重定向到相同的食谱. 回调函数是非常有用的, 一旦你完成任务的情况下执行或者处理一些行为. 339 | 340 | 有两种方式可以在这里保存食谱. 一种是如代码所示, 通过执行$scope.recipe.$save()方法. 这只是可能, 因为`recipe`是一个通过开头部分的RecipeLoader返回的资源对象. 341 | 342 | 另外, 你可以像这样来保存食谱: 343 | ```js 344 | Recipe.save(recipe); 345 | ``` 346 | `remove`函数也是很简单的, 在这里它会从作用域中移除食谱, 同时将用户重定向到主菜单页. 请注意, 它并没有真正的从我们的服务器上删除它, 尽管它很再做出额外的调用. 347 | 348 | 接下来, 我们来看看New控制器: 349 | ```js 350 | app.controller('NewCtrl', ['$scope', '$location', 'Recipe', function($scope, $location, Recipe){ 351 | $scope.recipe = new Recipe({ 352 | ingredents: [{}] 353 | }); 354 | 355 | $scope.save = function(){ 356 | $scope.recipe.$save(function(recipe){ 357 | $location.path('/view/' + recipe.id); 358 | }); 359 | }; 360 | }]); 361 | ``` 362 | New控制器几乎与Edit控制器完全一样. 实际上, 你可以结合两个控制器作为一个单一的控制器来做一个练习. 唯一的主要不同是New控制器会在第一步创建一个新的食谱(这也是一个资源, 因此它也有一个`save`函数).其他的一切都保持不变. 363 | 364 | 最后, 我们还有一个Ingredients控制器. 这是一个特殊的控制器, 在我们深入了解它为什么或者如何特殊之前, 先来看一看它: 365 | ```js 366 | app.controller('Ingredients', ['$scope', function($scope){ 367 | $scope.addIngredients = function(){ 368 | var ingredients = $scope.recipe.ingredients; 369 | ingredients[ingredients.length] = {}; 370 | }; 371 | 372 | $scope.removeIngredient = function(index) { 373 | $scope.recipe.ingredients.splice(index, 1); 374 | }; 375 | }]); 376 | ``` 377 | 到目前为止, 我们看到的所有其他控制器斗鱼UI视图上的相关部分联系着. 但是这个Ingredients控制器是特殊的. 它是一个子控制器, 用于在编辑页面封装特定的恭喜而不需要在外层(父级)来处理. 有趣的是要注意, 由于它是一个字控制器, 继承自作用域中的父控制器(在这里就是Edit/New控制器). 因此, 它可以访问来自父控制器的`$scope.recipe`. 378 | 379 | 这个控制器本身并没有什么有趣或者独特的地方. 它只是添加一个新的成份到现有的食谱成份数组中, 或者从食谱的成份列表中删除一个特定的成份. 380 | 381 | 那么现在, 我们就来完成最后的控制器. 唯一的JavaScript代码块展示了如何设置路由: 382 | ```js 383 | // This file is app/scripts/controllers/controllers.js 384 | 385 | var app = angular.module('guthub', ['guthub.directives', 'guthub.services']); 386 | 387 | app.config(['$routeProvider', function($routeProvider){ 388 | $routeProvider. 389 | when('/', { 390 | controller: 'ListCtrl', 391 | resolve: { 392 | recipes: function(MultiRecipeLoader) { 393 | return MultiRecipeLoader(); 394 | } 395 | }, 396 | templateUrl: '/views/list.html' 397 | }).when('/edit/:recipeId', { 398 | controller: 'EditCtrl', 399 | resolve: { 400 | recipe: function(RecipeLoader){ 401 | return RecipeLoader(); 402 | } 403 | }, 404 | templateUrl: '/views/recipeForm.html' 405 | }).when('/view/:recipeId', { 406 | controller: 'ViewCtrl', 407 | resolve: { 408 | recipe: function(RecipeLoader){ 409 | return RecipeLoader(); 410 | } 411 | }, 412 | templateUrl: '/views/viewRecipe.html' 413 | }).when('/new', { 414 | controller: 'NewCtrl', 415 | templateUrl: '/views/recipeForm.html' 416 | }).otherwise({redirectTo: '/'}); 417 | }]); 418 | ``` 419 | 正如我们所承诺的, 我们终于到了解析函数使用的地方. 前面的代码设置Guthub AngularJS模块, 路由以及参与应用程序的模板. 420 | 421 | 它挂接到我们已经创建的指令和服务上, 然后指定不同的路由指向应用程序的不同地方. 422 | 423 | 对于每个路由, 我们指定了URL, 它备份控制器, 加载模板, 以及最后(可选的)提供了一个`resolve`对象. 424 | 425 | 这个`resolve`对象会告诉AngularJS, 每个resolve键需要在确定路由正确时才能显示给用户. 对我们来说, 我们想要加载所有的食谱或者个别的配方, 并确保在显示页面之前服务器要响应我们. 因此, 我们要告诉路由提供者我们的食谱, 然后再告诉他如何来取得它. 426 | 427 | 这个环节中我们在第一节中定义了两个服务, 分别时`MultiRecipeLoader`和`RecipeLoader`. 如果`resolve`函数返回一个AngularJS promise, 然后AngularJS会智能在获得它之前等待promise解决问题. 这意味着它将会等待到服务器响应. 428 | 429 | 然后将返回结果传递到构造函数中作为参数(与来自对象字段的参数的名称一起作为参数). 430 | 431 | 最后, `otherwise`函数表示当没有路由匹配时重定向到默认URL. 432 | 433 | > 你可能会注意到Edit和New控制器两个路由通向同一个模板URL, `views/recipeForm.html`. 这里发生了什么呢? 我们复用了编辑模板. 依赖于相关的控制器, 将不同的元素显示在编辑食谱模板中. 434 | 435 | 完成这些工作之后, 现在我们可以聚焦到模板部分, 来看看控制器如何挂接到它们之上, 以及如何管理现实给最终用户的内容. 436 | 437 | ##模板 438 | 439 | 让我们首先来看看最外层的主模板, 这里就是`index.html`. 这是我们单页应用程序的基础, 同时所有其他的视图也会装在到这个模板的上下文中: 440 | ```html 441 | 442 | 443 | 444 | Guthub - Create and Share 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 |
455 |

Guthub

456 |
457 |
Loading...
458 | 459 |
460 |
461 |
462 | 463 | 464 | 465 |
466 |
467 |
468 |
469 |
470 |
471 | 472 | 473 | ``` 474 | 注意前面的模板中有5个有趣的元素, 其中大部分你在第2章中都已经见过了. 让我们逐个来看看它们: 475 | 476 | `ng-app` 477 | 478 | 我们设置了`ng-app`模块为Guthub. 这与我们在`angular.module`函数中提供的模块名称相同. 这便是AngularJS如何知道两个挂接在一起的原因. 479 | 480 | `script`标签 481 | 482 | 这表示应用程序在哪里加载AngularJS. 这必须在所有使用AngularJS的JS文件被加载之前完成. 理想的情况下, 它应该在body的底部完成(\之前). 483 | 484 | `Butterbar` 485 | 486 | 我们第一次使用自定义指令. 在我们定义我们的`butterbar`指令之前, 我们希望将它用于一个元素, 以便在路由改变时显示它, 在成功的时候隐藏它(loading...处理). 需要突出显示这个元素的文本(在这里我们使用了一个非常烦人的"Loading..."). 487 | 488 | 链接的`href`值 489 | 490 | `href`用于链接到我们单页应用程序的各个页面. 追它们如何使用#字符来确保页面不会重载的, 并且相对于当前页面. AngularJS会监控URL(只要页面没有重载), 然后在需要的时候起到神奇的作用(或者通常, 将这个非常烦人的路由管理定义为我们路由的一部分). 491 | 492 | `ng-view` 493 | 494 | 这是最后一个神奇的杰作. 在我们的控制器一节, 我们定义了路由. 作为定义的一部分, 每个路由表示一个URL, 控制器关联路由和一个模板. 当AngularJS发现一个路由改变时, 它就会加载关联的模板, 并将控制器添加给它, 同时替换`ng-view`为该模板的内容. 495 | 496 | 有一件引人注目的事情是这里缺少`ng-controller`标签. 大部分应用程序某种程度上都需要一个与外部模板关联的MainController. 其最常见的位置是在body标签上. 在这种情况下, 我们并没有使用它, 因为完整的外部模板没有AngularJS内容需要引用到一个作用域. 497 | 498 | 现在我们来看看与每个控制器关联的单独的模板, 就从"食谱列表"模板开始: 499 | ```html 500 | 501 |

Recipe List

502 | 507 | ``` 508 | 是的, 它是一个非常无聊(普通)的模板. 这里只有两个有趣的点. 第一个是非常标准的`ng-repeat`标签用法. 他会获得作用域内的所有食谱并重复检出它们. 509 | 510 | 第二个是`ng-href`标签的用法而不是`href`属性. 这是一个在AngularJS加载期间纯粹无效的空白链接. `ng-href`会确保任何时候都不会给用户呈现一个畸形的链接. 总是会使用它在任何时候使你的URLs都是动态的而不是静态的. 511 | 512 | 当然, 你可能感到奇怪: 控制器在哪里? 这里没有`ng-controller`定义, 也确实没有Main Controller定义. 这是路由映射发挥的作用. 如果你还记得(或者往前翻几页), `/`路由会重定向到列表模板并且带有与之关联的ListController. 因此, 当引用任何变量或者类似的东西时, 它都在List Controller作用域内部. 513 | 514 | 现在我们来看一些有更多实质内容的东西: 视图形式. 515 | ```html 516 | 517 |

{{recipe.title}}

518 | 519 |
{{recipe.decription}}
520 | 521 |

Ingredients

522 | No Ingredients 523 |
    524 |
  • 525 | {{ingredient.amount}} 526 | {{ingredient.amountUnits 527 | {{ingredient.ingredientName}} 528 |
  • 529 |
530 | 531 |

Instructions

532 |
{{recipe.instructions}}
533 | 534 |
535 |
536 | 537 |
538 |
539 | ``` 540 | 这是另一个不错的, 很小的包含模板. 我们将提醒你注意三件事, 虽然不会按照它们所出现的顺序. 541 | 542 | 第一个就是非常标准的`ng-repeat`. 食谱(recipes)再次出现在View Controller作用域中, 这是用过在页面现实给用户之前通过`resolve`函数加载的. 这确保用户查看它时也面不是一个破碎的, 未加载的状态. 543 | 544 | 接下来一个有趣的用法是使用`ng-show`和`ng-class`(这里应该是`ng-hide`)来设置模板的样式. `ng-show`标签被添加到\标签上, 这是用来显示一个星号标记的icon. 现在, 这个星号标记只在食谱是一个特色食谱的时候才显示(例如通过`recipe.featured`布尔值来标记). 理想的情况下, 为了确保适当的间距, 你需要使用一个空白的空格图标, 并给这个空格图标绑定`ng-hide`指令, 然后同归同样的AngularJS表达式`ng-show`来显示. 这是一个常见的用法, 显示一个东西并在给定的条件下来隐藏. 545 | 546 | `ng-class`用于添加一个类(CSS类)给\标签(在这种情况下就是"特色")当食谱是一个特色食谱时. 它添加了一些特殊的高亮来使标题更加引人注目. 547 | 548 | 最后一个需要注意的时表单上的`ng-submit`指令. 这个指令规定在表单被提交的情况下调用`scope`中的`edit()`函数. 当任何没有关联明确函数的按钮被点击时机会提交表单(这种情况下便是Edit按钮). 同样, AngularJS足够智能的在作用域中(从模块,路由,控制器中)在正确的时间里引用和调用正确的方法. 549 | 550 | > **上面这段解释与原书代码有一些差别, 读者自行理解. 原书作者暂未给出解答.** 551 | 552 | 现在我们可以来看看我们最后的模板(可能目前为止最复杂的一个), 食谱表单模板: 553 | ```html 554 | 555 |

Edit Recipe

556 |
557 |
558 | 559 |
560 | 561 |
562 |
563 | 564 |
565 | 566 |
567 | 568 |
569 |
570 | 571 |
572 | 573 |
574 |
    575 |
  • 576 | 577 | 578 | 579 | 580 |
  • 581 | 582 |
583 |
584 |
585 | 586 |
587 | 588 |
589 | 590 |
591 |
592 | 593 |
594 | 595 | 596 |
597 |
598 | ``` 599 | 不要惊慌. 它看起来像很多代码, 并且它时一个很长的代码, 但是如果你认真研究以下它, 你会发现它并不是非常复杂. 事实上, 其中很多都是很简单的, 比如重复的显示可编辑输入字段用于编辑食谱的模板: 600 | 601 | + `focus`指令被添加到第一个输入字段上(`title`输入字段). 这确保当用户导航到这个页面时, 标题字段会自动聚焦, 并且用户可以立即开始输入标题信息. 602 | 603 | + `ng-submit`指令与前面的例子非常相似, 因此我们不会深入讨论它, 它只是保存是食谱的状态和编辑过程的结束信号. 它会挂接到Edit Controller中的`save()`函数. 604 | 605 | + `ng-model`指令用于将不同的文本输入框和文本域绑定到模型中. 606 | 607 | 在这个页面更有趣的一方面, 并且我们建议你花一点之间来了解它的便是配料列表部分的`ng-controller`标签. 让我们花一分钟的事件来了解以下这里发生了什么. 608 | 609 | 我们看到了一个显示配料成份的列表, 并且容器标签关联了一个`ng-controller`. 这意味着这个`\`标签是Ingredients Controller的作用域. 但是这个模板实际的控制器是什么呢, 是Edit Controller? 事实证明, Ingredients Controller是作为Edit Controller的子控制器创建的, 从而继承了Edit Controller的作用域. 这就是为什么它可以从Edit Controller访问食谱对象(recipe object)的原因. 610 | 611 | 此外, 它还增加了一个`addIngredient()`方法, 这是通过处理高亮的`ng-click`使用的, 那么就只能在`\`标签作用域内访问. 那么为什么你需要这么做呢? 因为这是分离你担忧的最好的方式. 为什么Edit Controller需要一个`addIngredients()`方法, 问99%的模板都不会关心它. 因为如此精确你的子控制器和嵌套控制器是很不错的, 它可以包含任务并循序你分离你的业务逻辑到更多的可维护模块中. 612 | 613 | + 另外一个控制器便是我们在这里想要深入讨论的表单验证控制. 它很容易在AngularJS中设置一个特定的表单字段为"必填项". 只需要简单的添加required标签到输入框上(与前面的代码中的情况一样). 但是现在你要怎么对它. 614 | 615 | 为此, 我们先跳到保存按钮部分. 注意它上面有一个`ng-disabled`指令, 这换言之就是`recipeForm.$invalid`. 这个`recipeForm`是我们已经声明的表单名称. AngularJS增加了一些特殊的变量(`$valid`和`$invalid`只是其中两个)允许你控制表单的元素. AngularJS会查找到所有的必填元素并更新所对应的特殊变量. 因此如果我们的Recipe Title为空, `recipeForm.$invalid`就会被这只为true(`$valid`就为false), 同时我们的保存(Save)按钮就会立刻被禁用. 616 | 617 | 我们还可以给一个文本输入框设置最大和最小长度(输入长度), 以及一个用于验证一个输入字段的正则表达式模式. 另外, 这里还有只在满足特定条件时用于显示特定错误消息的高级用法. 让我们使用一个很小的分支例子来看看: 618 | ```html 619 |
620 | User name: 621 | Too Short! 622 |
623 | ``` 624 | 在前面的这个例子中, 我们添加了一要求: 用户名至少是三个字符(通过使用`ng-minlength`指令). 现在, 表单范围内会关心每个命名输入框的填充形式--在这个例子中我们只有一个`userName`--其中每个输入框都会有一个`$error`对象(这里具体的还包括什么样的错误或者没有错误: `required`, `minlength`, `maclength`或者模式)和一个`$valid`标签来表示输入框本身是否有效. 625 | 626 | 我们可以利用这个来选择性的将错误信息显示给用户, 这根据不用的输入错误类型来显示, 正如我们上面的实例所示. 627 | 628 | 跳回到我们原来的模板中--Recipe表单模板--在这里的ingredients repeater里面还有另外一个很好的`ng-show`高亮的用法. 这个Add Ingredient按钮只会在最后的一个配料的旁边显示. 着可以通过在一个repeater元素范围内调用一个`ng-show`并使用特殊的`$last`变量来完成. 629 | 630 | 最后我们还有最后的一个`ng-click`, 这是附加的第二个按钮, 用于删除该食谱. 注意这个按钮只会在食谱尚未保存的时候显示. 虽然通常它会编写一个更有意义的`ng-hide="recipe.id", 有时候它会使用更有语义的`ng-show="!recipe.id". 也就是说, 如果食谱没有一个id的时候显示, 而不是在食谱有一个id的时候隐藏. 631 | 632 | ##测试 633 | 634 | 随着控制器部分, 我们已经推迟向你显示测试部分了, 但你知道它会即将到来, 不是吗? 在这一节, 我们将会涵盖你已经编写部分的代码测试, 以及涉及你要如何编写它们. 635 | 636 | ###单元测试 637 | 638 | 第一个, 也是非常重要的一种测试是单元测试. 对于控制器(指令和服务)的测试你已经开发和编写的正确的结构, 并且你可能会想到它们会做什么. 639 | 640 | 在我们深入到各个单元测试之前, 让我们围绕所有我们的控制器单元测试来看看测试装置: 641 | ```js 642 | describle('Controllers', function() { 643 | var $scope, ctrl; 644 | //you need to include your module in a test 645 | beforeEach(module('guthub')); 646 | beforeEach(function() { 647 | this.addMatchers({ 648 | toEqualData: function(expected) { 649 | return angular.equals(this.actual, expected); 650 | } 651 | }); 652 | }); 653 | 654 | describle('ListCtrl', function() {....}); 655 | // Other controller describles here as well 656 | }); 657 | ``` 658 | 这个测试装置(我们仍然使用Jasmine的行为方式来编写这些测试)做了几件事情: 659 | 660 | 1. 创建一个全局(至少对于这个测试规范是这个目的)可访问的作用域和控制器, 所以我们不用担心每个控制器会创建一个新的变量. 661 | 662 | 2. 初始化我们应用程序所用的模块(在这里是Guthub). 663 | 664 | 3. 添加一个我们称之为`equalData`的特殊的匹配器. 这基本上允许我们在资源对象(就像食谱)通过`$resource`服务和调用RESTful来执行断言(测试判断). 665 | 666 | > 记得在任何我们处理在`ngRsource`上返回对象的断言时添加一个称为`equalData`特殊匹配器. 这是因为`ngRsource`返回对象还有额外的方法在它们失败时默认希望调用equal方法. 667 | 668 | 这个装置到此为止, 让我们来看看List Controller的单元测试: 669 | ```js 670 | describle('ListCtrl', function(){ 671 | var mockBackend, recipe; 672 | // _$httpBackend_ is the same as $httpBackend. Only written this way to diiferentiate between injected variables and local variables 673 | breforeEach(inject(function($rootScope, $controller, _$httpBackend_, Recipe) { 674 | recipe = Recipe; 675 | mockBackend = _$httpBackend_; 676 | $scope = $rootScope.$new(); 677 | ctrl = $controller('ListCtrl', { 678 | $scope: $scope, 679 | recipes: [1, 2, 3] 680 | }); 681 | })); 682 | 683 | it('should have list of recipes', function() { 684 | expect($scope.recipes).toEqual([1, 2, 3]); 685 | }); 686 | }); 687 | ``` 688 | 记住这个List Controller只是我们最简单的控制器之一. 这个控制器的构造器只是接受一个食谱列表并将它保存到作用域中. 你可以编写一个测试给它, 但它似乎有一点不合理(我们还是这么做了, 因为这个测试很不错). 689 | 690 | 相反, 更有趣的是MulyiRecipeLoader服务方面. 它负责从服务器上获取食谱列表并将它作为一个参数传递(当通过`$route`服务正确的连接时). 691 | ```js 692 | describe('MultiRecipeLoader', function() { 693 | var mockBackend, recipe, loader; 694 | // _$httpBackend_ is the same as $httpBackend. Only written this way to differentiate between injected variables and local variables. 695 | 696 | beforeEach(inject(function(_$httpBackend_, Recipe, MultiRecipeLoader) { 697 | recipe = Recipe; 698 | mockBackend = _$httpBackend_; 699 | loader = MultiRecipeLoader; 700 | })); 701 | 702 | it('should load list of recipes', function() { 703 | mockBackend.expectGET('/recipes').respond([{id: 1}, {id: 2}]); 704 | 705 | var recipes; 706 | 707 | var promise = loader(); promise.then(function(rec) { 708 | recipes = rec; 709 | }); 710 | 711 | expect(recipes).toBeUndefined( ) ; 712 | 713 | mockBackend. f lush() ; 714 | 715 | expect(recipes).toEqualData([{id: 1}, {id: 2}]); }); 716 | }); 717 | // Other controller describes here as well 718 | ``` 719 | 在我们的测试中, 我们通过挂接到一个模拟的`HttpBackend`来测试MultiRecipeLoader. 这来自于测试运行时所包含的`angular-mocks.js`文件. 只需将它注入到你的`beforeEach`方法中就足以让你设置预期目的. 接下来, 我们进行了一个更有意义的测试, 我们期望设置一个服务器的GET请求来获取recipes, 浙江返回一个简单的数组对象. 然后使用我们新的自定义的匹配器来确保正确的返回数据. 注意在模拟backend中的`flush()`调用, 这将告诉模拟Backend从服务器返回响应. 你可以使用这个机制来测试控制流程和查看你的应用程序在服务器返回一个响应之前和之后是如何处理的. 720 | 721 | 我们将跳过View Controller, 因为它除了在作用域中添加一个`edit()`方法之外基于与List Controller一模一样. 这是非常简单的测试, 你可以在你的测试中注入`$location`并检查它的值. 722 | 723 | 现在让我们跳到Edit Controller, 其中有两个有趣的点我们进行单元测试. 一个是类似我们之前看到过的`resolve`函数, 并且可以以同样的方式测试. 相反, 我们现在想看看我们可以如和测试`save()`和`remove()`方法. 让我们来看看对于它们的测试(假设我们的测试工具来自于前面的例子): 724 | ```js 725 | describle('EditController', function() { 726 | var mockBackend, location; 727 | beforeEach(inject($rootScope, $controller, _$httpBackend_, $location, Recipe){ 728 | mockBackend = _$httpBackend_; 729 | location = $location; 730 | $scope = $rootScope.$new(); 731 | 732 | ctrl = $controller('EditCtrl', { 733 | $scope: $scope, 734 | $location: $location, 735 | recipe: new Recipe({id: 1, title: 'Recipe'}); 736 | }); 737 | })); 738 | 739 | it('should save the recipe', function(){ 740 | mockBackend.expectPOST('/recipes/1', {id: 1, title: 'Recipe'}).respond({id: 2}); 741 | 742 | // Set it to something else to ensure it is changed during the test 743 | location.path('test'); 744 | 745 | $scope.save(); 746 | expect(location.path()).toEqual('/test'); 747 | 748 | mockBackend.flush(); 749 | 750 | expect(location.path()).toEqual('/view/2'); 751 | }); 752 | 753 | it('should remove the recipe', function(){ 754 | expect($scope.recipe).toBeTruthy(); 755 | location.path('test'); 756 | 757 | $scope.remove(); 758 | 759 | expect($scope.recipe).toBeUndefined(); 760 | expect(location.path()).toEqual('/'); 761 | }); 762 | }); 763 | ``` 764 | 在第一个测试用, 我们测试了`save()`函数. 特别是, 我们确保在我们的对象保存时首先创建一个到服务器的POST请求, 然后, 一旦服务器响应, 地址就改变到新的持久对象的视图食谱页面. 765 | 766 | 第二个测试更简单. 我们进行了简单的检测以确保在作用域中调用`remove()`方法的时候移除当前食谱, 然后重定向到用户主页. 这可以很容易通过注入`$location`服务到我们的测试中并使用它. 767 | 768 | 其余的针对控制器的单元测试遵循非常相似的模式, 因此在这里我们跳过它们. 在他们的底层中, 这些单元测试依赖于一些事情: 769 | 770 | + 确保控制器(或者更可能是作用域)在结束初始化时达到正确的状态 771 | 772 | + 确认经行正确的服务器调用, 以及通过作用域在服务器调用期间和完成后去的正确的状态(通过在单元测试中使用我们的模拟后端服务) 773 | 774 | + 利用AngularJS的依赖注入框架着手处理元素以及控制器对象用于确保控制器会设置正确的状态. 775 | 776 | ###脚本测试 777 | 778 | 一旦我们对单元测试很满意, 我们可能禁不住的往后靠一下, 抽根雪茄, 收工. 但是AngularJS开发者不会这么做, 直到他们完成了他们的脚本测试(场景测试). 虽然单元测试确保我们的每一块JS代码都按照预期工作, 我们也要确保模板加载, 并正确的挂接到控制器上, 以及在模板重点击做正确的事情. 779 | 780 | 这正是AngularJS带给你的脚本测试(场景测试), 它允许你做以下事情: 781 | 782 | + 加载你的应用程序 783 | + 浏览一个特定的页面 784 | + 随意的点击周围和输入文本 785 | + 确保发生正确的事情 786 | 787 | 所以, 脚本测试如何在我们的"食谱列表"页面工作? 首先, 在我们开始实际的测试之前, 我们需要做一些基础工作. 788 | 789 | 对于该脚本测试工作, 我们需要一个工作的Web服务器以准备从Guthub应用上接受请求, 同时将允许我们从它上面存储和获取一个食谱列表. 随意的更改代码以使用内存中的食谱列表(移除`$resource`食谱并只是将它转换为一个JSON对象), 或者复用和修改我们前面章节向你展示的Web服务器, 或者使用Yeoman! 790 | 791 | 一旦我们有了一个服务器并运行起来, 同时服务于我们的应用程序, 然后我们就可以编写和运行下面的测试: 792 | ```js 793 | describle('Guthub App', function(){ 794 | it('should show a list of recipes', function(){ 795 | browser().navigateTo('/index.html'); 796 | //Our Default Guthub recipes list has two recipes 797 | expect(repeater('.recipes li').count()).toEqual(2); 798 | }); 799 | }); 800 | ``` 801 | -------------------------------------------------------------------------------- /Chapter5.markdown: -------------------------------------------------------------------------------- 1 | #与服务器通信 2 | 3 | 目前,我们已经接触过下面要谈的主题的主要内容,这些内容包括你的Angular应用如何规划设计、不同的AngularJS部件如何装配在一起并正常工作以及AngularJS中的模板代码运行机制的一小部分内容。把它们结合在一起,你就可以搭建一些简洁优雅的Web应用,但他们的运作主要还是限制在客户端.在前面第二章,我们接触了一点用`$http`服务做与服务器端通信的内容,但是在这一章,我们将会深入探讨如何在现实世界的真实应用中使用它(`$http`). 4 | 5 | 在这一章,我们将讨论一下AngularJS如何帮你与服务器端通信,这其中包括在最低抽象等级的层面或者用它提供的优雅的封装器。而且我们将会深入探讨AngularJS如何用内建缓存机制来帮你加速你的应用.如果你想用`SocketIO`开发一个实时的Angular应用,那么第八章有一个例子,演示了如何把·SocketIO·封装成一个指令然后如何使用这个指令,在这一章,我们就不涉及这方面内容了. 6 | 7 | ## 目录 8 | 9 | - [通过$http进行通行](#通过http进行通行) 10 | - [进一步配置你的请求](#进一步配置你的请求) 11 | - [设定HTTP头信息(Headers)](#设定http头信息headers) 12 | - [缓存响应数据](#缓存响应数据) 13 | - [对请求(Request)和响应(Response)的数据所做的转换](#对请求request和响应response的数据所做的转换) 14 | - [单元测试](#单元测试) 15 | - [使用RESTful资源](#使用restful资源) 16 | - [resource资源的声明](#resource资源的声明) 17 | - [定制方法](#定制方法) 18 | - [不要使用回调函数机制!(除非你真的需要它们)](#不要使用回调函数机制除非你真的需要它们) 19 | - [简化的服务器端操作](#简化的服务器端操作) 20 | - [对ngResource做单元测试](#对ngresource做单元测试) 21 | - [$q和预期值(Promise)](#q和预期值promise) 22 | - [响应拦截处理](#响应拦截处理) 23 | - [安全方面的考虑](#安全方面的考虑) 24 | - [JSON的安全脆弱性](#json的安全脆弱性) 25 | - [跨站请求伪造(XSRF)](#跨站请求伪造xsrf) 26 | 27 | ##通过$http进行通行 28 | 29 | 从Ajax应用(使用XMLHttpRequests)发动一个请求到服务器的传统方式包括:得到一个XMLHttpRequest对象的引用、发起请求、读取响应、检验错误代码然后最后处理服务器响应。它就是下面这样: 30 | 31 | var xmlhttp = new XMLHttpRequest(); 32 | xmlhttp.onreadystatechange = function() { 33 | if (xmlhttp.readystate == 4 && xmlhttp.status == 200) { 34 | var response = xmlhttp.responseText; 35 | } else if (xmlhttp.status == 400) { // or really anything in the 4 series 36 | // Handle error gracefully 37 | } 38 | }; 39 | // Setup connection 40 | xmlhttp.open(“GET”, “http://myserver/api”, true); 41 | // Make the request 42 | xmlhttp.send(); 43 | 44 | 对于这样一个简单、常用且经常重复的任务,上面这个代码量比较大.如果你想重复性地做这件事,你最终可能会做一个封装或者使用现成的库. 45 | 46 | AngularJS XHR(XMLHttpRequest) API遵循Promise接口.因为XHRs是异步方法调用,服务器响应将会在未来一个不定的时间返回(当然希望是越快越好).Promise接口保证了这样的响应将会如何处理,它允许Promise接口的消费者以一种可预计的方式使用这些响应. 47 | 48 | 假设我们想从我们的服务器取回用户的信息.如果接口在/api/user地址可用,并且接受id作为url参数,那么我们的XHR请求就可以像下面这样使用Angular的核心$http服务: 49 | 50 | $http.get('api/user', {params: {id: '5'} 51 | }).success(function(data, status, headers, config) { 52 | // Do something successful. 53 | }).error(function(data, status, headers, config) { 54 | // Handle the error 55 | }); 56 | 57 | 如果你来自jQuery世界,你可能会注意到:AngularJS和jQuery处理异步需求的方式很相似. 58 | 59 | 我们上面例子中使用的$htttp.get方法仅仅是AngularJS核心服务$http提供的众多简便方法之一.类似的,如果你想使用AngularJS向相同URL带一些POST请求数据发起一个POST请求,你可以像下面这样做: 60 | 61 | var postData = {text: 'long blob of text'}; 62 | // The next line gets appended to the URL as params 63 | // so it would become a post request to /api/user?id=5 64 | var config = {params: {id: '5'}}; 65 | $http.post('api/user', postData, config 66 | ).success(function(data, status, headers, config) { 67 | // Do something successful 68 | }).error(function(data, status, headers, config) { 69 | // Handle the error 70 | }); 71 | 72 | AngularJS为大多数常用请求类型都提供了类似的简便方法,他们包括: 73 | 74 | + GET 75 | + HEAD 76 | + POST 77 | + DELETE 78 | + PUT 79 | + JSONP 80 | 81 | ###进一步配置你的请求 82 | 83 | 有时,工具箱提供的标准请求配置还不够,它可能是因为你想做下面这些事情: 84 | 85 | + 你可能想为请求添加权限验证的头信息 86 | + 改变请求数据的缓存方式 87 | + 在请求被发送或者响应返回时,对数据以一些方式做一定的转换处理 88 | 89 | 在上面这样的情况之下,你可以进一步配置请求,通过可选的传递进请求的配置对象.在之前的例子中,我们使用配置对象来标明可选的URL参数,即便我们哪儿演示的GET和POST方法是简便方法。内部的原生方法可能看上面像相面这样: 90 | 91 | $http(config) 92 | 93 | 下面演示的是一个调用这个方法的伪代码模板: 94 | 95 | $http({ 96 | method: string, 97 | url: string, 98 | params: object, 99 | data: string or object, 100 | headers: object, 101 | transformRequest: function transform(data, headersGetter) or an array of functions, 102 | transformResponse: function transform(data, headersGetter) or an array of functions, 103 | cache: boolean or Cache object, 104 | timeout: number, 105 | withCredentials: boolean 106 | }); 107 | 108 | GET、POST和其它的简便方法已经设置了请求的method类型,所以不需要再设置这个,config配置对象是传给·$http.get·、·$http.post·方法的最后一个参数,所以当你使用任何简便方法的时候,你任何能用这个config配置对象. 109 | 110 | 你也可以通过传入含有下面这些键的属性集config对象来改变已有的request对象 111 | 112 | + method : 一个表示http请求类型的字符串,比如GET,或者POST 113 | + url : 一个URL字符串代表要请求资源的绝对或相对URL 114 | + params : 一个对象(准确的说是键值映射)包含字符串到字符串内容,它代表了将会转换为URL参数的键值对,比如下面这样: 115 | [{key1: 'value1', key2: 'value2'}] 116 | 它将会被转换为: 117 | ?key1=value&key2=value2 118 | 这串字符将会加在URL后面,如果在value的位置你用一个对象取代字符串或数字,那这个对象将会转换为JSON字符串. 119 | + data :一个字符串或一个对象,它将会被作为请求消息数据被发送. 120 | + timeout : 这是请求被认定为过期之前所要等待的毫秒数. 121 | 122 | 还有部分另外的选项可以被配置,在下面的章节中,我们将会深度探索这些选项. 123 | 124 | ###设定HTTP头信息(Headers) 125 | 126 | AngularJS有一个默认的头信息,这个头信息将会对所有的发送请求使用,它包含以下信息: 127 | 1.Accept: application/json, text/plain, / 128 | 2.X-Requested-With:XMLHttpRequest 129 | 130 | 如果你想设置任何特定的头信息,这儿有两种方法来做这件事: 131 | 132 | 第一种方法,如果你相对所有的发送请求都使用这些特定头信息,那你需要把特定有信息设置为Angular默认头信息的一部分.可以在`$httpProvider.defaults.headers`配置对象里面设置这个,这个步骤通常会在你的app设置config部分来做.所以如果你想移除"Requested-With"头信息且对所有的GET请求启用"DO NOT TRACK"设置,你可以简单地通过以下代码来做: 133 | 134 | angular.module('MyApp',[]). 135 | config(function($httpProvider) { 136 | // Remove the default AngularJS X-Request-With header 137 | delete $httpProvider.default.headers.common['X-Requested-With']; 138 | // Set DO NOT TRACK for all Get requests 139 | $httpProvider.default.headers.get['DNT'] = '1'; 140 | }); 141 | 142 | 如果你只想对某个特定的请求设置头信息,而不是设置默认头信息.那么你可以通过给$http服务传递包含指定头信息的config对象来做.相同的定制头信息可以作为第二个参数传递给GET请求,第一个参数是URL字符串: 143 | 144 | $http.get('api/user', { 145 | // Set the Authorization header. In an actual app, you would get the auth 146 | // token from a service 147 | headers: {'Authorization': 'Basic Qzsda231231'}, 148 | params: {id: 5} 149 | }).success(function() { // Handle success }); 150 | 151 | 如何在应用中处理权限验证头信息的成熟示例将会在第八章的Cheetsheets示例部分给出. 152 | 153 | ###缓存响应数据 154 | 155 | AngularJS为HTTP GET请求提供了一个开箱即用的简单缓存系统.缺省情况下,它对所有的请求都是禁用的,但是如果你想对你的请求启用缓存系统,你可以使用以下代码: 156 | 157 | $http.get('http://server/myapi', { 158 | cache: true 159 | }).success(function() { // Handle success }); 160 | 161 | 这段代码启用了缓存系统,然后AngularJS将会缓存来自Server的响应数据.但对相同的URL的请求第二次发出时,AngularJS将会从缓存里面取出前一次的响应数据作为响应返回.这个缓存系统也很智能,即使你同时对相同URL发出多个请求,只有一个请求会发向Server,这个请求的响应数据将会反馈给所有(同时发起的)请求。 162 | 163 | 然而这种做法从可用性的角度看可能是有所冲突的,当一个用户首先看到旧的结果,然后新的结果突然冒出来,比如一个用户可能即将单击一个数据项,而实际上这个数据项后台已经发生了变化. 164 | 165 | 注意所有响应(即使是从缓存里取出的)本质上仍旧是异步响应.换句话说,期望你的利用缓存响应时的异步代码运行仍旧和他向后台服务器发出请求时的代码运行机制是一样的. 166 | 167 | ###对请求(Request)和响应(Response)的数据所做的转换 168 | 169 | AngularJS对所有`$http`服务发起的请求和响应做一些基本的转换,它们包括: 170 | 171 | + 请求(Request)转换: 172 | 如果请求的Cofig配置对象的data属性包含一个对象,将会把这个对象序列化为JSON格式. 173 | + 响应(Response)转换: 174 | 如果探测到一个XSRF头,把它剥离掉.如果响应数据被探测为JSON格式,用JSON解析器把它反序列化为JSON对象. 175 | 176 | 如果你需要部分系统默认提供的转换,或者想使用你自己的转换,你可以把你的转换函数作为Config配置对象的一部分传递进去(后面有细述).这些转换函数得到HTTP请求和HTTP响应的数据主体以及它们的头信息.然后把序列化的修改后版本返回出来.在Config对象里面配置这些函数需要使用·transformRequest·键和·transformResponse·键,这些都可以通过使用`$httpProvider·服务在模块的config函数里面配置它. 177 | 178 | 我们什么时候使用这些哪?让我假设我们有一个服务器,它更习惯于jQuery运行的方式.它可能希望我们的POST数据以`key1=val1&key2=val2`字符串的形式传递,而不是以`{key1:val1,key2:val2}`这样的JSON格式.这个时候,我们可能相对每个请求做这样的转换,或者单个地增加transformRequest转换函数,为了达成这个示例这样的目标,我们将要设置一个通用transformRequet转换函数,以便对所有的发出请求,这个函数都可以把JSON格式转换为键值对字符串,下面代码演示了如何做这个工作: 179 | 180 | var module = angular.module('myApp'); 181 | module.config(function ($httpProvider) { 182 | $httpProvider.defaults.transformRequest = function(data) { 183 | // We are using jQuery’s param method to convert our 184 | // JSON data into the string form 185 | return $.param(data); 186 | }; 187 | }); 188 | 189 | ##单元测试 190 | 191 | 目前为止,我们已经了解如何使用`$http`服务以及如何以可能的方式做你需要的配置.但是如何写一些单元测试来保证这些够真实有效的运行哪? 192 | 193 | 正如我们曾经三番五次的提到的那样,AngularJS一直以测试为先的原则而设计.所以Angualr有一个模拟服务器后端,在单元测试中,它可以帮你就可以测试你发出的请求是否正确,甚至可以精确控制模拟响应如何得到处理,什么时候得到处理. 194 | 195 | 让我们探索一下下面这样的单元测试场景:一个控制向服务器发起请求,从服务器得到数据,把它赋给作用域内的模型,然后以具体的模板格式显示出来. 196 | 197 | 我们的`NameListCtrl`控制器是一个非常简单的控制器.它的存在只有一个目的:访问`names`API接口,然后把得到数据存储在作用域scope模型内. 198 | 199 | function NamesListCtrl($scope, $http) { 200 | $http.get('http://server/names', {params: {filter: ‘none’}}). 201 | success(function(data) { 202 | $scope.names = data; 203 | }); 204 | } 205 | 206 | 怎样对这个控制器做单元测试?在我们的单元测试中,我们必须保证下面这些条件: 207 | 208 | + `NamesListCtrl`能够找到所有的依赖项(并且正确的得到注入的这些依赖)》 209 | + 当控制器加载时尽可能快地立刻发情请求从服务器得到names数据. 210 | + 控制器能够正确地把响应数据存储到作用域scope的`names`变量属性中. 211 | 212 | 在我们的单元测试中构造一个控制器时,我们给它注入一个scope作用域和一个伪造的HTTP服务,在构建测试控制器的方式和生产中构建控制器的方式其实是一样的.这是推荐方法,尽管它看上去上有点复杂。让我看一下具体代码: 213 | 214 | describe('NamesListCtrl', function(){ 215 | var scope, ctrl, mockBackend; 216 | 217 | // AngularJS is responsible for injecting these in tests 218 | beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) { 219 | // This is a fake backend, so that you can control the requests 220 | // and responses from the server 221 | mockBackend = _$httpBackend_; 222 | 223 | // We set an expectation before creating our controller, 224 | // because this call will get triggered when the controller is created 225 | mockBackend.expectGET('http://server/names?filter=none'). 226 | respond(['Brad', 'Shyam']); 227 | scope = $rootScope.$new(); 228 | 229 | // Create a controller the same way AngularJS would in production 230 | ctrl = $controller(PhoneListCtrl, {$scope: scope}); 231 | })); 232 | 233 | it('should fetch names from server on load', function() { 234 | // Initially, the request has not returned a response 235 | expect(scope.names).toBeUndefined(); 236 | 237 | // Tell the fake backend to return responses to all current requests 238 | // that are in flight. 239 | mockBackend.flush(); 240 | // Now names should be set on the scope 241 | expect(scope.names).toEqual(['Brad', 'Shyam’]); 242 | }); 243 | }); 244 | 245 | ##使用RESTful资源 246 | 247 | ·$http·服务提供一个比较底层的实现来帮你发起XHR请求,但是同时也给提供了很强的可控性和弹性.在大多数情况下,我们处理的是对象集或者是封装有一定属性和方法的对象模型,比如带有个人资料的自然人对象或者信用卡对象. 248 | 249 | 在上面这样的情况下,如果我们自己构建一个JS对象来表示这种较复杂对象模型,那做法就有点不够nice.如果我们仅仅想编辑某个对象的属性、保存或者更新一个对象,那我们如何让这些状态在服务器端持久化. 250 | 251 | `$resource`正好给你提供这种能力.AngularJS resources可以帮助我们以描述的方式来定义对象模型,可以定义一下这些特征: 252 | 253 | + resource的服务器端URL 254 | + 这种请求常用参数的类型 255 | + (你可以免费自动得到get、save、query、remove和delete方法),除了那些方法,你可以定义其它的方法,这些方法封装了对象模型的特定功能和业务逻辑(比如信用卡模型的charge()付费方法) 256 | + 响应的期望类型(数组或者一个独立对象) 257 | + 头信息 258 | 259 | ------------------------------------------------------ 260 | 什么时候你可以用Angular Resources组件? 261 | 262 | 只有你的服务器端设施是以RESTful方式提供服务的时候,你才应该用Angular resources组件.比如信用卡那个案例,我们将用它作为本章这一部分的例子,他将包括以下内容: 263 | 264 | 1. 给地址`/user/123/card`发送一个GET请求,将会得到用户123的信用卡列表. 265 | 2. 给地址`/user/123/card/15`发送一个GET请求,将会得到用户123本人的ID为15的信用卡信息 266 | 3. 给地址`/user/123/card`发送一个在POST请求数据部分包含信用卡信息的POST请求,将会为用户123新创建一个信用卡 267 | 4. 给地址`/user/123/card/15`发送一个在POST请求数据部分包含信用卡信息的POST请求,将会更新用户123的ID为5的信用卡的信息. 268 | 5. 给地址`/user/123/card/15`一个方法为DELETE类型的请求,将会删除掉用户123的ID为5的信用卡的数据. 269 | 270 | ------------------------------------------------------- 271 | 272 | 除了按照你的要求给你提供一个查询服务器端信息的对象,`$resource`还可以让你使用返回的数据对象就像他们是持久化数据模型一样,可以修改他们,还可以把你的修改持久化存储下来. 273 | 274 | `ngResource`是一个单独的、可选的模块.要想使用它,你看需要做以下事情: 275 | 276 | + 在你的HTML文件里面引用angular-resource.js的实际地址 277 | + 在你的模块依赖里面声明对它的依赖(例如,angular.module('myModule',['ngResource'])). 278 | + 在需要的地方,注入$resource这个依赖项. 279 | 280 | 在我们看怎样用ngResource方法创建一个resource资源之前,我们先看一下怎样用基本的$http服务做类似的事情.比如我们的信用卡resource,我们想能够读取、查询、保存信用卡信息,另外还要能为信用卡还款. 281 | 282 | 这儿是上述需求一个可能的实现: 283 | 284 | myAppModule.factory('CreditCard', ['$http', function($http) { 285 | var baseUrl = '/user/123/card'; 286 | return { 287 | get: function(cardId) { 288 | return $http.get(baseUrl + '/' + cardId); 289 | }, 290 | save: function(card) { 291 | var url = card.id ? baseUrl + '/' + card.id : baseUrl; 292 | return $http.post(url, card); 293 | }, 294 | query: function() { 295 | return $http.get(baseUrl); 296 | }, 297 | charge: function(card) { 298 | return $http.post(baseUrl + '/' + card.id, card, {params: {charge: true}}); 299 | } 300 | }; 301 | }]); 302 | 303 | 取代以上方式,你也可以轻松创建一个在你的应用中始终如一的Angular资源服务,就像下面代码这样: 304 | 305 | myAppModule.factory('CreditCard', ['$resource', function($resource) { 306 | return $resource('/user/:userId/card/:cardId', 307 | {userId: 123, cardId: '@id'}, 308 | {charge: {method:'POST', params:{charge:true}, isArray:false}); 309 | }]); 310 | 311 | 做到现在,你就可以任何时候从Angular注入器里面请求一个CreditCard依赖,你就会得到一个Angular资源,默认情况下,这个资源会提供一些初始的可用方法.表格5-1列出了这些初始方法以及他们的运行行为,这样你就可以知道在服务器怎样配置来配合这些方法了. 312 | 313 | 表格5-1 一个信用卡reource 314 | Function Method URL Expected Return 315 | CreditCard.get({id: 11}) GET /user/123/card/11 Single JSON 316 | CreditCard.save({}, ccard) POST /user/123/card with post data “ccard” Single JSON 317 | CreditCard.save({id: 11}, ccard) POST /user/123/card/11 with post data “ccard” Single JSON 318 | CreditCard.query() GET /user/123/card JSON Array 319 | CreditCard.remove({id: 11}) DELETE /user/123/card/11 Single JSON 320 | CreditCard.delete({id: 11}) DELETE /user/123/card/11 Single JSON 321 | 322 | 让我们看一个信用卡resource使用的代码样例,这样可以让你理解起来觉得更浅显易懂. 323 | 324 | // Let us assume that the CreditCard service is injected here 325 | // We can retrieve a collection from the server which makes the request 326 | // GET: /user/123/card 327 | var cards = CreditCard.query(); 328 | // We can get a single card, and work with it from the callback as well 329 | CreditCard.get({cardId: 456}, function(card) { 330 | // each item is an instance of CreditCard 331 | expect(card instanceof CreditCard).toEqual(true); 332 | card.name = "J. Smith"; 333 | // non-GET methods are mapped onto the instances 334 | card.$save(); 335 | // our custom method is mapped as well. 336 | card.$charge({amount:9.99}); 337 | // Makes a POST: /user/123/card/456?amount=9.99&charge=true 338 | // with data {id:456, number:'1234', name:'J. Smith'} 339 | }); 340 | 341 | 前面这个样例代码里面发生了很多事情,所以我们将会一个一个地认真讲解其中的重要部分: 342 | 343 | ###resource资源的声明 344 | 345 | 声明你自己的`$resource`非常简单,只要调用注入的$resource函数,并给他传入正确的参数即可。(你现在应该已经知道如何注入依赖,对吧?) 346 | 347 | $resource函数只有一个必须参数,就是提供后台资源数据的URL地址,另外还有两个可选参数:默认request参数信息和其它的想在资源上要配置的动作. 348 | 349 | 请注意:第一个URL地址参数中的的变量数据是参数化可配置的(:冒号是参数变量的语法符号,比如`:userId`以为这个参数将会被实际的userId参数变量取代(译者注:写过参数化SQL语句的人应该很熟悉),而`:cardId`将会被cardId参数变量的值所取代),如果没有给函数传递这些参数变量,那那个位置将会被空字符取代. 350 | 351 | 函数的第二个参数则负责提供所有请求的默认参数变量信息.在这个案例中,我们给userId参数传递一个常量:123,cardId参数则更有意思,我们给cardId参数传递了一个"@id"字符串.这意味着如果我们使用一个从服务器返回的对象而且我们可以调用这个对象的任何方法(比如$save),那么这个对象的id属性将会被取出来赋给cardId字段. 352 | 353 | 函数的第三个参数是一些我们想要暴露的其它方法,这些方法是对定制资源做操作的方法.在下面的章节,我们将会深度讨论这个话题 354 | 355 | ###定制方法 356 | 357 | $resource函数的第三个参数是可选的,主要用来传递要在resource资源上暴露的其它自定义方法。 358 | 359 | 在这个案例中,我们自定义了一个方法charge.这个自定义方法可以通过传递一个对象而被配置上.这个对象里有个键值,表明了此方法的暴露名称.这个配置需要顶一个request请求的方法类型(GET,POST等等),以及该请求中需要的参数也要被传递(比如charge=true),并且声明返回对象是数组还是单个普通对象。这一切到搞定之后,你就可以在有这个业务实际需要求的时候,自由地调用`CreditCard.charge()`方法. 360 | 361 | ###不要使用回调函数机制!(除非你真的需要它们) 362 | 363 | 第三个需要注意的事情是资源调用的返回类型.让我们再次关注一下`CreditCard.query()`这个函数.你将会看到不是在回调函数中给cards赋值,而是直接把它赋给card变量.在异步服务器请求的情况下唉,这样的代码是如何运作的哪? 364 | 365 | 你担心代码是否正常工作是对的,但是代码实际上是可以正常工作的.这里实际发生的是AngularJS赋值了一个引用(是普通对象的还是数组的取决于你期望的返回类型),这个引用将会在未来服务器请求响应返回时被填充.在这期间,这个引用是个空应用. 366 | 367 | 因为在AngularJS应用中的大多数通用过程都是从服务器端取数据,把它赋给一个变量,然后在模版上显示它,而上面这样的简化机制非常优雅.在你的控制器代码中,你所有需要去做的就是发出服务器端请求,把返回值赋给正确的作用域(scope)变量.然后剩下的合适渲染这些数据就由模板系统去操心了. 368 | 369 | 如果你想对返回值做一些业务逻辑处理,拿着汇总方法就不能奏效了.在这种情况下,你就得依赖回调函数机制了,比如在Credit.get()调用中使用的那种机制. 370 | 371 | ###简化的服务器端操作 372 | 373 | 无论你使用资源简化函数机制还是回调函数,关于返回对象都有几点问题需要我们注意. 374 | 375 | 返回的对象不是一个普通JS对象,实际上,他是“resource”类型的对象.这就意味着对象里除了包含服务器返回的数据以外,还有一些附加的行为函数(在这个案例中如$save()和$charge函数).这样我们就可以很方便的执行服务器端操作,比如取数据、修改数据并把修改在服务器端持久化保存下来(其实也就是一般CURD应用里面的通用操作). 376 | 377 | ###对ngResource做单元测试 378 | 379 | ngResource依赖项是一个封装,它以Angular核心服务`$http`为基础.因此,你可能已经知道如何对它做单元测试了.它和我们看到的对`$http`做单元测试的样例比起来基本没什么真正的变化.你只需要知道最终的服务器端请求应该由resource发起,告诉模拟`$http`服务关于请求的信息.其他的基本都一样.下面我们来看看如何本节测试前面的代码: 380 | 381 | describe('Credit Card Resource', function(){ 382 | var scope, ctrl, mockBackend; 383 | beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) { 384 | mockBackend = _$httpBackend_; 385 | scope = $rootScope.$new(); 386 | // Assume that CreditCard resource is used by the controller 387 | ctrl = $controller(CreditCardCtrl, {$scope: scope}); 388 | })); 389 | 390 | it('should fetched list of credit cards', function() { 391 | // Set expectation for CreditCard.query() call 392 | mockBackend.expectGET('/user/123/card'). 393 | respond([{id: '234', number: '11112222'}]); 394 | 395 | ctrl.fetchAllCards(); 396 | 397 | // Initially, the request has not returned a response 398 | expect(scope.cards).toBeUndefined(); 399 | 400 | // Tell the fake backend to return responses to all current requests 401 | // that are in flight. 402 | mockBackend.flush(); 403 | 404 | // Now cards should be set on the scope 405 | expect(scope.cards).toEqualData([{id: '234', number: '11112222'}]); 406 | }); 407 | }); 408 | 409 | 这个测试看上去和简单的`$http`单元测试非常相似,除了一些细微区别.注意在我们的expect语句里面,取代了简单的"equals"方法,哦我们用的是特殊的"toEqualData"方法.这种eapect语句会智能地省略ngResource添加到对象上的附加方法. 410 | 411 | ##`$q`和预期值(Promise) 412 | 413 | 目前为止,我们已经看到了AngulrJS是如何实现它的异步延迟API机制. 414 | 415 | 预期值建议(Promise proposal)是AngularJS构建异步延迟API的底层基础.作为底层机制,预期值建议(Promise proposal)为异步请求做了下面这些事: 416 | 417 | + 异步请求返回的是一个预期(promise)而不是一个具体数据值. 418 | + 预期值有一个`then`函数,这个函数有两个参数,一个参数函数响应"resolved“或者"sucess"事件,另外一个参数函数响应"rejected”或者"failure"事件.这些函数以一个结果参数调用,或者以一个拒绝原因参数调用. 419 | + 确保当结果返回的时候,两个参数函数中有一个将会被调用 420 | 421 | 大多数的延迟机制和Q(详见$q API文档)是以上面这种方法实现的,AngularJS为什么这样实现具体是因为以下原因: 422 | 423 | + $q对于整个AngularJS是可见的,因此它被集成到作用域数据模型里面。这样返回数据就能快速传递,UI上的闪烁更新也就更少. 424 | + AngularJS模板也能识别$q预期值,因为预期值可以被当作结果值一样对待,而不是把它仅仅当作结果的预期.这种预期值会在响应返回时被通知提醒. 425 | + 更小的覆盖范围:AngularJS仅仅实现那些基本的、对于公共异步任务的需求来说最重要的延迟函数机制. 426 | 427 | 你也许会问这样的问题:为什么我们会做如此疯狂激进的实现机制?让我们先看一个在在异步函数使用方面的标准问题: 428 | 429 | fetchUser(function(user) { 430 | fetchUserPermissions(user, function(permissions) { 431 | fetchUserListData(user, permissions, function(list) { 432 | // Do something with the list of data that you want to display 433 | }); 434 | }); 435 | }); 436 | 上面这个代码就是人们使用JavaScirpt时经常抱怨的令人恐惧的深层嵌套缩进椎体的噩梦.返回值异步本质与实际问题的同步需求之间产生矛盾:导致多级函数包含关系,在这种情况下要想准确跟踪里面某句代码的执行上下文环境就很难. 437 | 438 | 另外,这种情况对错误处理也有很大影响.错误处理的最好方法是什么?在每次都做错误处理?那代码结构就会非常乱. 439 | 440 | 为了解决上面这些问题,预期值建议(Promise proposal)机制提供了一个then函数的概念,这个函数会在响应成功返回的时候调用相关的函数去执行,另一方面,当产生错误的时候也会干相同的事,这样整个代码就有嵌套结构变为链式结构.所以之前那个例子用预期值API机制(至少在AngularJS中已经被实现的)改造一下,代码结构会平整许多: 441 | 442 | var deferred = $q.defer(); 443 | var fetchUser = function() { 444 | // After async calls, call deferred.resolve with the response value 445 | deferred.resolve(user); 446 | 447 | // In case of error, call 448 | deferred.reject(‘Reason for failure’); 449 | } 450 | // Similarly, fetchUserPermissions and fetchUserListData are handled 451 | 452 | deferred.promise.then(fetchUser) 453 | .then(fetchUserPermissions) 454 | .then(fetchUserListData) 455 | .then(function(list) { 456 | // Do something with the list of data 457 | }, function(errorReason) { 458 | // Handle error in any of the steps here in a single stop 459 | }); 460 | 461 | 那个完全的横椎体代码一下子被优雅地平整了,而且提供了链式的作用域,以及一个单点的错误处理.你在你自己的应用中处理异步请求回调时也可以用相同的代码,只要调用Angular的$q服务.这种机制可以帮我做一些很酷的事情:比如响应拦截. 462 | 463 | ##响应拦截处理 464 | 465 | 我们的讲解已经覆盖了怎样调用服务器端服务、怎样处理响应、怎样把响应优雅地抽象化封装、怎样处理异步回调.但是在真实世界的Web应用中,你最终还不得不对每个服务器端请求调用做一些通用的处理操作,比如错误处理、权限认证、以及其它考虑到安全问题的处理操作,比如对响应数据做裁剪处理(译注:有的Ajax响应为了安全需要,会添加一定约定好的噪声数据). 466 | 467 | 有着现在已经对$q API的深入理解,我们目前就可以利用响应拦截器机制来做上面所有提出过的功能.响应拦截器(正如其名)可以在响应数据被应用使用之前拦截他它,并且对它做数据转换处理,比如错误处理以及其它任何处理,包括厨房洗碗槽.(估计是指数据清洗) 468 | 469 | 让我们看一个代码例子,这个例子中的代码拦截响应,并对响应数据做了轻微的数据转换. 470 | 471 | // register the interceptor as a service 472 | myModule.factory('myInterceptor', function($q, notifyService, errorLog) { 473 | return function(promise) { 474 | return promise.then(function(response) { 475 | // Do nothing 476 | return response; 477 | }, function(response) { 478 | // My notify service updates the UI with the error message 479 | notifyService(response); 480 | // Also log it in the console for debug purposes 481 | errorLog(response); 482 | return $q.reject(response); 483 | }); 484 | } 485 | }); 486 | 487 | // Ensure that the interceptor we created is part of the interceptor chain 488 | $httpProvider.responseInterceptors.push('myInterceptor'); 489 | 490 | ##安全方面的考虑 491 | 492 | 目前我们开发Web应用的时候,安全是一个非常重要的关注点,在我们的考虑维度直中,它必须作为首位被考虑.AngularJS给我们提供了一些帮助,同时也带来了两个安全攻击的角度,下面这一节我们将会讲解这些内容. 493 | 494 | ###JSON的安全脆弱性 495 | 496 | 当我们对服务器发送一个请求JSON数组数据的GET请求时(特别是当这些数据是敏感数据且需要登录验证或读取授权时),就会有一个不易察觉的JSON安全漏洞被暴露出来. 497 | 498 | 当我们使用一个恶意站点时,站点可能会用\标签发起同样的请求而得到相同的信息.因为你仍旧是登录状态,恶意站点利用了你的验证信息而请求了JSON数据,并且得到了它. 499 | 500 | 你或许惊奇是如何做到的,因为信息仍旧在你客户端,服务器也得不到这个数组信息的引用.并且通常作为请求脚本返回响应JSO对象会导致一个执行错误,虽然数组是个列外. 501 | 502 | 但是漏洞真正的切入点是:在JavaScript里,你是可以对内建对象做重定义的.在这个漏洞里面,数组的构造函数可以被重定义,通过这种重定义,恶意站点脚本就可以得到对响应数据的引用,然后就可以把响应数据发回它自己的服务器喽. 503 | 504 | 有两种方法可以防止这个漏洞:一是通常要确保敏感数据信息只作为POST请求的响应被返回,二是返回一个不合法的JSON表达式,然后客户端用约定好的逻辑把不合法数据转换为可用的真实数据. 505 | 506 | AngulaJS中你可以两种方法都用来阻止这个漏洞.在你的应用中,你可以而且应该选择敏感JSON信息只通过POST请求来获取. 507 | 508 | 进一步,你可以在服务器端给JSON响应数据配置一个前缀字符串: 509 | 510 | ")]}`,\n" 511 | 512 | 那么一个正常JSON响应比如: 513 | 514 | ['one','two'] 515 | 516 | 通过前缀字符串设置,这个JSON响应就会变为 517 | 518 | ")]}'", 519 | ['one', 'two'] 520 | 521 | AngularJS将会自动的把前缀字符串过滤掉,然后仅仅处理真实JSON数据. 522 | 523 | ###跨站请求伪造(XSRF) 524 | 525 | 跨站请求伪造攻击主要有以下特征: 526 | 527 | + 它们影响的站点通常依赖于授权或者用户认证. 528 | + 它们往往利用漏洞站点保存登录或者授权信息这个事实. 529 | + 它们发起以假乱真的HTTP或者XMLHTTPRequest请求来制造副作用,这种副作用通常是有害的. 530 | 531 | 考虑依稀下面这个跨站请求伪造攻击的案例: 532 | 533 | + 用户A登录进他的银行帐号(http://www.examplebank.com/) 534 | + 用户B意识到这点,然后诱导用户A访问用户B的个人主页 535 | + 主页上有一个特殊手工生成的图片连接地址,这个图片的的指向地址将会导致一次跨站请求伪造攻击,比如如下代码: 536 | `` 537 | 538 | 如果用户A的银行站点把授权信息保存在cookie里,且Cookie还没过期.当用户A打开用户B的站点时,就会导致非授权的用户A给用户B转账行为. 539 | 540 | 那么AngularJS是怎么帮助你防止这种事情发生?它提供一种双步机制来防止跨站请求伪造攻击. 541 | 542 | 在客户端,当发起XHR异步请求时,$http服务会从一个叫XSRF-TOKEN的cookie中读取令牌值,然后把它设置成X-XSRF-TOKEN头信息的值,因为只有你自己域的请求才能读取和设置这个令牌,你可以保证XHR请求只来自你自己的域. 543 | 544 | 同时,服务器端代码也需要一点轻微的修改,以便于你收到你的第一个HTTP GET请求时就设置一个可读取的对话Cookie,这个对话Cookie键叫XSRF-TOKEN。后续客户端发往服务器的请求就可以通过对比请求头信息的令牌值和之前第一个请求设置的Cookie令牌值来达到验证的目的.当然,令牌必须是一个用户一个唯一的令牌值.这个令牌值必须在服务器端验证(以防止恶意脚本捏造假令牌). 545 | -------------------------------------------------------------------------------- /Chapter6.markdown: -------------------------------------------------------------------------------- 1 | #指令 2 | 3 | 对于指令, 你可以扩展HTML来以添加声明性语法来做任何你喜欢做的事情. 通过这样做, 你可以替换一些特定于你的应用程序的通用的\s和\s元素和属性的实际意义. 它们都带有Angular提供的基础功能, 但是你可以创建特定于应用程序的你自己想做的事情. 4 | 5 | 首先我们要复习以下指令API以及它在Angular启动和运行生命周期里是如何运作的. 从那里, 我们将使用这些只是来创建一个指令类型. 在本将完成时我们将学习到如何编写指令的单元测试和使它们运行得更快. 6 | 7 | 但是首先, 我们来看看一些使用指令的语法说明. 8 | 9 | ## 目录 10 | 11 | - [指令和HTML验证](#指令和html验证) 12 | - [API预览](#api预览) 13 | - [为你的指令命名](#为你的指令命名) 14 | - [指令定义对象](#指令定义对象) 15 | - [编译和链接功能](#编译和链接功能) 16 | - [作用域](#作用域) 17 | - [操作DOM元素](#操作dom元素) 18 | - [控制器](#控制器) 19 | - [小结](#小结) 20 | 21 | ##指令和HTML验证 22 | 23 | 在本书中, 我们已经使用了Angular内置指令的`ng-directive-name`语法. 例如`ng-repeat`, `ng-view`和`ng-controller`. 这里, `ng`部分是Angular的命名空间, 并且dash之后的部分便是指令的名称. 24 | 25 | 虽然我们喜欢这个方便输入的语法, 但是在大部分的HTML验证机制中它不是有效的. 为了支持这些, Angular指令允许你以几种方式调用任意的指令. 以下在表6-1中列出的语法, 都是等价的并能够让你偏爱的[首选的]验证器正常工作 26 | 27 | Table 6-1 HTML Validation Schemes 28 | ```html 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 |
ValidatorFormatExample
nonenamespace-nameng-repeat=item in items
XMLnamespace:nameng:repeat=item in items
HTML5data-namespace-namedata-ng-repeat=item in items
xHTMLx-namespace-namex-ng-repeat=item in items
60 | ``` 61 | 由于你可以使用任意的这些形式, [AngularJS文档](http://docs.angularjs.org/)中列出了一个驼峰式的指令, 而不是任何这些选项. 例如, 在`ngRepeat`标题下你可以找到`ng-repeat`. 稍后你会看到, 在你定义你自己的指令时你将会使用这种命名格式. 62 | 63 | 如果你不适用HTML验证器(大多数人都不使用), 你可以很好的使用在目前你所见过的例子中的命名空间-指令[namespace-directive]语法 64 | 65 | ##API预览 66 | 67 | 下面是一个创建任意指令伪代码模板 68 | ```js 69 | var myModule = angular.module(...); 70 | 71 | myModule.directive('namespaceDirectiveName', function factory(injectables) { 72 | var directiveDefinitionObject = { 73 | restrict: string, 74 | priority: number, 75 | template: string, 76 | templateUrl: string, 77 | replace: bool, 78 | transclude: bool, 79 | scope: bool or object, 80 | controller: function controllerConstructor($scope, $element, $attrs, $transclude){...}, 81 | require: string, 82 | link: function postLink(scope, iElement, iAttrs) {...}, 83 | compile: function compile(tElement, tAttrs, transclude){ 84 | return: { 85 | pre: function preLink(scope, iElement, iAttrs, controller){...}, 86 | post: function postLink(scope, iElement, iAttrs, controller){...} 87 | } 88 | } 89 | }; 90 | return directiveDefinitionObject; 91 | }); 92 | ``` 93 | 有些选项是互相排斥的, 它们大多数都是可选的, 并且它们都有有价值的详细说明: 94 | 95 | 当你使用每个选项时, 表6-2提供了一个概述. 96 | 97 | Table 6-2 指令定义选项 98 | ```html 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 |
PropertyPurpose
restrict声明指令可以作为一个元素, 属性, 类, 注释或者任意的组合如何用于模板中
priority设置模板中相对于其他元素上指令的执行顺序
template指令一个作为字符串的内联模板. 如果你指定一个模板URL就不要使用这个模板属性.
templateUrl指定通过URL加载的模板. 如果你指定了字符串的内联模板就不需要使用这个.
replace如果为true, 则替换当前元素. 如果为false或者未指定, 则将这个指令追加到当前元素上.
transclude让你将一个指令的原始自节点移动到心模板位置内.
scope为这个指令创建一个新的作用域而不是继承父作用域.
controller为跨指令通信创建一个发布的API.
require需要其他指令服务于这个指令来正确的发挥作用.
link以编程的方式修改生成的DOM元素实例, 添加事件监听器, 设置数据绑定.
compile以编程的方式修改一个指令的DOM模板的副本特性, 如同使用`ng-repeat`时. 你的编译函数也可以返回链接函数来修改生成元素的实例.
153 | ``` 154 | 下面让我们深入细节来看看. 155 | 156 | ###为你的指令命名 157 | 158 | 你可以用模块的指令函数为你的指令创建一个名称, 如下所示: 159 | ```js 160 | myModule.directive('directiveName', function factory(injectables){...}); 161 | ``` 162 | 虽然你可以使用任何你喜欢的名字命名你的指令, 该符号会选择一个前缀命名空间标识你的指令, 同时避免与可能包含在你的项目中的外部指令冲突. 163 | 164 | 你当然不希望它们使用一个`ng-`前缀, 因为这可能与Angular自带的指令相冲突. 如果你从事于SuperDuper MegaCorp, 你可以选择一个super-, superduper-, 或者甚至是superduper-megacorp-, 虽然你可能选择第一个选项, 只是为了方便输入. 165 | 166 | 正如前面所描述的, Angular使用一个标准化的指令命名机制, 并且试图有效的在模板中使用驼峰式的指令命名方式来确保在5个不同的友好的验证器中正常工作. 例如, 如果你已经选择了`super-`作为你的前缀, 并且你在编写一个日期选择(datepicker)组件, 你可能将它命名为`superDatePicker`. 在模板中, 你可以像这样来使用它: `super-date-picker`, `super:date-picker`, `data-super-date-picker`或者其他多样的形式. 167 | 168 | ###指令定义对象 169 | 170 | 正如前面提到的, 在指令定义中大多数的选项都是可选的. 实际上, 这里并没有硬性的要求必须选择哪些选项, 并且你可以构造出许多有利于指令的子集参数. 让我们来逐步讨论这些选项是做什么的. 171 | 172 | ####restrict 173 | 174 | `restrict`属性允许你指定你的指令声明风格--也就是说, 它是否能够用于作为元素名称, 属性, 类[className], 或者注释. 你可以根据表6-3来指定一个或多个声明风格, 只需要使用一个字符串来表示其中的每一中风格: 175 | 176 | Table 6-3 指令声明用法选项 177 | ```html 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 |
CharacterDeclaration styleExample
Eelement<my-menu title=Products></my-menu>
Aattribute<div my-menu=Products></div>
Cclass<div class=my-menu:Products></div>
Mcomment<!--directive:my-menu Products-->
209 | ``` 210 | 如果你希望你的指令用作一个元素或者一个属性, 那么你应该传递`EA`作为`restrict`字符串. 211 | 212 | 如果你忽略了`restrict`属性, 则默认为`A`, 并且你的指令只能用作一个属性(属性指令). 213 | 214 | 如果你计划支持IE8, 那么基于attribute-和class-的指令就是你最好的选择, 因为它需要额外的努力来使新元素正常工作. 可以查看Angular文档来详细了解这一点. 215 | 216 | ####Priorities 217 | 218 | 在你有多个指令绑定在一个单独的DOM元素并要确定它们的应用顺序的情况下, 你可以使用`priority`属性来指定应用的顺序. 数值高的首先运行. 如果你没有指定, 则默认的priority为0. 219 | 220 | 很难发生需要设置优先级的情况. 一个需要设置优先级例子是`ng-repeat`指令. 当重复元素时, 我们希望Angular在应用指令之前床在一个模板元素的副本. 如果不这么做, 其他的指令将会应用到标准的模板元素上而不是我们所希望在应用程序中重复我们的元素. 221 | 222 | 虽然它(proority)不在文档中, 但是你可以搜索Angular资源中少数几个使用`priority`的其他指令. 对于`ng-repeat`, 我们使用优先级值为1000, 这样就有足够的优先级处理优先处理它. 223 | 224 | ####Templates 225 | 226 | 当创建组件, 挂件, 控制器一起其他东西时, Angular允许你提供一个模板替换或者包裹元素的内容. 例如, 如果你在视图中创建一组tab选项卡, 可能会呈现出如图6-1所示视图. 227 | 228 | ![tab](figure/tab.png) 229 | 230 | 图6-1 tab选项卡视图 231 | 232 | 并不是一堆\, \\和\元素, 你可以创建一个\和\指令, 用来声明每个单独的tab选项卡的结构. 然后你的HTML可以做的更好来表达你的模板意图. 最终结果可能看起来像这样: 233 | ```html 234 | 235 | 236 |

Welcome home!

237 |
238 | 239 | 240 | 241 |
242 | ``` 243 | 你还可以给title绑定一个字符串数据, 通过在\或者\上绑定控制器处理tab选项内容. 它不仅限于用在tabs上--你还可以用于菜单, 手风琴, 弹窗, dialog对话框或者其他任何你希望以这种方式实现的地方. 244 | 245 | 你可以通过`template`或者`templateUrl`属性来指定替换的DOM元素. 使用`template`通过字符串来设置模板内容, 或者使用`templateUrl`来从服务器的一个文件上来加载模板. 正如你在接下来的例子中会看到, 你可以预先缓存这些模板来减少GET请求, 这有利于提高应用的性能. 246 | 247 | 让我们来编写一个dumb指令: 一个\元素, 只是用于使用\Hi there\来替换自身. 在这里, 我们将设置`restrict`来允许元素和设置`template`显示我们所希望的东西. 由于默认的行为只将内容追加到元素中, 因此我们将设置`replace`属性为true来替换原来的模板: 248 | ```js 249 | var appModule = angular.module('app', []); 250 | appModule.directive('hello', function(){ 251 | return { 252 | restrict: 'E', 253 | template: '
Hi there
', 254 | replace: true 255 | }; 256 | }); 257 | ``` 258 | 在页面中我们可以像这样使用它: 259 | ```html 260 | 261 | ... 262 | 263 | 264 | 265 | ... 266 | ``` 267 | 将它载入到浏览器中, 我们会看到"Hi there". 268 | 269 | 如果你查看页面的源代码, 在页面上你仍然会看到\\, 但是如果你查看生成的源代码(在Chrome中, 你可以在"Hi there"上右击然后选择审查元素), 你会看到: 270 | ```html 271 | 272 |
Hi there
273 | 274 | ``` 275 | \\被模板中的\替换了. 276 | 277 | 如果你从指令定义中移除`replace: true`, 那么你会看到\\Hi there\\. 278 | 279 | 通常你会希望使用`templateUrl`而不是`template`, 因为输入HTML字符串并不是那么有趣. `template`属性通常有利于非常小的模板. 使用templateUrl`同样非常有用, 可以设置适当的头来使模板可缓存. 我们可以像下面这样重写我们的`hello`指令: 280 | ```js 281 | var appModule = angular.module('app', []); 282 | appModule.directive('hello', function(){ 283 | return { 284 | restrict: 'E', 285 | templateUrl: 'helloTemplate.html', 286 | replace: true 287 | }; 288 | }); 289 | ``` 290 | 在`helloTemplate.html`中, 你只需要输入: 291 | ```html 292 |
Hi there
293 | ``` 294 | 如果你使用Chrome浏览器, 它的"同源策略"会组织Chrome从`file://`中加载这些模板, 并且你会得到一个类似"Origin null is not allowed by Access-Control-Allow-Origin."的错误. 那么在这里, 你有两个选择: 295 | 296 | + 通过服务器来加载应用 297 | + 在Chrome中设置一个标志. 你可以通过在命令行中使用`chrome --allow-file-access-from-files`命令来运行Chrome做到这一点. 298 | 299 | 这将会通过`templateUrl`加载这些文件, 然而, 这会让你的用户要等待到指令加载. 如果你希望在首页加载模板, 你可以在一个`script`标签中将它作为这个页面的一部分包含进来, 就像这样: 300 | ```html 301 | 304 | ``` 305 | 这里的id属性很重要, 因为这是Angular用来存储模板的URL键. 稍候你将会使用这个id在指令的`templateUrl`中指定要插入的模板. 306 | 307 | 这个版本能够很好的载入而不需要服务器, 因为没有必要的`XMLHttpRequest`来获取内容. 308 | 309 | 最后, 你可以越过`$http`或者以其他机制来加载你自己的模板, 然后将它们直接设置在Angular中称为`$templateCache`的对象上. 我们希望在指令运行之前缓存中的这个模板可用, 因此我们将通过module上的run函数来调用它. 310 | ```js 311 | var appModule = angular.module('app', []); 312 | 313 | appModule.run(function($templateCache){ 314 | $templateCache.put('helloTemplateCached.html', '
Hi there
'); 315 | }); 316 | 317 | appModule.directive('hello', function(){ 318 | return { 319 | restrict: 'E', 320 | templateUrl: 'helloTemplateCached.html', 321 | replace: true; 322 | }; 323 | }); 324 | ``` 325 | 你可能希望在产品中这么做, 仅仅作为一个减少所需的GET请求数量的技术. 你可以运行一个脚本将所有的模板合并到一个单独的文件中, 并在一个新的模块中加载它, 然后你就可以从你的主应用程序模块中引用它. 326 | 327 | ####Transclusion 328 | 329 | 除了替换或者追加内容, 你还可以通过`transclude`属性将原来的内容移到新模板中. 当设置为true时, 指令将删除原来的内容, 但是在你的模板中通过一个名为`ng-transclude`的指令重新插入来使它可用. 330 | 331 | 我们可以使用transclusion来改变我们的示例: 332 | ```js 333 | appModule.directive('hello', function() { 334 | return { 335 | template: '
Hi there
', 336 | transclude: true 337 | }; 338 | }); 339 | ``` 340 | 像这样来应用它: 341 | ```html 342 |
Bob
343 | ``` 344 | 你会看到: "Hi there Bob." 345 | 346 | ###编译和链接功能 347 | 348 | 虽然插入模板是有用的, 任何指令真正有趣的工作发生在它的`compile`和它的`link`函数中. 349 | 350 | `compile`和`link`函数被指定为Angular用来创建应用程序实际视图的后两个阶段. 让我们从更高层次来看看Angular的初始化过程, 按一定的顺序: 351 | 352 | **Script loads** 353 | 354 | >Angular加载和查找`ng-app`指令来判定应用程序界限. 355 | 356 | **Compile phase(阶段)** 357 | 358 | >在这个阶段, Angular会遍历DOM节点以确定所有注册在模板中的指令. 对于每一个指令, >然后基于指令的规则(`template`,`replace`,`transclude`等等)转换DOM, 并且如果它存在就调用`compile`函数. >它的返回结果是一个编译过的`template`函数, 这将从所有的指令中调用`link`函数来收集. 359 | 360 | **Link phase(阶段)** 361 | 362 | >创建动态的视图, 然后Angular会对每个指令运行一个`link`函数. `link`函数通常在DOM或者模型上创建监听器. >这些监听器用于视图和模型在所有的时间里都保持同步. 363 | 364 | 因此我们必须在编译阶段处理模板的转换, 同时在链接阶段处理在视图中修改数据. 按照这个思路, 指令中的`compile`和`link`函数之间主要的区别是`compile`函数处理模板自身的转换, 而`link`函数处理在模型和视图之间创造一个动态的连接. 作用域挂接到编译过的`link`函数正是在这个第二阶段, 并且通过数据绑定将指令变成活动的. 365 | 366 | 出于性能的考虑, 这两个阶段才分开的. `compile`函数仅在编译阶段执行一次, 而`link`函数会被执行多次, 对每个指令实例. 例如, 让我们来说说你上面使用的`ng-repeat`指令. 你并不想用`compile`, 这会导致在每次`ng-repeat`重复时都产生一个DOM遍历的操作. 相反, 你会希望一次编译, 然后链接. 367 | 368 | 虽然你毫无疑问的应该学习编译和链接之间的不同, 以及每个功能, 你需要编写的大部分的指令都不需要转换模板; 你还会编写大部分的链接函数. 369 | 370 | 让我们再看看每个语法来比较一下, 我们有: 371 | ```js 372 | compile: function compile(tElement, tAttrs, transclude) { 373 | return { 374 | pre: function preLink(scope, iElement, iAttrs, controller) {...}, 375 | post: function postLink(scope, iElement, iAttrs, controller) {...} 376 | } 377 | } 378 | ``` 379 | 以及链接: 380 | ```js 381 | link: function postLink(scope, iElement, iAttrs) {...} 382 | ``` 383 | 注意这里有一点不同的是`link`函数获得了一个作用域的访问, 而`compile`没有. 这是因为在编译阶段期间, 作用域并不存在. 然而你有能力从`compile`函数返回`link`函数. 这些`link`函数能够访问到作用域. 384 | 385 | 还要注意的是`compile`和`link`都会获得一个到它们对应的DOM元素和这些元素属性[attributes]列表的引用. 这里的一点区别是`compile`函数是从模板中获得模板元素和属性, 并且会获取到`t`前缀. 而`link`函数使用模板创建的视图实例中获得它们的, 它们会获取到`i`前缀. 386 | 387 | 这种区别只存在于当指令位于其他指令中制造模板副本的时候. `ng-repeat`就是一个很好的例子. 388 | ```html 389 |
390 | 391 |
392 | ``` 393 | 这里, `compile`函数将只被调用一次, 而`link`函数在每次复制`my-widget`时都会被调用一次--等价于元素在things中的数量. 因此, 如果`my-widget`需要到所有`my-widget`副本(实例)中修改一些公共的东西, 为了提升效率, 正确的做法是在`compile`函数中处理. 394 | 395 | 你可能还会注意到`compile`函数好哦的了一个`transclude`属性函数. 这里, 你还有机会以编写一个函数以编程的方式transcludes内容, 对于简单的的基于模板不足以transclusion的情况. 396 | 397 | 最后, `compile`可以返回一个`preLink`和`postLink`函数, 而`link`仅仅指向一个`postLink`函数. `preLink`, 正如它的名字所暗示的, 它运行在编译阶段之后, 但是会在指令链接到子元素之前. 同样的, `postLink`会运行在所有的子元素指令被链接之后. 这意味着如果你需要改变DOM结构, 你将在`posyLink`中处理. 在`preLink`中处理将会混淆流程并导致一个错误. 398 | 399 | ###作用域 400 | 401 | 你会经常希望从指令中访问作用域来监控模型的值并在它们改变时更新UI, 同时在外部时间造成模型改变时通知Angular. 者时最常见的, 当你从jQuery, Closure或者其他库中包裹一些非Angular组件或者实现简单的DOM事件时. 然后将Angular表达式作为属性传递到你的指令中来执行. 402 | 403 | 这也是你期望使用一个作用域的原因之一, 你可以获得三种类型的作用域选项: 404 | 405 | 1. 从指令的DOM元素中获得**现有的作用域**. 406 | 2. 创建一个**新作用域**, 它继承自你闭合的控制器作用域. 这里, 你见过能够访问树上层作用域中的所有值. 这个作用域将会请求这种作用域与你DOM元素中其他任意指令共享它并被用于与它们通信. 407 | 3. 从它的父层**隔离出来的作用域**不带有模型属性. 当你在创建可重用的组件而需要从父作用域中隔离指令操作时, 你将会希望使用这个选项. 408 | 409 | 你可以使用下面的语法来创建这些作用域类型的配置: 410 | ```html 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 |
Scope TypeSyntax
existing scopescope: false(如果不指定将使用这个默认值) 422 |
new scopescope: true
isolate scopescope: { /* attribute names and binding style */ }
433 | ``` 434 | 当你创建一个隔离的作用域时, 默认情况下你不需要访问父作用域中模型中的任何东西. 然而, 你也可以指定你想要的特定属性传递到你的指令中. 你可以认为是吧这些属性名作为参数传递给函数的. 435 | 436 | 注意, 虽然隔离的作用域不就成模型属性, 但它们仍然是其副作用域的成员. 就像所有其他作用域一样, 它们都有一个`$parent`属性引用到它们的父级. 437 | 438 | 你可以通过传递一个指令属性名的映射的方式从父作用域传递特定的属性到隔离的作用域中. 这里有三种合适的方式从父作用域中传递数据. 我们称这些传递数据不同的方式为"绑定策略". 你也可以可选的指定一个局部别名给属性名称. 439 | 440 | 以下是没有别名的语法: 441 | ```js 442 | scope: { 443 | attributeName1: 'BINDING_STRATEGY', 444 | attributeName2: 'BINDING_STRATEGY',... 445 | } 446 | ``` 447 | 以下是使用别名的方式: 448 | ```js 449 | scope: { 450 | attributeAlias: 'BINDING_STRATEGY' + 'templateAttributeName',... 451 | } 452 | ``` 453 | 绑定策略被定义为表6-4中的符号: 454 | 455 | 表6-4 绑定策略 456 | ```html 457 | 458 | 459 | 460 | 461 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 |
SymbolMeaning 462 |
@将属性作为字符串传递. 你也可以通过在属性值中使用插值符号{{}}来从闭合的作用域中绑定数据值.
=使用你的指令的副作用域中的一个属性绑定数据到属性中.
&从父作用域中传递到一个函数中, 以后调用.
479 | ``` 480 | 这些都是相当抽象的概念, 因此让我们来看一个具体的例子上的变化来进行说明. 比方说我们希望创建一个`expander`指令在标题栏被点击时显示额外的内容. 481 | 482 | 收缩时它看起来如图6-2所示. 483 | 484 | ![6-2](figure/6-2.png) 485 | 486 | 图6-2 Expander in closed state 487 | 488 | 展开时它看起来如图6-3所示. 489 | 490 | ![6-3](figure/6-3.png) 491 | 492 | 图6-3 Expander in open state 493 | 494 | 我们会编写如下代码: 495 | ```html 496 |
497 | 498 | {{text}} 499 | 500 |
501 | ``` 502 | 标题(Cliked me to expand)和文本(Hi there folks...)的值来自于闭合的作用域中. 我们可以像下面这样来设置一个控制器: 503 | ```js 504 | function SomeController($scope) { 505 | $scope.title = 'Clicked me to expand'; 506 | $scope.text = 'Hi there folks, I am the content that was hidden but is now shown.'; 507 | } 508 | ``` 509 | 然后我们可以来编写指令: 510 | ```js 511 | angular.module('expanderModule', []) 512 | .directive('expander', function(){ 513 | return { 514 | restrict: 'EA', 515 | replace: true, 516 | transclude: true, 517 | scope: { title: '=expanderTitle'}, 518 | template: '
' + 519 | '
{{title}}
' + 520 | '
' + 521 | '
', 522 | link: function(scope, element, attris){ 523 | scope.showMe = false; 524 | scope.toggle = function toggle(){ 525 | scope.showMe = !scope.showMe; 526 | } 527 | } 528 | } 529 | }); 530 | ``` 531 | 然后编写下面的样式: 532 | ```css 533 | .expander { 534 | border: 1px solid black; 535 | width: 250px; 536 | } 537 | .expander > .title { 538 | background-color: black; 539 | color: white; 540 | padding: .1em .3em; 541 | cursor: pointer; 542 | } 543 | .expander > .body { 544 | padding: .1em .3em; 545 | } 546 | ``` 547 | 接下来让我们来看看指令中的每个选项是做什么的, 在表6-5中. 548 | 549 | 表6-5 Functions of elements 550 | ```html 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 |
FunctionNameDescription
restrict: EA一个元素或者属性都可以调用这个指令. 也就是说, \...\与\
...\是等价
replace:true使用我们提供的模板替换原始元素
transclude:true将原始元素的内容移动到我们所提供的模板的另外一个位置.
scope: {title: =expanderTitle}创建一个称为`title`的局部作用域, 将父作用域的属性数据绑定到声明的`expanderTitle`属性中. 这里, 我们重命名title为更方便的expanderTitle. 我们可以编写`scope: { expanderTitle: '='}`, 那么在模板中我们就要使用`expanderTitle`了. 但是在其他指令也有一个`title`属性的情况下, 在API中消除title的歧义和只是重命名它用于在局部使用是有意义的. 请注意, 这里自定义指令也使用了相同的驼峰式命名方式作为指令名.
template: \<'div'\>+声明这个指令要插入的模板. 注意我们使用了`ng-click`和`ng-show`来显示和隐藏自身并使用`ng-transclude`声明了原始内容会去哪里. 还要注意的是transcluded的内容能够访问父作用域, 而不是指令闭合中的作用域.
link...设置`showMe`模型来检测expander的展开/关闭状态, 同时定义在用于点击`title`这个div的时候调用定义的`toggle()`函数.
585 | ``` 586 | 如果我们像使用更多有意义的东西来在模板中定义`expander title`而不是在模型中, 我们还可以使用传递通过在作用域声明中使用`@`符号传递一个字符串风格的属性, 就像下面这样: 587 | ```js 588 | scope: { title: '@expanderTitle'}, 589 | ``` 590 | 在模板中我们就可以实现相同的效果: 591 | ```html 592 | 593 | {{text}} 594 | 595 | ``` 596 | 注意, 对于@策略我们仍然可以通过使用插入法将title数据绑定到我们的控制器作用域中: 597 | ```html 598 | 599 | {{text}} 600 | 601 | ``` 602 | ###操作DOM元素 603 | 604 | 传递给指令的`link`和`compile`函数的`iElement`和`tElement`是包裹原生DOM元素的引用. 如果你已经加载了jQuery库, 你也可以使用你已经习惯使用的jQuery元素. 605 | 606 | 如果你没有使用jQuery, 你也可以使用Angular内置的被称为jqLite的包装器. 它提供了一个jQuery的子集便于我们在Angular中创建任何东西. 对于多数应用程序, 你都可以单独使用这些API做任何你需要做的事情. 607 | 608 | 如果你需要直接访问原生的DOM元素你可以通过使用`element[0]`访问对象的第一个元素来获得它. 609 | 610 | 你可以在Angular文档的`angular.element()`查看它所支持的API的完整列表--你可以用这个函数创建你自己的jqLite包装的DOM元素. 它包含像`addClass()`, `bind()`, `find()`, `toggleClass()`等等其他方法. 其次, 其中大多数有用的核心方法都来自于jQuery, 但是它的代码亮更少. 611 | 612 | 对于其他的jQuery API, 元素在Angular中都有指定的函数. 这些都是存在的, 无论你是否使用完整的jQuery库. 613 | 614 | Table 6-6. Angular specific functions on an element 615 | ```html 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 |
FunctionDescription
controller(name)当你需要直接与一个控制器通信时, 这个函数会返回附加到元素上的控制器. 如果没有现有的元素, 它会遍历DOM元素并查找最近的父控制器. name参数是可选的, 它是用于指定相同元素上其他指令名称的. 如果提供这个参数, 它会从相应的指令中返回控制器. 这个名字应该与所有指令一样使用一个驼峰式的格式. 也就是说, 使用`ngModle`来替换`ng-model`的形式.
injector()获取当前元素或者它的父元素的注入器. 它还允许你访问在这些元素上定义的所依赖的模块.
scope()返回当前元素或者它最近的父元素的作用域.
inheritedData()正如jQuery的`data()`函数, `inheritedData()`会在一个封闭的方式中设置和获取数据. 此外还能够从当前元素获取数据, 它也会遍历DOM元素并查找值.
642 | ``` 643 | 这里有一个例子, 让我们重新定义之前的expander例子而不使用`ng-show`和`ng-click`. 它看起来像下面这样: 644 | ```js 645 | angular.module('expanderModule', []) 646 | .directive('expander', function(){ 647 | return { 648 | restrict: 'EA', 649 | replace: true, 650 | transclude: true, 651 | scope: { title: '=expanderTitle' }, 652 | template: '
' + 653 | '
{{title}}
' + 654 | '
' + 655 | '
', 656 | 657 | link: function(scope, element, attrs) { 658 | var titleElement = angular.element(element.children().eq(0)); 659 | var bodyElement = angular.element(element.children().eq(1)); 660 | 661 | titleElement.bind('click', toggle); 662 | 663 | function toggle() { 664 | bodyElement.toggleClass('closed'); 665 | } 666 | } 667 | } 668 | }); 669 | ``` 670 | 这里我们从模板中移除了`ng-click`和`ng-show`. 相反的时, 当用户单击expander的title时执行所定义的行为, 我们将从title元素创建一个jqLite元素, 然后它绑定一个click事件并将`toggle()`函数作为它的回调函数. 在`toggle()`函数中, 我们在expander的body元素上调用`toggleClass()`来添加或者移除一个被称为`closed`的class(HTML类名), 这里我们给元素设置了一个值为`display: none`的类, 像下面这样: 671 | ```css 672 | .closed { 673 | display: none; 674 | } 675 | ``` 676 | 677 | ###控制器 678 | 679 | 当你有相互嵌套的指令需要相互通信时, 你可以通过控制器做到这一点. 比如一个\可能需要知道它自身内部的\元素它才能适当的显示或者隐藏它们. 同样的对于一个\也需要知道它的\元素, 或者一个\要知道它的\元素. 680 | 681 | 正如前面所展示的, 创建一个API用于在指令之间沟通, 你可以使用控制器属性的语法声明一个控制器作为一个指令的一部分: 682 | 683 | controller: function controllerConstructor($scope, $element, $attrs, $transclude) 684 | 685 | 这个控制器函数就是依赖注入, 因此这里列出的参数都是潜在的可用并且全部都是可选的--它们可以按照任意顺序列出. 它们也仅仅只是可用服务的一个子集. 686 | 687 | 其他的指令也可以使用`require`属性语法将这个控制器传递给它们. 完整的`require`的形式看起来像: 688 | 689 | require: '^?directiveName' 690 | 691 | 关于`require`字符串参数的说明可以在表6-7中找到. 692 | 693 | Table 6-7. Options for required controllers 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 |
OptionUsage
directiveName这个指令驼峰式命名规范应该是来自于控制器. 因此如果我们的\s指令需要在它的父元素\上找到一个控制器, 我们需要将它编写为`myMenu`.
^默认情况下, Angular会从同一元素的命名指令中获取控制器. 加入可选的^符号表示总是遍历DOM树来以查找指令. 对于\示例, 我们需要添加这个符号; 最终的字符就是`\^myMenu`.
?如果你所需要的控制器没有找到, Angular将抛出一个异常信息来告诉你遇到了什么问题. 添加一个?符号给字符串就是说这个控制器时可选的并且如果没有找到控制器它不应该抛出一个异常. 虽然者听起来不可能, 但是如果我们希望让\不需要使用一个\容器, 我们可以将这个添加给最终所需要的字符串?\^myMenu.
717 | 718 | 例如, 让我们重写我们的expander指令用于一组称为"手风琴"的组件, 它可以确保当你打开一个expander时, 其他的都会自动关闭. 它看起来如图6-4所示. 719 | 720 | ![6-4](figure/accordion.png) 721 | 722 | 图 6-4. Accordion component in multiple states 723 | 724 | 首先, 让我们编写处理手风琴菜单的accordion指令. 这里我们将添加我们的控制器构造器方法来处理手风琴: 725 | 726 | appModule.directive('accordion', function() { 727 | return { 728 | restrict: 'EA', 729 | replace: true, 730 | transclude: true, 731 | template: '
', 732 | controller: function() { 733 | var expanders = []; 734 | this.gotOpened = function(selectedExpander) { 735 | angular.forEach(expanders, function(expander){ 736 | if(selectedExpander != expander) { 737 | expander.showMe = false; 738 | } 739 | }); 740 | } 741 | 742 | this.addExpander = function(expander) { 743 | expanders.push(expander); 744 | } 745 | } 746 | } 747 | }); 748 | 749 | 这里我们定义了一个`addExpander()`函数给expanders便于调用它来注册自身实例. 我们也创建了一个`gotOpened()`函数给expanders便于调用, 因而让accordion的控制器可以知道它能够去关闭任何其他展开的expanders. 750 | 751 | 在expander指令自身中, 我们将从它的父元素扩展它所需要的accordion控制器并在适当的时间里调用`addExpander()`和`gotOpened()`方法. 752 | 753 | appModule.directive('expander', function(){ 754 | return { 755 | restrict: 'EA', 756 | replace: true, 757 | transclude: true, 758 | require: '^?accordion', 759 | scope: { title: '=expanderTitle' } 760 | template: '
' + 761 | '
{{title}}
' + 762 | '
' + 763 | '
', 764 | link: function(scope, element, attrs, accordionController) { 765 | scope.showMe = false; 766 | accordionController.addExpander(scope); 767 | 768 | scope.toggle = function toggle() { 769 | scope.showMe = !scope.showMe; 770 | accordionController.gotOpened(scope); 771 | } 772 | } 773 | } 774 | }); 775 | 776 | 注意在手风琴指令的控制器中我们创建了一个API, 通过它可以让expander可以相互通信. 777 | 778 | 然后我们可以编写一个模板来使用这些指令, 最后生成的结果整如图6-4所示. 779 | 780 | 781 | 782 | 783 | {{expander.text}} 784 | 785 | 786 | 787 | 788 | 当然接下是对应的控制器: 789 | 790 | function SomeController($scope){ 791 | $scope.expanders = [ 792 | {title: 'Click me to expand', 793 | text: 'Hi there folks, I am the content that was hidden but is now shown.'}, 794 | {title: 'Click this', 795 | text: 'I am even better text than you have seen previously'}, 796 | {title: 'No, click me!', 797 | text: 'I am text should be seen before seeing other texts'} 798 | ]; 799 | } 800 | 801 | ##小结 802 | 803 | 正如我们所看到的, 指令允许我们扩展HTML的语法并让很多应用程序按照我们声明的意思工作. 指令使重用(代码重用/组件复用)变得轻而易举--从使用`ng-model`和`ng-controller`配置你的应用程序, 到处理模板的任务的像`ng-repeat`和`ng-view`指令, 再到前几年被限制的可复用的组件像数据栅格, 饼图, 工具提示和选项卡等等. 804 | -------------------------------------------------------------------------------- /Chapter7.markdown: -------------------------------------------------------------------------------- 1 | # 其他关注点 2 | 3 | 在这一章中, 我们将看一切目前Angular所实现的其他有用的特性, 但是我们不会涵盖所有的或者深入的章节和例子. 4 | 5 | ## 目录 6 | 7 | - [$location](#location) 8 | - [HTML5模式和Hashbang模式](#html5模式和hashbang模式) 9 | - [AngularJS模块方法](#angularjs模块方法) 10 | - [主方法在哪?](#主方法在哪) 11 | - [加载和依赖](#加载和依赖) 12 | - [快捷方法](#快捷方法) 13 | - [$on, $emit和$broadcast之间的作用域通信](#on-emit和broadcast之间的作用域通信) 14 | - [Cookies](#cookies) 15 | - [国际化和本地化](#国际化和本地化) 16 | - [在AngularJS中我能做什么?](#在angularjs中我能做什么) 17 | - [如何获取所有工作?](#如何获取所有工作) 18 | - [常见问题](#常见问题) 19 | - [净化HTML和模块](#净化html和模块) 20 | - [Linky](#linky) 21 | 22 | ## $location 23 | 24 | 到现在为止, 你已经看到了不少使用AngularJS中的`$location`服务的例子. 它们大多数都只是短暂的一撇--在这里访问, 那里设置. 在这一小节, 我们将深入研究AngularJS中的`$location`服务是什么, 以及什么时候你应该使用它, 什么时候不应该使用它. 25 | 26 | `$location`服务是一个存在于任何浏览器中的`window.location`的包装器. 那么为什么你应该使用它而不是直接使用`window.location`呢? 27 | 28 | **不再使用全局状态** 29 | 30 | `window.location`是一个使用全局状态的很好的例子(实际上, 浏览器中的`window`和`document`对象都是很好的例子). 一旦你的应用程序中有全局的状态(通常我们都说全局变量), 它的测试, 维护和工作都会变得困难(即使不是现在, 从长远来看它肯定是一个潜在的隐患). `$location`服务隐藏了这个潜在的隐患(也就是我们所谓的全局状态), 并且允许你通过注入mocks到你的单元测试中来测试你的浏览器位置信息. 31 | 32 | **API** 33 | 34 | `window.location`让你能够完全访问浏览器位置信息的内容. 也就是说, `window.location`给你一个字符串而`$location`服务给你提供了更好的服务, 它提供了类似于jQuery的setters和getters让你能够使用它以一个干净的方式工作. 35 | 36 | **AngularJS集成** 37 | 38 | 如果你使用`$location`, 你可以在任何你希望使用的时候使用它. 但是如果直接使用`window.location`, 在有变化时你必须负责通知给AngularJS, 并且还要监听这些改变/变化. 39 | 40 | **HTML5集成** 41 | 42 | `$location`服务会在HTML5 APIs在浏览器中可用时智能的识别并使用它们. 如果它们不可用, 它会降级使用默认的用法. 43 | 44 | 那么什么时候你应该使用`$location`服务呢? 任何你想反应URL变化的时候(它并不是通过`$routes`来覆盖的, 而且你应该主要用于基于URL工作的视图中), 以及在浏览器中响应当前URL变化的时候使用. 45 | 46 | 让我们考虑使用一个小例子来看看你应该如何在一个实际的应用程序中使用`$location`服务. 想象一下我们有一个`datepicker`, 并且当我们选择一个日期时, 应用程序导航到某个URL. 让我们一起来看看它看起来可能是什么样子: 47 | 48 | // Assume that the datepicker calls $scope.dateSelected with the date 49 | $scope.dateSelected = function(dateTxt) { 50 | $location.path('filteredResults?startDate=' + dateTxt); 51 | // If this were being done in the callback for 52 | // an external library, like jQuery, then we would have to 53 | $scope.$apply(); 54 | }; 55 | 56 | ####用或者不用$apply? 57 | 58 | 对于AngularJS开发者来说什么时候调用`$scope.$apply()`, 什么时候不能调用它是比较混乱的. 互联网上的建议和谣言非常猖獗. 在本小节我们将让它变得非常清楚. 59 | 60 | 但是首先让我们先尝试以一个简单的形式使用`$apply`. 61 | 62 | `Scope.$apply`就像一个延迟的worker. 我们会告诉它有很多工作要做, 它负责响应并确保更新绑定和所有变化的视图效果. 但并不是所有的时间都只做这项工作, 它只会在它觉得有足够的工作要做时才会做. 在所有的其他情况下, 它只是点点头并标记在稍候处理. 它只是在你给它指示时并显示的告诉它处理实际的工作. AngularJS只是定期在它的声明周期内做这些, 但是如果调用来自于外部(比如说一个jQuery UI事件), `scope.$apply`只是做一个标记, 但并不会做任何事. 这就是为什么要调用`scope.$apply`来告诉它"嘿!你现在需要做这件事, 而不是等待!". 63 | 64 | 这里有四个快速的提示告诉你应该什么时候(以及如何)调用`$apply`. 65 | 66 | + **不要**始终调用它. 当AngularJS发现它将导致一个异常(在其`$digest`周期内, 我们调用它)时调用`$apply`. 因此"有备无患"并不是你希望使用的方法. 67 | 68 | + 当控制器在AngularJS外部(DOM时间, 外部回调函数如jQuery UI控制器等等)调用AngularJS函数时**调用**它. 对于这一点, 你希望告诉AngularJS来更新它自身(模型, 视图等等), 而`$apply`就是做这个的. 69 | 70 | + 只要可能, 通过传递给`$apply`来执行你的代码或者函数, 而不是执行函数, 然后调用`$apply()`. 例如, 执行下面的代码: 71 | 72 | $scope.$apply(function(){ 73 | $scope.variable1 = 'some value'; 74 | excuteSomeAction(); 75 | }); 76 | 77 | 而不是下面的代码: 78 | 79 | $scope.variable1 = 'some value'; 80 | excuteSomeAction(); 81 | $scope.$apply(); 82 | 83 | 尽管这两种方式将有相同的效果, 但是它们的方式明显不同. 84 | 85 | 第一个会在`excuteSomeAction`被调用时将捕获发生的任何错误, 而后者则会瞧瞧的忽略此类错误. 只有使用第一种方式时你才会从AngularJS中获取错误的提示. 86 | 87 | + kaov使用类似的`safeApply`: 88 | 89 | $scope.safeApply = function(fn){ 90 | var phase = this.$root.$$phase; 91 | if(phase == '$apply' || phase == '$digest') { 92 | if(fn && (typeof(fn) === 'function')) { 93 | fn(); 94 | } 95 | }else{ 96 | this.$apply(fn); 97 | } 98 | }; 99 | 100 | 你可以在顶层作用域或者根作用域中捕获到它, 然后在任何地方使用`$scope.$safeApply`函数. 一直都在讨论这个, 希望在未来的版本中这会称为默认的行为. 101 | 102 | 是否那些其他的方法也可以在`$location`对象中使用呢? 表7-1包含了一个快速的参考用于让你绑定使用. 103 | 104 | 让我们来看看`$location`服务是如何表现的, 如果浏览器中的URL时`http://www.host.com/base/index.html#!/path?param1=value1#hashValue`. 105 | 106 | Table 7-1 Functions on the $location service 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 |
Getter FunctionGetter ValueSetter Function
absUrl()http://www.host.com/base/index.html#!/path?param1=value1#hashValue,N/A
hash()hashValuehash('newHash')
host()www.host.comN/A
path()/pathpath('/newPath')
protocol()httpN/A
search(){'a':'b'}search({'c':'def'})
url()/path?param1=value1?hashValueurl('/newPath?p2=v2')
154 | 155 | 表7-1的Setter Function一列提供了一个值样本表示setter函数与其的对象类型. 156 | 157 | 注意`search()`setter函数还有一些操作模式: 158 | 159 | + 基于一个`object`调用`search(searchObj)`表示所有的参数和它们的值. 160 | + 调用`search(string)`将直接在URL上设置URL的参数为`q=String`. 161 | + 使用一个字符串参数和值调用`search(param, value)`来设置URL中一个特定的搜索参数(或者使用null调用它来移除参数). 162 | 163 | 使用任意一个这些setter函数并不意味着window.location将立即获得改变. `$location`服务会在Angular生命周期内运行, 所有的位置改变将积累在一起并在周期的后期应用. 所以可以随时作出改变, 一个借一个的, 而不用担心用户会看到一个不断闪烁和不断变更的URL的情况. 164 | 165 | ## HTML5模式和Hashbang模式 166 | 167 | `$location`服务可以使用`$locationProvider`(就像AngularJS中的一切一样, 可以注入)来配置. 对它提供两个属性特别有兴趣, 分别是: 168 | 169 | **html5Mode** 170 | 171 | 一个决定`$location`服务是否工作在HTML5模式中的布尔值. 172 | 173 | **hashPrefix** 174 | 175 | 提个字符串值(实际上是一个字符)被用作Hashbang URLs(在Hashbang模式或者旧版浏览器的HTML模式中)的前缀. 默认情况下它为空, 所以Angular的hash就只是''. 如果`hashPrefix`设置为'!', 然后Angular就会使用我们所称作的Hashbang URLs(url紧随'!'之后). 176 | 177 | 你可能会问, 这些模式是什么? 嗯, 假设你有一个超级棒的网站`www.superawesomewebsite.com`在使用AngularJS. 178 | 179 | 比方说你有一个特定的路由(它有一些参数和一个hash), 比如`/foo?bar=123#baz`. 180 | 181 | 在默认的Hashbang模式中(使用`hashPrefix`设置为'!'), 或者不支持HTML5模式的旧版浏览器中, 你的URL看起来像这样: 182 | 183 | http://www.superawesomewebsite.com/#!/foo?bar=123#baz 184 | 185 | 然而在HTML5模式中, URL看起来会像这样: 186 | 187 | http://www.superawesomewebsite.com/foo?bar=123#baz 188 | 189 | 在这两种情况下, `location.path()`就是`/foo`, `location.search()`就是`bar=123`, location.hash()`就是`baz`. 因此如果是这种情况, 为什么你不希望使用HTML5模式呢? 190 | 191 | Hashbang方法能够在所有的浏览器中无缝的工作, 并且只需要最少的配置. 你只需要设置`hashBang`前缀(默认情况下为!)并且你可以做到更好. 192 | 193 | HTML模式中, 在另一方面, 还可以通过使用HTML5的History API来访问浏览器的URL. 而`$location`服务能足够智能的判断浏览器是否支持HTML5模式, 必要的情况下还可以降级使用Hashbang方法, 因此你不需要担心额外的工作. 但是你不得不注意以下事情: 194 | 195 | **服务端配置** 196 | 197 | 因为HTML5的链接看起来像你应用程序的所有其他URL, 你需要很小心的在服务端将你应用程序的所有链接路由连接到你的主HTML页面(最有可能的是,`index.html`). 例如, 如果你的应用是`superawesomewebsite.com`的登录页, 并且你的应用中有一个`/amazing?who=me`的路由, 然后URL在浏览其中显示为`http://www.superawesomewebsite.com/ amazing?who=me+`. 198 | 199 | 当你浏览你的应用程序时, 默认情况下表现很好, 因为有HTML5 History API介入和负责很多事情. 但是如果你尝试直接浏览这个URL, 你的服务器会认为你是不是疯了, 因为在服务端它并不知道这个资源. 所以你必须确保所有指向`/amazing`的请求被充定向到`/index.html#!/amazing`. 200 | 201 | AngularJS将会以这种形式来在这一点注意这些事情. 它会检测路径的改变并冲顶像到我们所定义的AngularJS路由中. 202 | 203 | **Link rewriting(链接改写)** 204 | 205 | 你可以很容易的像下面这样指定一个URL: 206 | 207 | link 208 | 209 | 根据你是否使用的HTML5模式, AngularJS会注意分别重定向到`/some?foo=bar`或者`index.html#!/some?foo=bar`. 没有额外的步骤需要你处理. 很棒, 是不是? 210 | 211 | 但是下面的链接形式像不会被改写, 并且浏览器将在这个页面上执行一个完整的重载: 212 | 213 | + a. 链接像下面这样包含一个`target`元素 214 | 215 | link 216 | 217 | + b. 链接到一个不用域名的绝对路径: 218 | 219 | link 220 | 221 | 这里时不同的, 因为它是一个绝对的URL路径, 而前面的记录会使用现有的基础URL. 222 | 223 | + c. 链接基于一个不同的已经定义好的路径开始时: 224 | 225 | link 226 | 227 | **Relative Links(相对链接)** 228 | 229 | 一定要检查所有的相对链接(相对路径), 图片, 脚本等等. 你必须在你主HTML文件的头部指定基本的参照URL(), 或者你必须在每一处使用绝对URLs路径(以/开头的), 因为相对的URL将会使用文档中初试的绝对URL被解析为绝对的URL, 这往往不同于应用程序的根源. 230 | 231 | 强烈建议从文档根源启用History API来运行Angular应用程序, 因为它要注意所有相对路径的问题. 232 | 233 | ## AngularJS模块方法 234 | 235 | AngularJS模块负责定义如何引导你的应用程序。它还声明定义了应用程序片段。接下来让我们一起看看它是如何实现这一点的。 236 | 237 | ### 主方法在哪? 238 | 239 | 如果你来自于Java,甚至是Python编程语言社区,你可能会疑惑,AngularJS中的主方法在哪?你知道的,主方法会引导一切,并且它是首先会个执行的东西?它会将JavaScript函数和实例以及所有的事情联系在一起,然后再通知你的应用程序去运行? 240 | 241 | 但是在AngularJS中没有。替代的是Angular中的模块的概念。模块允许我们声明指定我们应用程序的依赖,以及应用程序的引导是如何发生的。使用这种方式的原因时多方面的。 242 | 243 | 1. 首先是**声明**。这意味以这种方式编写代码更容易编写和理解。就像阅读英语一样! 244 | 2. 它是**模块化**的。它会迫使你去思考如何定义你的组件和依赖,并让它们很明确。 245 | 3. 它还允许**简单测试**。在你的单元测试中,你可以选择性的拉去模块来测试,以规避代码中不可以测试的部分。同时在你的场景测试中,你还可以加载附加的模块,这样可以结合某些组件一起工作让工作变得更容易。 246 | 247 | 那么接下来,先让我们看看你要如何使用一个已经定义好的模块,然后再来看看我们如何声明一个模块。 248 | 249 | 比方说我们有一个模块,实际上,我们的应用程序中有一个名为"MyAwesoneApp"的模块。在我的HTML中,我可以只添加下面的\标签(从技术上将,也可以是任何其他的标签)。 250 | 251 | 252 | 253 | 这里的`ng-app`指令会告诉你的AngularJS可以使用`MyAwesomeApp`模块来引导你的应用程序。 254 | 255 | 那么,这个模块是如何定义的呢?嗯,首先我们建议你分你的服务,指令和过滤器模块。然后在你的主模块中,你就可以只声明你所依赖的其他模块(与我们在第4章中使用RequireJS的例子一样)。 256 | 257 | 这种方式让你管理模块变得更容易,因为它们都是很好的完整的代码块。每个模块有且仅有一个职责。这样就允许你在测试中只载入你所关心的模块,从而减少了初始化这些模块的数量。这样,测试就可以变得更小并且只会关心重点。 258 | 259 | ### 加载和依赖 260 | 261 | 模块的加载发生在两个不同的阶段,并且它们都有对应的函数。它们分别是配置和运行块(阶段): 262 | 263 | **配置块** 264 | 265 | AngularJS会在这个阶段挂接和注册所有的供应商(提供的模块)。这是因为,只有供应商和常量才能够注入到配置块中。服务能不能被初始化,并不能被注入到这个阶段。 266 | 267 | **运行块** 268 | 269 | 运行快用于快速启动你的应用程序,并且在注入任务完成创建之后开始执行应用程序。从此刻开始会阻止接下来的系统配置,只有实例和常量可以注入到运行块中。在AngularJS中,运行块是最接近你想要寻找的主方法的东西。 270 | 271 | ### 快捷方法 272 | 273 | 那么可以用模块做什么呢?我们可以实例化控制器,指令,过滤器和服务,但是模块类允许你做更多的事情,正如表7-2所示: 274 | 275 | Table 7-2 模块的快捷方法 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 |
API方法描述
config(configFn)模块加载时使用这个方法注册模块需要做的工作。
constant(name, object)这个首先发生,因此你可以在这里声明所有的常量`app-wide`,和声明所有可用的配置(也就是列表中的第一个方法)以及方法实例(从这里获取所有的方法,如控制器,服务等等).
controller(name, constructor)我们已经看过了很多控制器的例子,它主要用于设置一个控制器。
directive(name, directiveFactory)正如第6章所讨论的,它允许你为应用程序创建指令。
filter(name, filterFactory)允许你创建命名AngularJS过滤器,正如第6章所讨论的。
run(initializationFn)使用这个方法在注入设置完成时处理你要执行的工作,也就是将你的应用程序展示给用户之前。
value(name, object)允许跨应用程序注入值。
service(name, serviceFactory)下一节中讨论。
factory(name, factoryFn)下一节中讨论。
provider(name, providerFn)下一节中讨论。
327 | 328 | 你可能意识到,在前面的表格中我们省略了三个特定API-Factory,Provider,和Service的详细信息。还有一个原因是:这三者之间的用法很容易混肴,因此我们使用一个简单的例子来更好的说明一下什么时候(以及如何)使用它们每一个。 329 | 330 | **The Factory** 331 | 332 | Factory API可以用来在每当我们有一个类或者对象需要一定逻辑或者参数之前才能初始化的时候调用。一个Factory就是一个函数,这个函数的职责是创建一个值(或者一个对象)。让我们来看一个例子,greeter函数需要和它的salutation参数一起初始化: 333 | 334 | function Greeter(salutation) { 335 | this.greet = function(name) { 336 | return salutation + ' ' + name; 337 | } 338 | } 339 | 340 | greeter工厂方法(它就是一个工厂函数或者说构造函数)看起来就像这样: 341 | 342 | myApp.factory('greeter', function(salut) { 343 | return new Greeter(salut); 344 | }); 345 | 346 | 然后可以像这样调用: 347 | 348 | var myGreeter = greeter('Halo'); 349 | 350 | **The Service** 351 | 352 | 什么时服务?嗯,一个Factory和一个Service之间的不同就是Factory方法会调用传递给它的函数并返回一个值。而Service方法会在传递给它的控制器方法上调用"new"操作符并返回调用结果。 353 | 354 | 因此前面的greeter工厂可以替换为如下所示的geeter服务: 355 | 356 | myApp.service('greeter', Greeter); 357 | 358 | 那么我每次访问一个greeter实例时,AngularJS都会调用`new Greeter()`并返回调用结果。 359 | 360 | **The Provider** 361 | 362 | 这是最复杂的(大部分的都是可配置,很的)一部分。Provider结合了Factory和Service,同时它会在注入系统完全到位之前抛出Provider函数能够进行配置的信息(也就是说,它就发生在配置块中)。 363 | 364 | 让我们来看看使用Provider修改之后的greeter Service,它看起来可能是下面这样的: 365 | 366 | myApp.provider('greeter', function() { 367 | var salutation = 'Hello'; 368 | this.setSalutation = function(s){ 369 | salutation = s; 370 | } 371 | 372 | function Greeter(a) { 373 | this.greet = function() { 374 | return salutation + ' ' + a; 375 | } 376 | } 377 | 378 | this.$get = function(a) { 379 | return new Greeter(a); 380 | } 381 | }); 382 | 383 | 这就允许我们在运行时(例如,根据用户选择语言)设置salutation的值。 384 | 385 | var myApp = angular.module(myApp, []).config(function(greeterProvider){ 386 | greeterProvider.setSalutation('Namaste'); 387 | }); 388 | 389 | 每当有人访问greeter对象实例的时候AngularJS都会吉利调用`$get`方法。 390 | 391 | > **警告!** 392 | > 393 | > 这里有一个轻量级的实现,但是它们之间的用法有明显的区别: 394 | 395 | angular.module('myApp', []); 396 | > 397 | > 以及 398 | 399 | angular.module('myApp'); 400 | 401 | > 这里的不同之处在于第一种方式会创建一个新的Angular模块,然后它会拉取在方括号([...])中列出的所依赖的模块。第二种方式使用的是现有的模块,它已经在第一次调用用定义好了。 402 | 403 | > 因此你应该确保在完整的应用程序中,下面的代码只使用一次就行了: 404 | 405 | angular.module('myApp', [...]); // Or MyModule, if you are modularizing your app 406 | 407 | > 如果你不打算将它保存为一个变量并且跨应用程序引用它,然后在其他文件中使用`angular.module(MyApp)`来确保你获取的是一个正确处理过的AngularJS模块。模块中的一切都在模块定义中访问变量,或者直接将某些东西加入到模块定义的地方。 408 | 409 | ## $on, $emit和$broadcast之间的作用域通信 410 | 411 | AngularJS中的作用域有一个非常有层次和嵌套分明的结构。其中它们都有一个主要的`$rootScope`(也就说对应的Angular应用或者`ng-app`),然后其他所有的作用域部分都是继承自这个`$rootScope`的,或者说都是嵌套在主作用域下面的。很多时候,你会发现这些作用域不会共享变量或者说都不会从另一个原型继承什么。 412 | 413 | 那么在这种情况下,如何在作用域之间通信呢?其中一个选择就是在应用程序作用域之中创建一个单例服务,然后通过这个服务处理所有子作用域的通信。 414 | 415 | 在AngularJS中还有另外一个选择:通过作用域中的事件处理通信。但是这种方法有一些限制;例如,你并不能广泛的将事件传播到所有监控的作用域中。你必须选择是否与父级作用域或者子作用域通信。 416 | 417 | 但是在我们讨论这些之前,那么如何监听这些事件呢?这里有一个例子,在我们任意的恒星系统的作用域中等待和监控一个我们称之为"planetDestroyed"的事件。 418 | 419 | $scope.$on('planetDestoryed', function(event, galaxy, planet){ 420 | // Custom event, so what planet was destroyed 421 | scope.alertNearbyPlanets(galaxy, planet); 422 | }); 423 | 424 | 或许你会疑惑,传递给事件监听器的这些附加的参数是从哪里来的?那么就让我们来看看一个独立的planet是如何与它的父级作用域通信的。 425 | 426 | scope.$emit('planetDestroyed', scope.myGalaxy, scope.myPlanet); 427 | 428 | `$emit`的附加参数是以作为监听器函数的函数参数的形式来传递的。并且,`$emit`只会从它自己当前作用域向上通信,因此,星球上的穷人们(如果它们有自身的作用域)在它们的星球被毁灭之前并不会收到通知。 429 | 430 | 类似的,如果银河系统希望向下与它的成员通信,也就是恒星系统,那么它们之间的通信可能像下面这样: 431 | 432 | scope.$emit('selfDestructSystem', targetSystem); 433 | 434 | 然后,所有的恒星系统都可能在目标系统中监听这个事件,并使用下面的命令来决定它们是否应该自毁: 435 | 436 | scope.$on('selfDestructSystem', function(event, targetSystem){ 437 | if(scope.mySystem === targetSystem){ 438 | scope.selfDestruct(); // Go ka-boom!! 439 | } 440 | }); 441 | 442 | 当然,正如事件向上(或者向下)传播,它都可能必须在同一级或者作用域中说:"够了,你不能通过!",或者阻止事件的默认行为。传递给监听器的事件对象都有函数来处理上面的这些所有事情,或者更多,因此让我们在表7-3中看看你可以获取事件对象的哪些信息。 443 | 444 | 表7-3 事件对象的属性和方法 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 |
事件属性目的
event.targetScope发出或者传播原始事件的作用域
event.currentScope目前正在处理的事件的作用域
event.name事件名称
event.stopPropagation()一个防止事件进一步传播(冒泡/捕获)的函数(这只适用于使用`$emit`发出的事件)
event.preventDefault()这个方法实际上不会做什么事,但是会设置`defaultPrevented`为true。直到事件监听器的实现者采取行动之前它才会检查`defaultPrevented`的值。
event.defaultPrevented如果调用了`preventDefault`则为true
480 | 481 | > 说明:关于通信这一节译文很粗糙,待斟酌校对。 482 | 483 | ## Cookies 484 | 485 | 不就之后,在你的应用程序中(假设它足够大并且很复杂),你需要在客户端通过用户的session来存储用户会话的某些状态。你可能还记得(或者说还会做噩梦),通过`document.cookie`接口来处理纯文本形式的cookies。 486 | 487 | 值得庆幸的是,这么多年过去了,并且HTML5提供的相关API都能够在现在已经出现的大多数现代浏览器上可用。此外, AngularJS还给你提供了很好的`$cookie`和`$cookieStore` API用来处理cookies。这两个服务都能够很好的发挥HTML5 cookies,当HTML5 API可用时浏览器会选择使用HTML5提供的API,如果不可用则默认选择`document.cookies`。无论那种方式,你都可以选择使用相同的API来进行工作。 488 | 489 | 首先让我们来看看`$cookie`服务。`$cookie`是一个简单的对象。它有键(属性)和值。给这个对象添加一个键和对应的值,就会将相关信息添加到cookie中,反之,从对象中移除键(属性)时就会从cookie中删除对应的信息。它就是这么简单。 490 | 491 | 但是大多数时候,你都不会希望直接在`$cookie`上工作。直接在`cookies`上工作意味着你必须自己处理字符串转换操作和解析工作,并且还要从在对象中转换相应的数据。对于这些情况,我们有一个`$cookieStore`方法,它提供了一种编写和移除`cookie`的方式。因此你可以很方便的使用`$cookieStore`来构建一个Search控制器用户记忆最后五个搜索结果,就像下面这样: 492 | 493 | function SearchController($scope, $cookieStore) { 494 | $scope.search = function(text) { 495 | // Do the search here 496 | ... 497 | // Get the past results, or initialize an empty array if nothing found 498 | var pastSearches = $cookieStore.get('myapp.past.searches') || []; 499 | if(pastSearches.length > 5) { 500 | pastSearches = pastSearches.splice(0); 501 | } 502 | pastSearches.push(text); 503 | $cookieStore.put('myapp.past.searches', pastSearches); 504 | } 505 | } 506 | 507 | ## 国际化和本地化 508 | 509 | 你可能会听到人们提到这些术语,当它们使用不同的语言支持应用程序时。但是在这两者之间有一些细微的区别。想象一下有一个简单的应用程序作为进入用户银行账单的入口。每当你进入这个应用时,它显示且只显示一个东西: 510 | 511 | *欢迎!2012年10月25日你的账单数据为`$XX,XXX`。* 512 | 513 | 现在,明显,上面的代码(信息)目标用户直接定位为美国公民。但是如果我们希望这个应用程序也能够在英国(UK)很好的工作(简单的说就是由程序自身根据环境来选择语言)要做些什么呢?大不列颠(英国)使用的是不同的日期格式和货币符号,但是你又不希望每次你需要应用程序只是一个新的环境是都发生一次变化(比如在`en_US`和`en_UK`)。这里需要抽象的处理输出的日期/事件格式,以及货币符号,都需要从你的代码中来适配**国际化**的环境(或者i18n--18表示单词中i和n之间的字符数)。 514 | 515 | 如果我们希望这个应用程序在印度也适用呢?或者俄罗斯?此外还有日期格式和货币符号(和格式),甚至在UI中使用的字符串都需要改变。这种在不同地区转换和本地化分离的二进制字符串的形式就是我们所知道的**本地化**(或者L10n--使用大写的L来区分i和l)。 516 | 517 | ### 在AngularJS中我能做什么? 518 | 519 | AngularJS支持下面所列出的i18n/L10n: 520 | 521 | + currency 522 | + date/time 523 | + number 524 | 525 | 对于这些使用`ngPluralize`指令也可以多元化支持(对于英语就如同i18n/L10n)。 526 | 527 | 所有的这些多元化的支持都是通过`$locale`服务来处理和维护的,用户管理本地特定的规则设置。`$locale`服务清理本地的IDs,一般由两部分组成:国家代码和语言代码。例如,`en_US`和`en_UK`,分别表示美式英语和英式英语。指定一个国家代码是可选的,并且只指定一个"en"也是有效的本地代码。 528 | 529 | ### 如何获取所有工作? 530 | 531 | 获取L10n(本地化)和i18n(国际化)工作的过程在AngularJS中分为三个步骤: 532 | 533 | **index.html changes** 534 | 535 | AngularJS需要你有一个单独的`index.html`来处理每个受支持的语言环境。你的服务器也需要知道所提供的`index.html`,根据用户地区的偏好设置(这也可以通过客户端的变化来触发,当用户改变它的语言环境时)。 536 | 537 | **创建语言环境规则集** 538 | 539 | 接下来的步骤是针对每个受支持的语言环境创建一个`angular.js`,就像`angular_en-US.js`和`angular_zh-CN.js`。者涉及到在`angular.js`或者`angular.min.js`的结束处关联每个特定语言的本地规则(前面两个语言环境的默认文件就是`angular-locale_en_US.js`和`angular-locale_zh-CN.js`)。因此你的`angular_en-US.js`首先要包含`angular.js`的内容,然后就是`angular-locale_en-US.js`的内容。 540 | 541 | **本地规则集来源** 542 | 543 | 最后一步就是涉及到你必须确保你的本地`index.html`引用本度规则集而不是原始的`angular.js`文件。因此`index_en-US.html`中应该使用`angular_en-US.js`而不是`angular.js`。 544 | 545 | ### 常见问题 546 | 547 | **翻译长度** 548 | 549 | 你设计的UI在显示June 24, 1988时,在div中尽量控制其大小以适当的正确显示。然后你在西班牙语环境中打开你的UI,然而24 de junio de 1988不再适应同一空间... 550 | 551 | 那么当你国际化你的应用程序时,请记住,你的字符串长度可能发生巨大的变化,从一个语言翻译为另一个语言时。你应该适当的设计你的CSS,并且应该在各个不同的语言环境中进行完整的测试(不要忘记还存在从右到左的语言)。 552 | 553 | **时区问题** 554 | 555 | AngularJS的日期/时间过滤器会直接获取来自浏览器的时区设置。因此它依赖于计算机的时区设置,不同的人可能看到不同的信息。无论时JS还是AngularJS都有任意的内置支持由开发者指定的显示时间的时区的机制。 556 | 557 | ## 净化HTML和模块 558 | 559 | AngularJS会很认真对待其安全性,它会尝试尽最大的努力以确保将大多数的攻击转向最小化。一种常见的攻击方式就是注入不安全的HTML内容到你的web页面中,使用这种方式触发一个跨站攻击或者注入攻击。 560 | 561 | 考虑有这样一个例子,在作用域中我们有一个称之为`myUnsafeHTMLContent`的变量。然后使用利用HTML,使用`OnMouseOver`指令修改元素的内容为`PWN3D!`,就像下面这样: 562 | 563 | $scope.myUnsafeHTMLContent = '

click hreer' + 565 | 'snippet

'; 566 | 567 | 在AngularJS中其默认行为是:你有一些HTML内容存储在一个变量中并且尝试绑定给它,其返回结果是AngularJS脱离你的内容并打印它。因此,最终得到的HTML内容被视为纯文本内容。 568 | 569 | 因此: 570 | 571 |
572 | 573 | 会返回: 574 | 575 |

an html 576 | click here snippet

577 | 578 | 最后作为文本渲染在你的Web页面上。 579 | 580 | 但是如果你想将`myUnsafeHTMLContent`的内容作为HTML呈现在你的AngularJS应用程序呢?在这种情况下,AngularJS惠友额外的指令(和用于引导的服务`$sanitize`)允许你以安全和不安全的方式呈现HTML。 581 | 582 | 让我们先来看看使用安全形式的例子(通常也应该如此!),并且呈现相关HTML,小心的避免HTML最可能受到攻击的部分。在这种情况下你会使用`ng-bind-html`指令。 583 | 584 | > `ng-bind-html`,`ng-bind-html-unsafe`以及linky过滤器都在`ngSanitize`模块中。因此在你的脚本依赖中需要包含`angular-sanitize.js`(或者`.min.js`),然后添加一个`ngSanitize`模块依赖,在所有这些工作进行之前。 585 | 586 | 那么当我么在同样的`myUnsafeHTMLContent`中使用`ng-bind-html`指令时会发生什么呢?就像这样: 587 | 588 |
589 | 590 | 在这种情况下输出内容就像下面这样: 591 | 592 | an html _click here_ snippet 593 | 594 | 重要的是要注意这里的样式标记(设置字体颜色为蓝色的样式),以及\标签上的`onmouseover`事件处理器都被AngularJS移除了。它们被视为不安全的信息,因而被弃用。 595 | 596 | 最终,如果你决定你确实像呈现`myUnsafeHTMLContent`的内容,无论你是真正相信`myUnsafeHTMLContent`的内容还是其他原因,那么你可以使用`ng-bind-html-unsafe`指令: 597 | 598 |
599 | 600 | 那么这种情况下,输出的内容就像下面这样: 601 | 602 | an html _cl ick here_ snippet 603 | 604 | 此时文本颜色为蓝色(正如附加给p标签的样式),并且click here还有一个注册给它的`onmouseover`指令。因此一旦你的鼠标从其他地方滑入click here这几个文本时,输出就为改变为: 605 | 606 | an html PWN3D! snippet 607 | 608 | 正如你可以看到的,显示中这是非常不安全的,因此大概你决定使用`ng-bind-html-unsafe`指令时你要绝对肯定这是你想要的。因为其他人可能很容易读取用户信息并发送到他/她的服务器中。 609 | 610 | ### Linky 611 | 612 | 目前`linky`过滤器也存在于`ngSanitize`模块中,并且基本上允许你将它添加到HTML内容中呈现并将现有的HTML转为锚点标记的链接。它的用法很简单,让我们来看一个例子: 613 | 614 | $scope.contents = 'Text with links: http://angularjs.org/ & mailto:us@there.org'; 615 | 616 | 现在,如果你使用下面的方式来绑定数据: 617 | 618 |
619 | 620 | 这将导致数据会作为HTML内容打印在页面中,就像下面这样: 621 | 622 | Text with links: http://angularjs.org/ & mailto:us@there.org 623 | 624 | 接下来让我们看看如果我们使用`linky`过滤器会发生什么: 625 | 626 |
627 | 628 | `linky`过滤器会通过在文本内容中的查找,给其中所有的URLs格式的文本添加一个\标签和一个`mailto`链接,从而最终展现给用户的HTML内容就编程下面这样了: 629 | 630 | Text with links: http://angularjs.org/ & us@there.org 631 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | #AngularJS 2 | 3 | ![AngularJS](figure/angularjs-book.jpg) 4 | 5 | "AngularJS"中译本 -《AngularJS》 6 | 7 | + 作者: [Brad Green](https://github.com/bradlygreen) [Shyam Seshadri](https://github.com/shyamseshadri) 8 | + 译者: [basecss](mailto:270842722@qq.com) [dhcn](https://github.com/dhcn) 9 | 10 | **更少的代码, 更多的乐趣, 增强结构化Web应用程序的生产力** 11 | 12 | ************************** 13 | 14 | #目录 15 | 16 | ##第1章 AngularJS简介 17 | 18 | + 概念 19 | + 客户端模板 20 | + 模型, 视图, 控制器(MVC) 21 | + 数据绑定 22 | + 依赖注入 23 | + 指令 24 | + 示例: 购物车 25 | + 小结 26 | 27 | ##第2章 AngularJS应用程序剖析 28 | 29 | + 引用Angular 30 | + 加载脚本 31 | + 使用ng-app声明Angular界限 32 | + 模型, 视图, 控制器 33 | + 模板和数据绑定 34 | + 显示文本 35 | + 表单输入 36 | + 关于无侵入JavaScript的一些话 37 | + 列表, 表格和其他重复元素 38 | + 显示和隐藏 39 | + CSS类和样式 40 | + src和href属性注意事项 41 | + 表达式 42 | + 使用控制器分离用户界面职责 43 | + 使用作用域发布模型数据 44 | + 使用$watch观察模型变化 45 | + watch()中的性能注意事项 46 | + 使用模块组织依赖 47 | + 我需要多少模块? 48 | + 使用过滤器格式化数据 49 | + 使用路由和$location更新视图 50 | + index.html 51 | + list.html 52 | + detail.html 53 | + controller.js 54 | + 对话服务器 55 | + 使用指令更新DOM 56 | + index.html 57 | + controller.js 58 | + 验证用户输入 59 | + 小结 60 | 61 | ##第3章 AngularJS开发 62 | 63 | + 项目组织 64 | + 工具 65 | + IDEs 66 | + 运行你的应用程序 67 | + 使用Yeoman 68 | + 不使用Yeoman 69 | + 测试AngularJS 70 | + Karma 71 | + 单元测试 72 | + 端到端/集成测试 73 | + 编译 74 | + 其他优秀工具 75 | + 调试 76 | + Batarang 77 | + Yeoman: 优化你的工作流程 78 | + 安装Yeoman 79 | + 启动一个新的AngularJS项目 80 | + 运行服务器 81 | + 添加新的路由, 视图和控制器 82 | + 测试的故事 83 | + 构建项目 84 | + 使用RequireJS整合AngularJS 85 | 86 | ##第4章 分析一个AngularJS应用程序 87 | 88 | + 应用程序 89 | + 模型, 控制器和模板之间的关系 90 | + 模型 91 | + 控制器, 指令和服务 92 | + 服务 93 | + 指令 94 | + 控制器 95 | + 模板 96 | + 测试 97 | + 单元测试 98 | + 脚本测试 99 | 100 | ##第5章 与服务器通信 101 | 102 | + $http通信 103 | + 进一步配置请求 104 | + 设置HTTP头 105 | + 缓存响应 106 | + 转换请求和响应 107 | + 单元测试 108 | + 使用RESTful资源 109 | + 声明 110 | + 自定义方法 111 | + 无回调(如果你真的希望这样) 112 | + 简化服务短操作 113 | + ngResource单元测试 114 | + $q和Promise 115 | + 截取响应 116 | + 安全注意事项 117 | + JSON漏洞 118 | + XSRF 119 | 120 | ##第6章 指令 121 | 122 | + 指令和HTML验证 123 | + API预览 124 | + 为你的指令命名 125 | + 指令定义对象 126 | + 编译和链接功能 127 | + 作用域 128 | + 操作DOM元素 129 | + 控制器 130 | + 小结 131 | 132 | ##第7章 其他关注点 133 | 134 | + $location 135 | + HTML5模式和Hashbang模式 136 | + AngularJS模块方法 137 | + 主方法在哪? 138 | + 加载和依赖 139 | + 快捷方法 140 | + $on, $emit和$broadcast之间的作用域通信 141 | + Cookies 142 | + 国际化和本地化 143 | + 在AngularJS中我能做什么? 144 | + 如何获取所有工作? 145 | + 常见问题 146 | + 净化HTML和模块 147 | + Linky 148 | 149 | ##第8章 备忘单和诀窍 150 | 151 | + 包装jQuery Datepicker 152 | + ng-model 153 | + 绑定select 154 | + 调用select 155 | + 其他示例 156 | + 团队列表应用程序: 过滤器和控制器通信 157 | + 搜索框 158 | + 组合框 159 | + 复选框 160 | + 重复 161 | + AngularJS中的文件上传 162 | + 使用Socket.IO 163 | + 一个简单的分页服务 164 | + 服务器和登录 165 | + 总结 166 | 167 | ************* 168 | 169 | ##索引 170 | 171 | > 译者按: 粗译, 如有理解错误还烦请大家纠正. 可以提交pull request, 也可以在issues中提出修正意见, 同时也可以给我发[邮件](mailto:270842722@qq.com)提出修正意见. 172 | -------------------------------------------------------------------------------- /examples/Chapter1/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Hello 8 | 9 | 10 | 11 | 12 |
13 | 14 |

{{greeting.text}}, World.

15 |
16 | 26 | -------------------------------------------------------------------------------- /examples/Chapter1/hello2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Hello 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 |

{{greeting.text}}, World.

16 |
17 | 26 | -------------------------------------------------------------------------------- /examples/Chapter1/shopping-cart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Shopping cart 6 | 7 | 8 | 9 |

Your Order

10 |
11 | {{item.title}} 12 | 13 | {{item.price | currency}} 14 | {{item.price * item.quantity | currency}} 15 | 16 |
17 | 29 | 30 | -------------------------------------------------------------------------------- /examples/Chapter2/aMail/README.markdown: -------------------------------------------------------------------------------- 1 | ###说明: 2 | 3 | 这个aMail实例并不能直接在本地运行, 需要借助服务器环境. 其运行结果如下图所示: 4 | 5 | + 初始化 6 | 7 | ![init](figure1.png) 8 | 9 | + 邮件信息 10 | 11 | ![message](figure2.png) -------------------------------------------------------------------------------- /examples/Chapter2/aMail/controllers.js: -------------------------------------------------------------------------------- 1 | // Create a module for our core AMail services 2 | var aMailServices = angular.module('AMail', []); 3 | 4 | // Set up our mappings between URLs, templates, and controllers 5 | function emailRouteConfig($routeProvider) { 6 | $routeProvider. 7 | when('/', { 8 | controller: ListController, 9 | templateUrl: 'list.html' 10 | }). 11 | // Notice that for the detail view, we specify a parameterized URL component 12 | // by placing a colon in front of the id 13 | when('/view/:id', { 14 | controller: DetailController, 15 | templateUrl: 'detail.html' 16 | }). 17 | otherwise({ 18 | redirectTo: '/' 19 | }); 20 | } 21 | 22 | // Set up our route so the AMail service can find it 23 | aMailServices.config(emailRouteConfig); 24 | 25 | // Some fake emails 26 | messages = [{ 27 | id: 0, sender: 'jean@somecompany.com', subject: 'Hi there, old friend', 28 | date: 'Dec 7, 2013 12:32:00', recipients: ['greg@somecompany.com'], 29 | message: 'Hey, we should get together for lunch sometime and catch up.' 30 | +'There are many things we should collaborate on this year.' 31 | }, { 32 | id: 1, sender: 'maria@somecompany.com', 33 | subject: 'Where did you leave my laptop?', 34 | date: 'Dec 7, 2013 8:15:12', recipients: ['greg@somecompany.com'], 35 | message: 'I thought you were going to put it in my desk drawer.' 36 | +'But it does not seem to be there.' 37 | }, { 38 | id: 2, sender: 'bill@somecompany.com', subject: 'Lost python', 39 | date: 'Dec 6, 2013 20:35:02', recipients: ['greg@somecompany.com'], 40 | message: 'Nobody panic, but my pet python is missing from her cage.' 41 | +'She doesn\'t move too fast, so just call me if you see her.' 42 | } ]; 43 | 44 | // Publish our messages for the list template 45 | function ListController($scope) { 46 | $scope.messages = messages; 47 | } 48 | 49 | // Get the message id from the route (parsed from the URL) and use it to 50 | // find the right message object. 51 | function DetailController($scope, $routeParams) { 52 | $scope.message = messages[$routeParams.id]; 53 | } -------------------------------------------------------------------------------- /examples/Chapter2/aMail/detail.html: -------------------------------------------------------------------------------- 1 |
Subject: {{message.subject}}
2 |
Sender: {{message.sender}}
3 |
Date: {{message.date}}
4 |
5 | To: 6 | {{recipient}} 7 |
8 |
{{message.message}}
9 | Back to message list -------------------------------------------------------------------------------- /examples/Chapter2/aMail/figure1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/examples/Chapter2/aMail/figure1.png -------------------------------------------------------------------------------- /examples/Chapter2/aMail/figure2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/examples/Chapter2/aMail/figure2.png -------------------------------------------------------------------------------- /examples/Chapter2/aMail/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 邮件应用程序 7 | 35 | 36 | 37 |

A-Mail

38 |
39 | 40 | -------------------------------------------------------------------------------- /examples/Chapter2/aMail/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
SenderSubjectDate
{{message.sender}}{{message.subject}}{{message.date}}
-------------------------------------------------------------------------------- /examples/Chapter2/custom-directive.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 自定义指令 5 | 6 | 11 | 12 | 13 | 14 | 15 | 16 |
{{message.text}}
17 | 40 | 41 | -------------------------------------------------------------------------------- /examples/Chapter2/death-ray.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Death Ray 6 | 7 | 8 | 9 |
10 | 11 |
    12 |
  • Stun
  • 13 |
  • Disintegrate
  • 14 |
  • Erase from history
  • 15 |
16 |
17 | 28 | 29 | -------------------------------------------------------------------------------- /examples/Chapter2/displaying-text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 显示文本 6 | 7 | 8 | 9 | 10 |

{{title}}

11 | 12 |

13 | 14 | 22 | 23 | -------------------------------------------------------------------------------- /examples/Chapter2/form-input1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | form input 6 | 7 | 8 | 9 |
10 | 11 | 12 |
13 | 26 | 27 | -------------------------------------------------------------------------------- /examples/Chapter2/form-validation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 表单验证 5 | 6 | 11 | 12 | 13 | 14 |

Sign Up

15 |
16 |
{{message}}
17 |
First name:
18 |
Last name:
19 |
Email:
20 |
Age:
21 |
22 |
23 | 33 | 34 | -------------------------------------------------------------------------------- /examples/Chapter2/ng-class.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ng-class 6 | 7 | 21 | 22 | 23 |
24 |
{{messageText}}
25 | 26 | 27 |
28 | 48 | 49 | -------------------------------------------------------------------------------- /examples/Chapter2/selected-row.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | select row 6 | 7 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
{{restaurant.name}}{{restaurant.cuisine}}
28 | 41 | 42 | -------------------------------------------------------------------------------- /examples/Chapter2/show-disabled-menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | disabled item 6 | 7 | 12 | 13 | 14 |
15 |
    16 | 17 |
  • Disintegrate
  • 18 |
  • Erase from history
  • 19 |
20 |

21 |
22 | 37 | 38 | -------------------------------------------------------------------------------- /examples/Chapter2/startup-controller-reset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StartUp Rest 6 | 7 | 8 | 9 |
10 |

Starting:

11 |

Recommendation: {{funding.needed}}

12 | 13 | 14 |
15 | 34 | 35 | -------------------------------------------------------------------------------- /examples/Chapter2/startup-controller.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | startup calculator 6 | 7 | 8 | 9 |
10 |

Starting:

11 |

Recommendation: {{funding.needed}}

12 |
13 | 29 | 30 | -------------------------------------------------------------------------------- /examples/Chapter2/startup-controller2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StartUp Controller 6 | 7 | 8 | 9 |
10 |

Starting:

11 |

Recommendation: {{funding.needed}}

12 | 13 |
14 | 33 | 34 | -------------------------------------------------------------------------------- /examples/Chapter2/student-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | student list 6 | 7 | 8 | 9 |
10 | 15 | 16 |
17 | 30 | 31 | -------------------------------------------------------------------------------- /examples/Chapter2/table-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $index 6 | 7 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
{{$index + 1}}{{track.name}}{{track.duration}}
26 | 34 | 35 | -------------------------------------------------------------------------------- /examples/Chapter2/talkToServer/README.markdown: -------------------------------------------------------------------------------- 1 | ###说明: 2 | 3 | 这个实例需要借助服务器环境运行, 实例中给出了一个简单的PHP返回JSON数据. 在应用程序的HTML页面中使用`$http()`函数发起对这个php文件的请求, 最后将根据响应的数据构建出如下图所示结果界面: 4 | 5 | ![$http](figure.png) -------------------------------------------------------------------------------- /examples/Chapter2/talkToServer/figure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/examples/Chapter2/talkToServer/figure.png -------------------------------------------------------------------------------- /examples/Chapter2/talkToServer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 服务器通信 6 | 7 | 18 | 19 | 20 |

Products

21 | 22 | 23 | 24 | 25 | 26 | 27 |
{{item.title}}{{item.description}}{{item.price | currency}}
28 | 35 | 36 | -------------------------------------------------------------------------------- /examples/Chapter2/talkToServer/server.php: -------------------------------------------------------------------------------- 1 | 0, "title" => "Paint pots", "description" => "Pots full of paint", "price" => 3.95), 7 | array("id" => 1, "title" => "Polka dots", "description" => "Dots with that polka groove", "price" => 12.95), 8 | array("id" => 2, "title" => "Pebbles", "description" => "Just little rocks, really", "price" => 6.95) 9 | ); 10 | $return = json_encode($items); 11 | 12 | print_r($return) ; 13 | 14 | ?> -------------------------------------------------------------------------------- /examples/Chapter2/titleCase.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 自定义过滤器 6 | 7 | 10 | 11 | 12 |

{{pageHeading | titleCase}}

13 | 14 | 34 | 35 | -------------------------------------------------------------------------------- /examples/Chapter2/use-namespace.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

{{someText.message}}

4 | 5 | 6 | 7 | 16 | 17 | -------------------------------------------------------------------------------- /examples/Chapter2/useModule.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 使用模块 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
{{item.title}}{{item.description}}{{item.price}}
17 | 40 | 41 | -------------------------------------------------------------------------------- /examples/Chapter2/watch1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $watch 6 | 7 | 8 | 9 | 22 | 23 | 24 |
25 |
26 | {{item.title}} 27 | 28 | {{item.price | currency}} 29 | {{item.price * item.quantity | currency}} 30 |
31 |
Total: {{totalCart() | currency}}
32 |
Discount: {{bill.discount | currency}}
33 |
Subtotal: {{subtotal() | currency}}
34 |
35 | 66 | 67 | -------------------------------------------------------------------------------- /examples/Chapter2/watch2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 改进版的$watch 6 | 7 | 8 | 9 |
10 |
11 | {{item.title}} 12 | 13 | {{item.price | currency}} 14 | {{item.price * item.quantity | currency}} 15 |
16 |
Total: {{bill.total | currency}}
17 |
Discount: {{bill.discount | currency}}
18 |
Subtotal: {{bill.subtotal | currency}}
19 |
20 | 47 | 48 | -------------------------------------------------------------------------------- /examples/Chapter2/watch3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 在$watch中直接监控 5 | 6 | 7 | 8 | 9 |
10 |
11 | {{item.title}} 12 | 13 | {{item.price | currency}} 14 | {{item.price * item.quantity | currency}} 15 |
16 |
Total: {{bill.total | currency}}
17 |
Discount: {{bill.discount | currency}}
18 |
Subtotal: {{bill.subtotal | currency}}
19 |
20 | 43 | 44 | -------------------------------------------------------------------------------- /examples/Chapter6/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | hello 7 | 8 | 9 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/Chapter6/hello2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | hello 7 | 8 | 9 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/Chapter6/helloTemplate.html: -------------------------------------------------------------------------------- 1 |
Hi there
-------------------------------------------------------------------------------- /examples/css/bootstrap-responsive.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.3.0 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | 11 | .clearfix { 12 | *zoom: 1; 13 | } 14 | 15 | .clearfix:before, 16 | .clearfix:after { 17 | display: table; 18 | line-height: 0; 19 | content: ""; 20 | } 21 | 22 | .clearfix:after { 23 | clear: both; 24 | } 25 | 26 | .hide-text { 27 | font: 0/0 a; 28 | color: transparent; 29 | text-shadow: none; 30 | background-color: transparent; 31 | border: 0; 32 | } 33 | 34 | .input-block-level { 35 | display: block; 36 | width: 100%; 37 | min-height: 30px; 38 | -webkit-box-sizing: border-box; 39 | -moz-box-sizing: border-box; 40 | box-sizing: border-box; 41 | } 42 | 43 | @-ms-viewport { 44 | width: device-width; 45 | } 46 | 47 | .hidden { 48 | display: none; 49 | visibility: hidden; 50 | } 51 | 52 | .visible-phone { 53 | display: none !important; 54 | } 55 | 56 | .visible-tablet { 57 | display: none !important; 58 | } 59 | 60 | .hidden-desktop { 61 | display: none !important; 62 | } 63 | 64 | .visible-desktop { 65 | display: inherit !important; 66 | } 67 | 68 | @media (min-width: 768px) and (max-width: 979px) { 69 | .hidden-desktop { 70 | display: inherit !important; 71 | } 72 | .visible-desktop { 73 | display: none !important ; 74 | } 75 | .visible-tablet { 76 | display: inherit !important; 77 | } 78 | .hidden-tablet { 79 | display: none !important; 80 | } 81 | } 82 | 83 | @media (max-width: 767px) { 84 | .hidden-desktop { 85 | display: inherit !important; 86 | } 87 | .visible-desktop { 88 | display: none !important; 89 | } 90 | .visible-phone { 91 | display: inherit !important; 92 | } 93 | .hidden-phone { 94 | display: none !important; 95 | } 96 | } 97 | 98 | .visible-print { 99 | display: none !important; 100 | } 101 | 102 | @media print { 103 | .visible-print { 104 | display: inherit !important; 105 | } 106 | .hidden-print { 107 | display: none !important; 108 | } 109 | } 110 | 111 | @media (min-width: 1200px) { 112 | .row { 113 | margin-left: -30px; 114 | *zoom: 1; 115 | } 116 | .row:before, 117 | .row:after { 118 | display: table; 119 | line-height: 0; 120 | content: ""; 121 | } 122 | .row:after { 123 | clear: both; 124 | } 125 | [class*="span"] { 126 | float: left; 127 | min-height: 1px; 128 | margin-left: 30px; 129 | } 130 | .container, 131 | .navbar-static-top .container, 132 | .navbar-fixed-top .container, 133 | .navbar-fixed-bottom .container { 134 | width: 1170px; 135 | } 136 | .span12 { 137 | width: 1170px; 138 | } 139 | .span11 { 140 | width: 1070px; 141 | } 142 | .span10 { 143 | width: 970px; 144 | } 145 | .span9 { 146 | width: 870px; 147 | } 148 | .span8 { 149 | width: 770px; 150 | } 151 | .span7 { 152 | width: 670px; 153 | } 154 | .span6 { 155 | width: 570px; 156 | } 157 | .span5 { 158 | width: 470px; 159 | } 160 | .span4 { 161 | width: 370px; 162 | } 163 | .span3 { 164 | width: 270px; 165 | } 166 | .span2 { 167 | width: 170px; 168 | } 169 | .span1 { 170 | width: 70px; 171 | } 172 | .offset12 { 173 | margin-left: 1230px; 174 | } 175 | .offset11 { 176 | margin-left: 1130px; 177 | } 178 | .offset10 { 179 | margin-left: 1030px; 180 | } 181 | .offset9 { 182 | margin-left: 930px; 183 | } 184 | .offset8 { 185 | margin-left: 830px; 186 | } 187 | .offset7 { 188 | margin-left: 730px; 189 | } 190 | .offset6 { 191 | margin-left: 630px; 192 | } 193 | .offset5 { 194 | margin-left: 530px; 195 | } 196 | .offset4 { 197 | margin-left: 430px; 198 | } 199 | .offset3 { 200 | margin-left: 330px; 201 | } 202 | .offset2 { 203 | margin-left: 230px; 204 | } 205 | .offset1 { 206 | margin-left: 130px; 207 | } 208 | .row-fluid { 209 | width: 100%; 210 | *zoom: 1; 211 | } 212 | .row-fluid:before, 213 | .row-fluid:after { 214 | display: table; 215 | line-height: 0; 216 | content: ""; 217 | } 218 | .row-fluid:after { 219 | clear: both; 220 | } 221 | .row-fluid [class*="span"] { 222 | display: block; 223 | float: left; 224 | width: 100%; 225 | min-height: 30px; 226 | margin-left: 2.564102564102564%; 227 | *margin-left: 2.5109110747408616%; 228 | -webkit-box-sizing: border-box; 229 | -moz-box-sizing: border-box; 230 | box-sizing: border-box; 231 | } 232 | .row-fluid [class*="span"]:first-child { 233 | margin-left: 0; 234 | } 235 | .row-fluid .controls-row [class*="span"] + [class*="span"] { 236 | margin-left: 2.564102564102564%; 237 | } 238 | .row-fluid .span12 { 239 | width: 100%; 240 | *width: 99.94680851063829%; 241 | } 242 | .row-fluid .span11 { 243 | width: 91.45299145299145%; 244 | *width: 91.39979996362975%; 245 | } 246 | .row-fluid .span10 { 247 | width: 82.90598290598291%; 248 | *width: 82.8527914166212%; 249 | } 250 | .row-fluid .span9 { 251 | width: 74.35897435897436%; 252 | *width: 74.30578286961266%; 253 | } 254 | .row-fluid .span8 { 255 | width: 65.81196581196582%; 256 | *width: 65.75877432260411%; 257 | } 258 | .row-fluid .span7 { 259 | width: 57.26495726495726%; 260 | *width: 57.21176577559556%; 261 | } 262 | .row-fluid .span6 { 263 | width: 48.717948717948715%; 264 | *width: 48.664757228587014%; 265 | } 266 | .row-fluid .span5 { 267 | width: 40.17094017094017%; 268 | *width: 40.11774868157847%; 269 | } 270 | .row-fluid .span4 { 271 | width: 31.623931623931625%; 272 | *width: 31.570740134569924%; 273 | } 274 | .row-fluid .span3 { 275 | width: 23.076923076923077%; 276 | *width: 23.023731587561375%; 277 | } 278 | .row-fluid .span2 { 279 | width: 14.52991452991453%; 280 | *width: 14.476723040552828%; 281 | } 282 | .row-fluid .span1 { 283 | width: 5.982905982905983%; 284 | *width: 5.929714493544281%; 285 | } 286 | .row-fluid .offset12 { 287 | margin-left: 105.12820512820512%; 288 | *margin-left: 105.02182214948171%; 289 | } 290 | .row-fluid .offset12:first-child { 291 | margin-left: 102.56410256410257%; 292 | *margin-left: 102.45771958537915%; 293 | } 294 | .row-fluid .offset11 { 295 | margin-left: 96.58119658119658%; 296 | *margin-left: 96.47481360247316%; 297 | } 298 | .row-fluid .offset11:first-child { 299 | margin-left: 94.01709401709402%; 300 | *margin-left: 93.91071103837061%; 301 | } 302 | .row-fluid .offset10 { 303 | margin-left: 88.03418803418803%; 304 | *margin-left: 87.92780505546462%; 305 | } 306 | .row-fluid .offset10:first-child { 307 | margin-left: 85.47008547008548%; 308 | *margin-left: 85.36370249136206%; 309 | } 310 | .row-fluid .offset9 { 311 | margin-left: 79.48717948717949%; 312 | *margin-left: 79.38079650845607%; 313 | } 314 | .row-fluid .offset9:first-child { 315 | margin-left: 76.92307692307693%; 316 | *margin-left: 76.81669394435352%; 317 | } 318 | .row-fluid .offset8 { 319 | margin-left: 70.94017094017094%; 320 | *margin-left: 70.83378796144753%; 321 | } 322 | .row-fluid .offset8:first-child { 323 | margin-left: 68.37606837606839%; 324 | *margin-left: 68.26968539734497%; 325 | } 326 | .row-fluid .offset7 { 327 | margin-left: 62.393162393162385%; 328 | *margin-left: 62.28677941443899%; 329 | } 330 | .row-fluid .offset7:first-child { 331 | margin-left: 59.82905982905982%; 332 | *margin-left: 59.72267685033642%; 333 | } 334 | .row-fluid .offset6 { 335 | margin-left: 53.84615384615384%; 336 | *margin-left: 53.739770867430444%; 337 | } 338 | .row-fluid .offset6:first-child { 339 | margin-left: 51.28205128205128%; 340 | *margin-left: 51.175668303327875%; 341 | } 342 | .row-fluid .offset5 { 343 | margin-left: 45.299145299145295%; 344 | *margin-left: 45.1927623204219%; 345 | } 346 | .row-fluid .offset5:first-child { 347 | margin-left: 42.73504273504273%; 348 | *margin-left: 42.62865975631933%; 349 | } 350 | .row-fluid .offset4 { 351 | margin-left: 36.75213675213675%; 352 | *margin-left: 36.645753773413354%; 353 | } 354 | .row-fluid .offset4:first-child { 355 | margin-left: 34.18803418803419%; 356 | *margin-left: 34.081651209310785%; 357 | } 358 | .row-fluid .offset3 { 359 | margin-left: 28.205128205128204%; 360 | *margin-left: 28.0987452264048%; 361 | } 362 | .row-fluid .offset3:first-child { 363 | margin-left: 25.641025641025642%; 364 | *margin-left: 25.53464266230224%; 365 | } 366 | .row-fluid .offset2 { 367 | margin-left: 19.65811965811966%; 368 | *margin-left: 19.551736679396257%; 369 | } 370 | .row-fluid .offset2:first-child { 371 | margin-left: 17.094017094017094%; 372 | *margin-left: 16.98763411529369%; 373 | } 374 | .row-fluid .offset1 { 375 | margin-left: 11.11111111111111%; 376 | *margin-left: 11.004728132387708%; 377 | } 378 | .row-fluid .offset1:first-child { 379 | margin-left: 8.547008547008547%; 380 | *margin-left: 8.440625568285142%; 381 | } 382 | input, 383 | textarea, 384 | .uneditable-input { 385 | margin-left: 0; 386 | } 387 | .controls-row [class*="span"] + [class*="span"] { 388 | margin-left: 30px; 389 | } 390 | input.span12, 391 | textarea.span12, 392 | .uneditable-input.span12 { 393 | width: 1156px; 394 | } 395 | input.span11, 396 | textarea.span11, 397 | .uneditable-input.span11 { 398 | width: 1056px; 399 | } 400 | input.span10, 401 | textarea.span10, 402 | .uneditable-input.span10 { 403 | width: 956px; 404 | } 405 | input.span9, 406 | textarea.span9, 407 | .uneditable-input.span9 { 408 | width: 856px; 409 | } 410 | input.span8, 411 | textarea.span8, 412 | .uneditable-input.span8 { 413 | width: 756px; 414 | } 415 | input.span7, 416 | textarea.span7, 417 | .uneditable-input.span7 { 418 | width: 656px; 419 | } 420 | input.span6, 421 | textarea.span6, 422 | .uneditable-input.span6 { 423 | width: 556px; 424 | } 425 | input.span5, 426 | textarea.span5, 427 | .uneditable-input.span5 { 428 | width: 456px; 429 | } 430 | input.span4, 431 | textarea.span4, 432 | .uneditable-input.span4 { 433 | width: 356px; 434 | } 435 | input.span3, 436 | textarea.span3, 437 | .uneditable-input.span3 { 438 | width: 256px; 439 | } 440 | input.span2, 441 | textarea.span2, 442 | .uneditable-input.span2 { 443 | width: 156px; 444 | } 445 | input.span1, 446 | textarea.span1, 447 | .uneditable-input.span1 { 448 | width: 56px; 449 | } 450 | .thumbnails { 451 | margin-left: -30px; 452 | } 453 | .thumbnails > li { 454 | margin-left: 30px; 455 | } 456 | .row-fluid .thumbnails { 457 | margin-left: 0; 458 | } 459 | } 460 | 461 | @media (min-width: 768px) and (max-width: 979px) { 462 | .row { 463 | margin-left: -20px; 464 | *zoom: 1; 465 | } 466 | .row:before, 467 | .row:after { 468 | display: table; 469 | line-height: 0; 470 | content: ""; 471 | } 472 | .row:after { 473 | clear: both; 474 | } 475 | [class*="span"] { 476 | float: left; 477 | min-height: 1px; 478 | margin-left: 20px; 479 | } 480 | .container, 481 | .navbar-static-top .container, 482 | .navbar-fixed-top .container, 483 | .navbar-fixed-bottom .container { 484 | width: 724px; 485 | } 486 | .span12 { 487 | width: 724px; 488 | } 489 | .span11 { 490 | width: 662px; 491 | } 492 | .span10 { 493 | width: 600px; 494 | } 495 | .span9 { 496 | width: 538px; 497 | } 498 | .span8 { 499 | width: 476px; 500 | } 501 | .span7 { 502 | width: 414px; 503 | } 504 | .span6 { 505 | width: 352px; 506 | } 507 | .span5 { 508 | width: 290px; 509 | } 510 | .span4 { 511 | width: 228px; 512 | } 513 | .span3 { 514 | width: 166px; 515 | } 516 | .span2 { 517 | width: 104px; 518 | } 519 | .span1 { 520 | width: 42px; 521 | } 522 | .offset12 { 523 | margin-left: 764px; 524 | } 525 | .offset11 { 526 | margin-left: 702px; 527 | } 528 | .offset10 { 529 | margin-left: 640px; 530 | } 531 | .offset9 { 532 | margin-left: 578px; 533 | } 534 | .offset8 { 535 | margin-left: 516px; 536 | } 537 | .offset7 { 538 | margin-left: 454px; 539 | } 540 | .offset6 { 541 | margin-left: 392px; 542 | } 543 | .offset5 { 544 | margin-left: 330px; 545 | } 546 | .offset4 { 547 | margin-left: 268px; 548 | } 549 | .offset3 { 550 | margin-left: 206px; 551 | } 552 | .offset2 { 553 | margin-left: 144px; 554 | } 555 | .offset1 { 556 | margin-left: 82px; 557 | } 558 | .row-fluid { 559 | width: 100%; 560 | *zoom: 1; 561 | } 562 | .row-fluid:before, 563 | .row-fluid:after { 564 | display: table; 565 | line-height: 0; 566 | content: ""; 567 | } 568 | .row-fluid:after { 569 | clear: both; 570 | } 571 | .row-fluid [class*="span"] { 572 | display: block; 573 | float: left; 574 | width: 100%; 575 | min-height: 30px; 576 | margin-left: 2.7624309392265194%; 577 | *margin-left: 2.709239449864817%; 578 | -webkit-box-sizing: border-box; 579 | -moz-box-sizing: border-box; 580 | box-sizing: border-box; 581 | } 582 | .row-fluid [class*="span"]:first-child { 583 | margin-left: 0; 584 | } 585 | .row-fluid .controls-row [class*="span"] + [class*="span"] { 586 | margin-left: 2.7624309392265194%; 587 | } 588 | .row-fluid .span12 { 589 | width: 100%; 590 | *width: 99.94680851063829%; 591 | } 592 | .row-fluid .span11 { 593 | width: 91.43646408839778%; 594 | *width: 91.38327259903608%; 595 | } 596 | .row-fluid .span10 { 597 | width: 82.87292817679558%; 598 | *width: 82.81973668743387%; 599 | } 600 | .row-fluid .span9 { 601 | width: 74.30939226519337%; 602 | *width: 74.25620077583166%; 603 | } 604 | .row-fluid .span8 { 605 | width: 65.74585635359117%; 606 | *width: 65.69266486422946%; 607 | } 608 | .row-fluid .span7 { 609 | width: 57.18232044198895%; 610 | *width: 57.12912895262725%; 611 | } 612 | .row-fluid .span6 { 613 | width: 48.61878453038674%; 614 | *width: 48.56559304102504%; 615 | } 616 | .row-fluid .span5 { 617 | width: 40.05524861878453%; 618 | *width: 40.00205712942283%; 619 | } 620 | .row-fluid .span4 { 621 | width: 31.491712707182323%; 622 | *width: 31.43852121782062%; 623 | } 624 | .row-fluid .span3 { 625 | width: 22.92817679558011%; 626 | *width: 22.87498530621841%; 627 | } 628 | .row-fluid .span2 { 629 | width: 14.3646408839779%; 630 | *width: 14.311449394616199%; 631 | } 632 | .row-fluid .span1 { 633 | width: 5.801104972375691%; 634 | *width: 5.747913483013988%; 635 | } 636 | .row-fluid .offset12 { 637 | margin-left: 105.52486187845304%; 638 | *margin-left: 105.41847889972962%; 639 | } 640 | .row-fluid .offset12:first-child { 641 | margin-left: 102.76243093922652%; 642 | *margin-left: 102.6560479605031%; 643 | } 644 | .row-fluid .offset11 { 645 | margin-left: 96.96132596685082%; 646 | *margin-left: 96.8549429881274%; 647 | } 648 | .row-fluid .offset11:first-child { 649 | margin-left: 94.1988950276243%; 650 | *margin-left: 94.09251204890089%; 651 | } 652 | .row-fluid .offset10 { 653 | margin-left: 88.39779005524862%; 654 | *margin-left: 88.2914070765252%; 655 | } 656 | .row-fluid .offset10:first-child { 657 | margin-left: 85.6353591160221%; 658 | *margin-left: 85.52897613729868%; 659 | } 660 | .row-fluid .offset9 { 661 | margin-left: 79.8342541436464%; 662 | *margin-left: 79.72787116492299%; 663 | } 664 | .row-fluid .offset9:first-child { 665 | margin-left: 77.07182320441989%; 666 | *margin-left: 76.96544022569647%; 667 | } 668 | .row-fluid .offset8 { 669 | margin-left: 71.2707182320442%; 670 | *margin-left: 71.16433525332079%; 671 | } 672 | .row-fluid .offset8:first-child { 673 | margin-left: 68.50828729281768%; 674 | *margin-left: 68.40190431409427%; 675 | } 676 | .row-fluid .offset7 { 677 | margin-left: 62.70718232044199%; 678 | *margin-left: 62.600799341718584%; 679 | } 680 | .row-fluid .offset7:first-child { 681 | margin-left: 59.94475138121547%; 682 | *margin-left: 59.838368402492065%; 683 | } 684 | .row-fluid .offset6 { 685 | margin-left: 54.14364640883978%; 686 | *margin-left: 54.037263430116376%; 687 | } 688 | .row-fluid .offset6:first-child { 689 | margin-left: 51.38121546961326%; 690 | *margin-left: 51.27483249088986%; 691 | } 692 | .row-fluid .offset5 { 693 | margin-left: 45.58011049723757%; 694 | *margin-left: 45.47372751851417%; 695 | } 696 | .row-fluid .offset5:first-child { 697 | margin-left: 42.81767955801105%; 698 | *margin-left: 42.71129657928765%; 699 | } 700 | .row-fluid .offset4 { 701 | margin-left: 37.01657458563536%; 702 | *margin-left: 36.91019160691196%; 703 | } 704 | .row-fluid .offset4:first-child { 705 | margin-left: 34.25414364640884%; 706 | *margin-left: 34.14776066768544%; 707 | } 708 | .row-fluid .offset3 { 709 | margin-left: 28.45303867403315%; 710 | *margin-left: 28.346655695309746%; 711 | } 712 | .row-fluid .offset3:first-child { 713 | margin-left: 25.69060773480663%; 714 | *margin-left: 25.584224756083227%; 715 | } 716 | .row-fluid .offset2 { 717 | margin-left: 19.88950276243094%; 718 | *margin-left: 19.783119783707537%; 719 | } 720 | .row-fluid .offset2:first-child { 721 | margin-left: 17.12707182320442%; 722 | *margin-left: 17.02068884448102%; 723 | } 724 | .row-fluid .offset1 { 725 | margin-left: 11.32596685082873%; 726 | *margin-left: 11.219583872105325%; 727 | } 728 | .row-fluid .offset1:first-child { 729 | margin-left: 8.56353591160221%; 730 | *margin-left: 8.457152932878806%; 731 | } 732 | input, 733 | textarea, 734 | .uneditable-input { 735 | margin-left: 0; 736 | } 737 | .controls-row [class*="span"] + [class*="span"] { 738 | margin-left: 20px; 739 | } 740 | input.span12, 741 | textarea.span12, 742 | .uneditable-input.span12 { 743 | width: 710px; 744 | } 745 | input.span11, 746 | textarea.span11, 747 | .uneditable-input.span11 { 748 | width: 648px; 749 | } 750 | input.span10, 751 | textarea.span10, 752 | .uneditable-input.span10 { 753 | width: 586px; 754 | } 755 | input.span9, 756 | textarea.span9, 757 | .uneditable-input.span9 { 758 | width: 524px; 759 | } 760 | input.span8, 761 | textarea.span8, 762 | .uneditable-input.span8 { 763 | width: 462px; 764 | } 765 | input.span7, 766 | textarea.span7, 767 | .uneditable-input.span7 { 768 | width: 400px; 769 | } 770 | input.span6, 771 | textarea.span6, 772 | .uneditable-input.span6 { 773 | width: 338px; 774 | } 775 | input.span5, 776 | textarea.span5, 777 | .uneditable-input.span5 { 778 | width: 276px; 779 | } 780 | input.span4, 781 | textarea.span4, 782 | .uneditable-input.span4 { 783 | width: 214px; 784 | } 785 | input.span3, 786 | textarea.span3, 787 | .uneditable-input.span3 { 788 | width: 152px; 789 | } 790 | input.span2, 791 | textarea.span2, 792 | .uneditable-input.span2 { 793 | width: 90px; 794 | } 795 | input.span1, 796 | textarea.span1, 797 | .uneditable-input.span1 { 798 | width: 28px; 799 | } 800 | } 801 | 802 | @media (max-width: 767px) { 803 | body { 804 | padding-right: 20px; 805 | padding-left: 20px; 806 | } 807 | .navbar-fixed-top, 808 | .navbar-fixed-bottom, 809 | .navbar-static-top { 810 | margin-right: -20px; 811 | margin-left: -20px; 812 | } 813 | .container-fluid { 814 | padding: 0; 815 | } 816 | .dl-horizontal dt { 817 | float: none; 818 | width: auto; 819 | clear: none; 820 | text-align: left; 821 | } 822 | .dl-horizontal dd { 823 | margin-left: 0; 824 | } 825 | .container { 826 | width: auto; 827 | } 828 | .row-fluid { 829 | width: 100%; 830 | } 831 | .row, 832 | .thumbnails { 833 | margin-left: 0; 834 | } 835 | .thumbnails > li { 836 | float: none; 837 | margin-left: 0; 838 | } 839 | [class*="span"], 840 | .uneditable-input[class*="span"], 841 | .row-fluid [class*="span"] { 842 | display: block; 843 | float: none; 844 | width: 100%; 845 | margin-left: 0; 846 | -webkit-box-sizing: border-box; 847 | -moz-box-sizing: border-box; 848 | box-sizing: border-box; 849 | } 850 | .span12, 851 | .row-fluid .span12 { 852 | width: 100%; 853 | -webkit-box-sizing: border-box; 854 | -moz-box-sizing: border-box; 855 | box-sizing: border-box; 856 | } 857 | .row-fluid [class*="offset"]:first-child { 858 | margin-left: 0; 859 | } 860 | .input-large, 861 | .input-xlarge, 862 | .input-xxlarge, 863 | input[class*="span"], 864 | select[class*="span"], 865 | textarea[class*="span"], 866 | .uneditable-input { 867 | display: block; 868 | width: 100%; 869 | min-height: 30px; 870 | -webkit-box-sizing: border-box; 871 | -moz-box-sizing: border-box; 872 | box-sizing: border-box; 873 | } 874 | .input-prepend input, 875 | .input-append input, 876 | .input-prepend input[class*="span"], 877 | .input-append input[class*="span"] { 878 | display: inline-block; 879 | width: auto; 880 | } 881 | .controls-row [class*="span"] + [class*="span"] { 882 | margin-left: 0; 883 | } 884 | .modal { 885 | position: fixed; 886 | top: 20px; 887 | right: 20px; 888 | left: 20px; 889 | width: auto; 890 | margin: 0; 891 | } 892 | .modal.fade { 893 | top: -100px; 894 | } 895 | .modal.fade.in { 896 | top: 20px; 897 | } 898 | } 899 | 900 | @media (max-width: 480px) { 901 | .nav-collapse { 902 | -webkit-transform: translate3d(0, 0, 0); 903 | } 904 | .page-header h1 small { 905 | display: block; 906 | line-height: 20px; 907 | } 908 | input[type="checkbox"], 909 | input[type="radio"] { 910 | border: 1px solid #ccc; 911 | } 912 | .form-horizontal .control-label { 913 | float: none; 914 | width: auto; 915 | padding-top: 0; 916 | text-align: left; 917 | } 918 | .form-horizontal .controls { 919 | margin-left: 0; 920 | } 921 | .form-horizontal .control-list { 922 | padding-top: 0; 923 | } 924 | .form-horizontal .form-actions { 925 | padding-right: 10px; 926 | padding-left: 10px; 927 | } 928 | .media .pull-left, 929 | .media .pull-right { 930 | display: block; 931 | float: none; 932 | margin-bottom: 10px; 933 | } 934 | .media-object { 935 | margin-right: 0; 936 | margin-left: 0; 937 | } 938 | .modal { 939 | top: 10px; 940 | right: 10px; 941 | left: 10px; 942 | } 943 | .modal-header .close { 944 | padding: 10px; 945 | margin: -10px; 946 | } 947 | .carousel-caption { 948 | position: static; 949 | } 950 | } 951 | 952 | @media (max-width: 979px) { 953 | body { 954 | padding-top: 0; 955 | } 956 | .navbar-fixed-top, 957 | .navbar-fixed-bottom { 958 | position: static; 959 | } 960 | .navbar-fixed-top { 961 | margin-bottom: 20px; 962 | } 963 | .navbar-fixed-bottom { 964 | margin-top: 20px; 965 | } 966 | .navbar-fixed-top .navbar-inner, 967 | .navbar-fixed-bottom .navbar-inner { 968 | padding: 5px; 969 | } 970 | .navbar .container { 971 | width: auto; 972 | padding: 0; 973 | } 974 | .navbar .brand { 975 | padding-right: 10px; 976 | padding-left: 10px; 977 | margin: 0 0 0 -5px; 978 | } 979 | .nav-collapse { 980 | clear: both; 981 | } 982 | .nav-collapse .nav { 983 | float: none; 984 | margin: 0 0 10px; 985 | } 986 | .nav-collapse .nav > li { 987 | float: none; 988 | } 989 | .nav-collapse .nav > li > a { 990 | margin-bottom: 2px; 991 | } 992 | .nav-collapse .nav > .divider-vertical { 993 | display: none; 994 | } 995 | .nav-collapse .nav .nav-header { 996 | color: #777777; 997 | text-shadow: none; 998 | } 999 | .nav-collapse .nav > li > a, 1000 | .nav-collapse .dropdown-menu a { 1001 | padding: 9px 15px; 1002 | font-weight: bold; 1003 | color: #777777; 1004 | -webkit-border-radius: 3px; 1005 | -moz-border-radius: 3px; 1006 | border-radius: 3px; 1007 | } 1008 | .nav-collapse .btn { 1009 | padding: 4px 10px 4px; 1010 | font-weight: normal; 1011 | -webkit-border-radius: 4px; 1012 | -moz-border-radius: 4px; 1013 | border-radius: 4px; 1014 | } 1015 | .nav-collapse .dropdown-menu li + li a { 1016 | margin-bottom: 2px; 1017 | } 1018 | .nav-collapse .nav > li > a:hover, 1019 | .nav-collapse .nav > li > a:focus, 1020 | .nav-collapse .dropdown-menu a:hover, 1021 | .nav-collapse .dropdown-menu a:focus { 1022 | background-color: #f2f2f2; 1023 | } 1024 | .navbar-inverse .nav-collapse .nav > li > a, 1025 | .navbar-inverse .nav-collapse .dropdown-menu a { 1026 | color: #999999; 1027 | } 1028 | .navbar-inverse .nav-collapse .nav > li > a:hover, 1029 | .navbar-inverse .nav-collapse .nav > li > a:focus, 1030 | .navbar-inverse .nav-collapse .dropdown-menu a:hover, 1031 | .navbar-inverse .nav-collapse .dropdown-menu a:focus { 1032 | background-color: #111111; 1033 | } 1034 | .nav-collapse.in .btn-group { 1035 | padding: 0; 1036 | margin-top: 5px; 1037 | } 1038 | .nav-collapse .dropdown-menu { 1039 | position: static; 1040 | top: auto; 1041 | left: auto; 1042 | display: none; 1043 | float: none; 1044 | max-width: none; 1045 | padding: 0; 1046 | margin: 0 15px; 1047 | background-color: transparent; 1048 | border: none; 1049 | -webkit-border-radius: 0; 1050 | -moz-border-radius: 0; 1051 | border-radius: 0; 1052 | -webkit-box-shadow: none; 1053 | -moz-box-shadow: none; 1054 | box-shadow: none; 1055 | } 1056 | .nav-collapse .open > .dropdown-menu { 1057 | display: block; 1058 | } 1059 | .nav-collapse .dropdown-menu:before, 1060 | .nav-collapse .dropdown-menu:after { 1061 | display: none; 1062 | } 1063 | .nav-collapse .dropdown-menu .divider { 1064 | display: none; 1065 | } 1066 | .nav-collapse .nav > li > .dropdown-menu:before, 1067 | .nav-collapse .nav > li > .dropdown-menu:after { 1068 | display: none; 1069 | } 1070 | .nav-collapse .navbar-form, 1071 | .nav-collapse .navbar-search { 1072 | float: none; 1073 | padding: 10px 15px; 1074 | margin: 10px 0; 1075 | border-top: 1px solid #f2f2f2; 1076 | border-bottom: 1px solid #f2f2f2; 1077 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1078 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1079 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1080 | } 1081 | .navbar-inverse .nav-collapse .navbar-form, 1082 | .navbar-inverse .nav-collapse .navbar-search { 1083 | border-top-color: #111111; 1084 | border-bottom-color: #111111; 1085 | } 1086 | .navbar .nav-collapse .nav.pull-right { 1087 | float: none; 1088 | margin-left: 0; 1089 | } 1090 | .nav-collapse, 1091 | .nav-collapse.collapse { 1092 | height: 0; 1093 | overflow: hidden; 1094 | } 1095 | .navbar .btn-navbar { 1096 | display: block; 1097 | } 1098 | .navbar-static .navbar-inner { 1099 | padding-right: 10px; 1100 | padding-left: 10px; 1101 | } 1102 | } 1103 | 1104 | @media (min-width: 980px) { 1105 | .nav-collapse.collapse { 1106 | height: auto !important; 1107 | overflow: visible !important; 1108 | } 1109 | } 1110 | -------------------------------------------------------------------------------- /examples/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.3.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /examples/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/examples/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /examples/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/examples/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /examples/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap.js by @fat & @mdo 3 | * Copyright 2012 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | !function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||s.toggleClass("open"),n.focus(),!1},keydown:function(n){var r,s,o,u,a,f;if(!/(38|40|27)/.test(n.keyCode))return;r=e(this),n.preventDefault(),n.stopPropagation();if(r.is(".disabled, :disabled"))return;u=i(r),a=u.hasClass("open");if(!a||a&&n.keyCode==27)return n.which==27&&u.find(t).focus(),r.click();s=e("[role=menu] li:not(.divider):visible a",u);if(!s.length)return;f=s.index(s.filter(":focus")),n.keyCode==38&&f>0&&f--,n.keyCode==40&&f').appendTo(document.body),this.$backdrop.click(this.options.backdrop=="static"?e.proxy(this.$element[0].focus,this.$element[0]):e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in");if(!t)return;i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,t):t()):t&&t()}};var n=e.fn.modal;e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e.fn.modal.noConflict=function(){return e.fn.modal=n,this},e(document).on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s,o,u,a;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,o=this.options.trigger.split(" ");for(a=o.length;a--;)u=o[a],u=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):u!="manual"&&(i=u=="hover"?"mouseenter":"focus",s=u=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this)));this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,this.$element.data(),t),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e.fn[this.type].defaults,r={},i;this._options&&e.each(this._options,function(e,t){n[e]!=t&&(r[e]=t)},this),i=e(t.currentTarget)[this.type](r).data(this.type);if(!i.options.delay||!i.options.delay.show)return i.show();clearTimeout(this.timeout),i.hoverState="in",this.timeout=setTimeout(function(){i.hoverState=="in"&&i.show()},i.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var t,n,r,i,s,o,u=e.Event("show");if(this.hasContent()&&this.enabled){this.$element.trigger(u);if(u.isDefaultPrevented())return;t=this.tip(),this.setContent(),this.options.animation&&t.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,t[0],this.$element[0]):this.options.placement,t.detach().css({top:0,left:0,display:"block"}),this.options.container?t.appendTo(this.options.container):t.insertAfter(this.$element),n=this.getPosition(),r=t[0].offsetWidth,i=t[0].offsetHeight;switch(s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}this.applyPlacement(o,s),this.$element.trigger("shown")}},applyPlacement:function(e,t){var n=this.tip(),r=n[0].offsetWidth,i=n[0].offsetHeight,s,o,u,a;n.offset(e).addClass(t).addClass("in"),s=n[0].offsetWidth,o=n[0].offsetHeight,t=="top"&&o!=i&&(e.top=e.top+i-o,a=!0),t=="bottom"||t=="top"?(u=0,e.left<0&&(u=e.left*-2,e.left=0,n.offset(e),s=n[0].offsetWidth,o=n[0].offsetHeight),this.replaceArrow(u-r+s,s,"left")):this.replaceArrow(o-i,o,"top"),a&&n.offset(e)},replaceArrow:function(e,t,n){this.arrow().css(n,e?50*(1-e/t)+"%":"")},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function i(){var t=setTimeout(function(){n.off(e.support.transition.end).detach()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.detach()})}var t=this,n=this.tip(),r=e.Event("hide");this.$element.trigger(r);if(r.isDefaultPrevented())return;return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?i():n.detach(),this.$element.trigger("hidden"),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").attr("title","")},hasContent:function(){return this.getTitle()},getPosition:function(){var t=this.$element[0];return e.extend({},typeof t.getBoundingClientRect=="function"?t.getBoundingClientRect():{width:t.offsetWidth,height:t.offsetHeight},this.$element.offset())},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},arrow:function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(t){var n=t?e(t.currentTarget)[this.type](this._options).data(this.type):this;n.tip().hasClass("in")?n.hide():n.show()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}};var n=e.fn.tooltip;e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},e.fn.tooltip.noConflict=function(){return e.fn.tooltip=n,this}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=(typeof n.content=="function"?n.content.call(t[0]):n.content)||t.attr("data-content"),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}});var n=e.fn.popover;e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'

'}),e.fn.popover.noConflict=function(){return e.fn.popover=n,this}}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var n=e(this),r=n.data("target")||n.attr("href"),i=/^#\w/.test(r)&&e(r);return i&&i.length&&[[i.position().top+(!e.isWindow(t.$scrollElement.get(0))&&t.$scrollElement.scrollTop()),r]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}};var n=e.fn.scrollspy;e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e.fn.scrollspy.noConflict=function(){return e.fn.scrollspy=n,this},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active:last a")[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}};var n=e.fn.tab;e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e.fn.tab.noConflict=function(){return e.fn.tab=n,this},e(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.source=this.options.source,this.$menu=e(this.options.menu),this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});return this.$menu.insertAfter(this.$element).css({top:t.top+t.height,left:t.left}).show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length"+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("focus",e.proxy(this.focus,this)).on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this)).on("mouseleave","li",e.proxy(this.mouseleave,this))},eventSupported:function(e){var t=e in this.$element;return t||(this.$element.setAttribute(e,"return;"),t=typeof this.$element[e]=="function"),t},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},focus:function(e){this.focused=!0},blur:function(e){this.focused=!1,!this.mousedover&&this.shown&&this.hide()},click:function(e){e.stopPropagation(),e.preventDefault(),this.select(),this.$element.focus()},mouseenter:function(t){this.mousedover=!0,this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")},mouseleave:function(e){this.mousedover=!1,!this.focused&&this.shown&&this.hide()}};var n=e.fn.typeahead;e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1},e.fn.typeahead.Constructor=t,e.fn.typeahead.noConflict=function(){return e.fn.typeahead=n,this},e(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;n.typeahead(n.data())})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)).on("click.affix.data-api",e.proxy(function(){setTimeout(e.proxy(this.checkPosition,this),1)},this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))};var n=e.fn.affix;e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e.fn.affix.noConflict=function(){return e.fn.affix=n,this},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery); -------------------------------------------------------------------------------- /figure/3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/3-1.png -------------------------------------------------------------------------------- /figure/3-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/3-2.png -------------------------------------------------------------------------------- /figure/3-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/3-3.png -------------------------------------------------------------------------------- /figure/3-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/3-4.png -------------------------------------------------------------------------------- /figure/3-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/3-5.png -------------------------------------------------------------------------------- /figure/4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/4-1.png -------------------------------------------------------------------------------- /figure/6-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/6-2.png -------------------------------------------------------------------------------- /figure/6-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/6-3.png -------------------------------------------------------------------------------- /figure/accordion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/accordion.png -------------------------------------------------------------------------------- /figure/angularjs-book.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/angularjs-book.jpg -------------------------------------------------------------------------------- /figure/custom-directive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/custom-directive.png -------------------------------------------------------------------------------- /figure/hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/hello.png -------------------------------------------------------------------------------- /figure/hello2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/hello2.png -------------------------------------------------------------------------------- /figure/hello3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/hello3.png -------------------------------------------------------------------------------- /figure/shopping-cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/shopping-cart.png -------------------------------------------------------------------------------- /figure/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/signup.png -------------------------------------------------------------------------------- /figure/tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/tab.png -------------------------------------------------------------------------------- /figure/titleCase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/titleCase.png -------------------------------------------------------------------------------- /figure/useModule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/useModule.png -------------------------------------------------------------------------------- /figure/watch1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuzhichao/angularjs-zh/2bee80b6d65107bb9e9cb3a817f3dd969153b95e/figure/watch1.png --------------------------------------------------------------------------------