本章,作者将通过收集和验证购物明细,来完成SportsStore应用,并在Deployd服务器上存储该订单。作者也构建了一个管理应用,允许认证用户查看订单,和管理产品分类。
1、准备实例项目
2、获取产品明细
在给用户显示购物车中的产品汇总后,作者将购物明细用于订单。这需要使用AngularJS的表单特性,你会在大多数web应用中需要。作者已经创建Views/placeOrder.html文件,捕获用户的购物明细。作者将介绍一些与表单相关度的特性,来避免重复大量相似的代码。作者首先添加一组数据属性(用户的名字和街道地址),然后再其他添加的属性。下面是placeOrder.html文件。
<h2>Check out now</h2> <p>Please enter your details, and we'll ship your goods right away!</p> <div class="well"> <h3>Ship to</h3> <div class="form-group"> <label>Name</label> <input class="form-control" ng-model="data.shipping.name" /> </div> <h3>Address</h3> <div class="form-group"> <label>Street Address</label> <input class="form-control" ng-model="data.shipping.street" /> </div> <div class="text-center"> <button class="btn btn-primary">Complete order</button> </div> </div>
第一件事情,是作者没有使用ng-controller指令,来为视图指定一个控制器。这意味着视图会被顶级controller ,sportsStoreCtrl支持,该控制器管理包含ng-view指令的视图。当视图不需要任何额外控制器行为时,不用为部分视图定义控制器。
这里,AngularJS的一个重要特性是,在input元素上,使用ng-model指令。
<input class="form-control" ng-model="data.shipping.name"/>
该ng-model指令,设置了一个双向数据绑定。作者会在第10章深入解释数据绑定,但这里简短介绍。它使用{{}},是单向绑定,意味着简单地从scope显示一个值。该单向绑定的值可以被过滤,它可以是一个表达式,而不仅仅是一个数据值,单他是一个只读关系。如果scope改变,显示的值也会变。它只能从scope到binding。
双向数据绑定用于表单元素,允许用户输入值,改变scope。更新流在scope和data binding之间。通过一个JavaScript功能,执行更新scope数据属性。这里只要知道,如果用户在input元素中输入了一个值,该值被指派到通过ng-model指令指定的scope属性上,在本例中是,data.shipping.name属性和data.shipping.street属性。
提示:注意作者没有必要更新控制器,让他在scope上定义一个data.shipping对象,或单独的name或street属性。如果该属性不存在,AngularJS scopes会假设你想动态地定义一个属性。作者将在第13章深入讲。
2.1、添加表单校验
如果你写过任何种类的web应用,使用表单元素,你会知道用户会在input字段中输入任何东西。要确保你得到预期的数据,AngularJS支持form validation,允许检查值。
AngularJS表单校验,基于标准HTML属性,应用到表单元素,例如type和required。表达那校验会自动执行,但必须显示校验feedback给用户。
提示:HTML5在input元素上定义了一组新的type属性的值,能被用于该值是一个e-mail地址或一个数字。作者会在第12章解释。
2.1.1、准备校验
设置表单校验的第一步,是添加一个form元素到视图,并添加校验属性到作者的input元素。下面是placeOrder.html文件。
<h2>Check out now</h2> <p>Please enter your details, and we'll ship your goods right away!</p> <form name="shippingForm" novalidate> <div class="well"> <h3>Ship to</h3> <div class="form-group"> <label>Name</label> <input class="form-control" ng-model="data.shipping.name" required /> </div> <h3>Address</h3> <div class="form-group"> <label>Street Address</label> <input class="form-control" ng-model="data.shipping.street" required /> </div> <div class="text-center"> <button class="btn btn-primary">Complete order</button> </div> </div> </form>
form元素有三个目的,作者没有使用浏览器为提交表单提供的内建支持的。
第一个目的,是启用校验。AngularJS使用自定义的指令,重定义了一些HTML元素,来启用制定特性,例如form。没有form元素,AngularJS不验证元素的内容,如input,select,textarea等。
第二个谜底,form元素禁用浏览器可能会尝试执行的任何校验,它通过novalidate属性。该属性是标准的HTML5特性,它确保只有AngularJS检查数据。如果你忽略了novalidate属性,那么用户可能会得到冲突,或多次校验feedback,基于浏览器的会被使用。
最后的目的,form元素会定义一个变量,用于报告表单校验。它是通过name属性做的,作者将它设为shippingForm。当用户点击button按钮,用户表单的内容被验证时,该值用于显示feedback。
2.1.2、显示验证Feedback
一旦form元素和校验属性放置好,AngularJS开始校验用户提供的数据,作者要为用户显示feedback。作者将在第12章详细讲解,但这里作者会用两种类型的feedback:定义CSS样式给AngularJS通过验证或没有通过验证的表单元素。可以使用scope变量控制feedback消息显示的元素的可见性。placeOrder.html文件:
<style> .ng-invalid { background-color: lightpink; } .ng-valid { background-color: lightgreen; } span.error { color: red; font-weight: bold; } </style> <h2>Check out now</h2> <p>Please enter your details, and we'll ship your goods right away!</p> <form name="shippingForm" novalidate> <div class="well"> <h3>Ship to</h3> <div class="form-group"> <label>Name</label> <input name="name" class="form-control" ng-model="data.shipping.name" required /> <span class="error" ng-show="shippingForm.name.$error.required"> Please enter a name </span> </div> <h3>Address</h3> <div class="form-group"> <label>Street Address</label> <input name="street" class="form-control" ng-model="data.shipping.street" required /> <span class="error" ng-show="shippingForm.street.$error.required"> Please enter a street address </span> </div> <div class="text-center"> <button class="btn btn-primary">Complete order</button> </div> </div> </form>
AngularJS会给表单元素制定ng-valid和ng-invalid classes。Form元素总是会持有这些样式中的一个。
该CSS样式表明校验的效果。所以作者必须给每个元素添加一个name属性,使用AngularJS添加到scope的校验数据,控制error消息的可见性。
<input name="street"class="form-control" ng-model="
data.shipping.street" required /> <span class="error" ng-show="
shippingForm.street.$error.required"> Please enter a street address </span>
input元素,用于捕获用户的街道,将name属性指派为street。AngularJS在scope上创建一个shippingForm.street对象(它是form元素的name和input元素的name的结合)。该对象定义一个$error属性,该属性是一个对象,为每个校验属性提供一个属性,表示input元素失败的原因。如果shippingForm.street.$error.required属性为真,知道street input元素通过没有校验通过,用于显示error消息给用户的空间,会执行ng-show指令。
记住:作者用的简单,但AngularJS可以用于创建更复杂的校验。
2.1.3、将按钮链接到校验
在多数web应用,用户在提供所有表单数据,并校验通过后,才能进入下一步。当表单没有通过校验,作者想禁用Complete order按钮,并在用户完成表单属性后,自动启用它。
要做到这点,作者要改进AngularJS添加到scope的校验信息。作者可以得到整个表单的状态。当input元素为没有通过校验,shippingForm.$invalid属性为true,作者将基于此,使用ng-disabled指令,管理按钮元素的状态。作者将在第11章描述ng-disable指令。
<div class="text-center"> <button ng-disabled="shippingForm.$invalid" class="btn btn-primary">Complete order</button> </div>
2.2、添加剩下的表单字段
现在你知道AngularJS是怎么进行表单校验的了,作者要将剩下的input元素添加到表单。
<style> .ng-invalid { background-color: lightpink; } .ng-valid { background-color: lightgreen; } span.error { color: red; font-weight: bold; } </style> <h2>Check out now</h2> <p>Please enter your details, and we'll ship your goods right away!</p> <form name="shippingForm" novalidate> <div class="well"> <h3>Ship to</h3> <div class="form-group"> <label>Name</label> <input name="name" class="form-control" ng-model="data.shipping.name" required /> <span class="error" ng-show="shippingForm.name.$error.required"> Please enter a name </span> </div> <h3>Address</h3> <div class="form-group"> <label>Street Address</label> <input name="street" class="form-control" ng-model="data.shipping.street" required /> <span class="error" ng-show="shippingForm.street.$error.required"> Please enter a street address </span> </div> <div class="form-group"> <label>City</label> <input name="city" class="form-control" ng-model="data.shipping.city" required /> <span class="error" ng-show="shippingForm.city.$error.required"> Please enter a city </span> </div> <div class="form-group"> <label>State</label> <input name="state" class="form-control" ng-model="data.shipping.state" required /> <span class="error" ng-show="shippingForm.state.$error.required"> Please enter a state </span> </div> <div class="form-group"> <label>Zip</label> <input name="zip" class="form-control" ng-model="data.shipping.zip" required /> <span class="error" ng-show="shippingForm.zip.$error.required"> Please enter a zip code </span> </div> <div class="form-group"> <label>Country</label> <input name="country" class="form-control" ng-model="data.shipping.country" required /> <span class="error" ng-show="shippingForm.country.$error.required"> Please enter a country </span> </div> <h3>Options</h3> <div class="checkbox"> <label> <input name="giftwrap" type="checkbox" ng-model="data.shipping.giftwrap" /> Gift wrap these items </label> </div> <div class="text-center"> <button ng-disabled="shippingForm.$invalid" class="btn btn-primary">Complete order</button> </div> </div> </form>
提示:你可能会尝试使用ng-repeat指令,来生成input元素。这样生成的,不能很好滴工作,因为有些指令属性值,如ng-model,ng-show,是计算过的。作者建议这样做,单你想用更先进的技术,看第15-17章,作者会描述创建自定义指令的方式。
3、存储订单
本节,我们会扩展Deployd服务器提供的数据库,使用Ajax请求发送订单数据给服务器,在流程的最后,播放一个感谢信息。
3.1、扩展Deployd服务器
使用dashboard,选择collection,将集合的名字设为/orders,点击创建按钮。定义如下属性:
Name Type Required
name string Yes
street string Yes
city string Yes
state string Yes
zip string Yes
country string Yes
giftwrap boolean No
products array Yes
多花点注意,在gifwrap和products属性的类型上。
3.2、定义控制器行为
下一步要定义使用Ajax请求,发送订单给Deployd服务器的控制器行为。我可以以很多不同的方式定义该功能——创建一个服务或创建一个新的控制器。你可以以你喜欢的方式,构建,没有绝对的对与错。作者将保持一切都很简单,添加行为到顶级sportsStore控制器上,该控制器已经包含用Ajax请求加载产品数据的代码。
angular.module("sportsStore") .constant("dataUrl", "http://localhost:5500/products") .constant("orderUrl", "http://localhost:5500/orders") .controller("sportsStoreCtrl", function ($scope, $http, $location, dataUrl, orderUrl, cart) { $scope.data = { }; $http.get(dataUrl) .success(function (data) { $scope.data.products = data; }) .error(function (error) { $scope.data.error = error; }); $scope.sendOrder = function (shippingDetails) { var order = angular.copy(shippingDetails); order.products = cart.getProducts(); $http.post(orderUrl, order) .success(function (data) { $scope.data.orderId = data.id; cart.getProducts().length = 0; }) .error(function (error) { $scope.data.orderError = error; }).finally(function () { $location.path("/complete"); }); } });
Depolyd会在数据库中创建一个新的对象,来相应POST请求,并返回刚刚创建的对象,包括id属性。
作者定义了一个新的constant,制定URL,你会用于POST请求,并添加一个cart服务的依赖,以得到产品明细。作者添加到控制器的行为叫做sendOrder,它将用户的购物明细,作为参数。
作者使用angular.copy工具方法,他在第5章描述过,用来创建shipping details对象的拷贝,所以他可以安全地操作它,而不影响应用的其他部分。该shipping details对象的属性,通过ng-model指令创建,代表作者的orders Deployd集合。左右要做的,就是定义一个products属性,来引用购物车中的products数组。
作者使用$http.post方法,创建一个Ajax POST请求指定URL和数据,他使用success和error方法,在第5章介绍过,来相应请求的结果。
作者也使用在$http.post方法返回的promise上,使用then方法。该then方法,持有一个功能,无论Ajax请求的结果如何,都会被调用。无论发生什么,他都想要显示相同的视图给用户,所以他使用then方法,调用$location.path方法。这是编程的方式设置URL的path组建,它将会通过第7章创建的URL配置,触发视图改变。
3.3、调用控制器行为
要调用心控制器行为,必须添加ng-click指令到shipping details视图的button元素。
<div class="text-center"> <button ng-disabled="shippingForm.$invalid" ng-click="sendOrder(data.shipping)" class="btn btn-primary"> Complete order </button> </div
3.4、定义视图
作者定义的Ajax请求完成后的URL路径是/complete,该URL路由配置映射到/views/thankYou.html文件:
<div class="alert alert-danger" ng-show="data.orderError"> Error ({{data.orderError.status}}). The order could not be placed. <a href="#/placeorder" class="alert-link">Click here to try again</a> </div> <div class="well" ng-hide="data.orderError"> <h2>Thanks!</h2> Thanks for placing your order. We'll ship your goods as soon as possible. If you need to contact us, use reference {{data.orderId}}. </div>
该视图定义了两个不同的内容块,来处理Ajax请求的success和unsuccessful。如果发生error,error的明细会显示,通过一个a链接,让用户返回到shipping details视图,让他可以再试一次。如果请求成功,为用户显示thank-you消息,包含新订单对象的id。
4、做改进
以后会做改进的地方:
第一,当你加载app.html文件到浏览器,你可能注意到一个小的延迟,在视图显示和products的元素和分类被生成。这是因为Ajax请求在后台获取数据,在等待服务器返回数据旗舰,AngularJS继续执行应用,并显示视图,当数据抵达后,在更新。在第22章,作者将描述如何使用URL路由特性,让AngularJS在Ajax请求已经完成后,再显示视图。
第二,作者将产品数据中的分类,提取出来,用于导航和分页特性。在一个真实的项目中,作者会考虑,在产品数据第一次抵达时,生成该信息。在第20章,作者描述如何使用promises,来构建行为链。
最后,作者使用$animate服务,在第23章介绍,当URL路径改变时,在视图切换时使用过度动画。
4.1、避免优化陷阱
你注意到,作者说要考虑重用分类和分页数据。这是因为任何类型的优化,都要十分小心,来确保它是明智的,并避免两个主要的陷阱。
第一个陷阱是,过早地优化。
第二个陷阱是,translation优化。
5、管理产品分类
要完成SportsStore应用,作者要创建一个应用,来允许管理员管理产品分类的内容,和订单队列。这回允许作者,演示AngularJS如何用于执行create,read,update,delete操作。
记住:每个后端服务实现认证有不同的方式,但基本的前提是相同的:将用户的凭证,通过请求发送给制定URL,如果请求成功,浏览器会返回一个cookie,浏览器会在随后的请求中,自动发送该cookie,用以标识用户。本例中使用Deployd。
5.1、准备Deployd
给数据库做一些改变,有些事情只能管理员做。在这里,我们会使用Deployd定义一个管理员用户,并穿件访问策略。
Collection | Admin | User |
products | create,read,update,delete | read |
orders | create,read,update,delete | create |
总之,管理员能在任何集合上执行任何操作。一般用户可以read产品集合,并创建orders集合中的新对象。
在dashboard上创建Users Collection ,设置新容器的名字为/users。
用户集合,定义了id,username,password属性,是作者需要的。创建一个新对象,用户名密码为admin,secret。
5.1.1、集合的安全
作者喜欢Deployd的一个特性,是他定义一个简单的JavaScript API,可以用于实现服务端功能。当在一个集合上执行操作时,一系列的事件会被触发。点击products集合的Events,你会看到一系列的tab,代表不同的集合事件:On Get,On Validate,On Post,On Put,On Delete。所有的集合都有这些事件,你可以使用JavaScript,来加强认证策略。填入一下代码到On Put和On Delete tabs:
if (me === undefined || me.username != "admin") { cancel("No authorization", 401); }
在Deployd API中,变量me,代表当前用户,cancel功能,使用制定消息和HTTP状态吗,结束一个请求。该代码,允许有权限的用户,和管理员用户访问,单其他请求都会用401状态吗结束,这代表客户端是没有权限做请求。
提示:不要担心这些事件是什么,作者会在后面说。
重复这些步骤,在orders集合的事件里,处理On Post和On Validate。强制执行认证控制的事件:
Collection Description
products On Put, On Delete
orders On Get, On Put, On Delete
users None
5.2、创建管理应用
作者要为管理任务,创建一个分隔的AngularJS应用。作者可以在主应用中集成这些特性,但这将意味着所有用户都将下载admin功能的代码,即使他们永远用不上。作者添加一个新文件,叫admin.html,放到angularjs文件夹:
<!DOCTYPE html> <html ng-app="sportsStoreAdmin"> <head> <title>Administration</title> <script src="angular.js"></script> <script src="ngmodules/angular-route.js"></script> <link href="bootstrap.css" rel="stylesheet" /> <link href="bootstrap-theme.css" rel="stylesheet" /> <script> angular.module("sportsStoreAdmin", ["ngRoute"]) .config(function ($routeProvider) { $routeProvider.when("/login", { templateUrl: "/views/adminLogin.html" }); $routeProvider.when("/main", { templateUrl: "/views/adminMain.html" }); $routeProvider.otherwise({ redirectTo: "/login" }); }); </script> </head> <body> <ng-view /> </body> </html>
作者使用Module.config方法,创建了三个路由,让ng-view指令显示body元素。
URL Path View
/login /views/adminLogin.html
/main /views/adminMain.html
All others Redirects to /login
otherwise方法定义的路由,作者使用redirectTo选项,将URL路径改变到其他路由。这将让浏览器到/login路径,用于认证用户。作者将在第22章描述。
5.2.1、添加占位符视图
作者要实现认证特性,需要为/views/adminMain.html视图文件创建一些占位符内容,当认证成功时会显示。下面是adminMain.html文件的内容:
<div class="well"> This is the main view </div>
作者将在后面替换它。
5.3、实现认证
Deployd认证用户,使用标准HTTP请求。该应用发送一个POST请求到/users/login URL,包含username和password值。如果认证成功,服务器响应状态吗200,如果失败,返回401。要实现认证,作者要定义一个控制器,发起Ajax调用,并处理响应。下面是controllers/adminControllers.js文件的内容:
angular.module("sportsStoreAdmin") .constant("authUrl", "http://localhost:5500/users/login") .controller("authCtrl", function($scope, $http, $location, authUrl) { $scope.authenticate = function (user, pass) { $http.post(authUrl, { username: user, password: pass }, { withCredentials: true }).success(function (data) { $location.path("/main"); }).error(function (error) { $scope.authenticationError = error; }); } });
5.3.1、定义视图认证
5.4、定义主视图和控制器
5.5、实现订单特性
5.6、实现产品特性
5.6.1、定义RESTful控制器
5.6.2、定义视图
5.6.3、添加对HTML文件的引用