SportsStore – Orders and Administration with AngularJS: Administering the Product Catalog

To complete the SportsStore application, I am going to create an application that will allow the administrator to manage the contents of the product catalog and the order queue. This will allow me to demonstrate how AngularJS can be used to perform create, read, update, and delete (CRUD) operations and reinforce the use of some key features from the main SportsStore application.

Note Every back-end service implements authentication in a slightly different way, but the basic premise is the same: Send a request with the user’s credentials to a specific URL, and if the request is successful, the browser will return a cookie that the browser will automatically send with subsequent requests to identify the user. The examples in this section are specific to Deployd, but they will translate easily to most platforms.

1. Preparing Deployd

Making changes to the database is something that only administrators should be able to do. To that end, I am going to use Deployd to define an administrator user and create the access policy described by Table 8-2.

In short, the administrator should be able to perform any operation on any collection. The normal users should be able to read (but not modify) the products collection and create new objects in the orders collection (but not be able to see, modify, or delete them).

Click the large green button in the Deployd dashboard and select Users Collection from the pop-up menu. Set the name of the new collection to be /users, as shown in Figure 8-7.

Click the Create button. Deployd will create the collection and display the property editor that I used to define the objects in the other collections. User collections are defined with id, username, and password properties, which are all that I need for this application. Click the Data button for the /users collection and create a new object with a username value of admin and a password of secret, as shown in Figure 8-8.

1.1. Securing the Collections

One of the features that I like about Deployd is that it defines a simple JavaScript API that can be used to implement server-side functionality, a series of events that are triggered when operations are performed on a collection. Click the products collection in the console and then click Events. You will see a series of tabs that represent different collection events: On Get, On Validate, On Post, On Put, and On Delete. These events are defined for all collections, and one of the many things you can do is use JavaScript to enforce an authorization policy. Enter the following JavaScript into the On Put and On Delete tabs:

if (me === undefined || me.username != “admin”) {

cancel(“No authorization”, 401);

}

In the Deployd API, the variable me represents the current user, and the cancel function terminates a request with the specified message and HTTP status code. This code allows access when there is an authenticated user and when that user is admin but terminates all other requests with a 401 status code, which indicates that the client is unauthorized to make the request.

Tip Don’t worry about what the On XXX tabs relate to at the moment; it will become clear when I start making Ajax requests to the server.

Repeat the process for all the Events tabs in the orders collection, except for the On Post and On Validate tabs. Table 8-3 summarizes which collection tabs require the code shown earlier. The other tabs should be empty.

2. Creating the Admin Application

I am going to create a separate AngularJS application for the administration tasks. I could integrate these features into the main application, but that would mean all users would be required to download the code for the admin functions, even though most of them would never use it. I added a new file called admin.html to the angularjs folder, the contents of which are shown in Listing 8-9.

Listing 8-9. The Contents of the admin.html File

<!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>

This HTML file contains the script and link elements required for the AngularJS and Bootstrap files and an inline script element that defines the sportsStoreAdmin module, which will contain the application functionality (and which I have applied to the html element using the ng-app directive). I have used the Module.config method to create three routes for the application, which drive the ng-view directive in the body element. Table 8-4 summarizes the paths that the URLs match and the view files that they load.

For the route defined with the otherwise method, I used the redirectTo option, which changes the URL path to another route. This has the effect of moving the browser to the /login path, which is the one that I will use to authenticate the user. I describe the complete set of configuration options that you can use with URL routes in Chapter 22.

2.1. Adding the Placeholder View

I am going to implement the authentication feature first, but I need to create some placeholder content for the /views/adminMain.html view file so that I have something to show when authentication is successful. Listing 8-10 shows the (temporary) contents of the file.

Listing 8-10. The Contents of the adminMain.html File

<div class=”well”>

This is the main view

</div>

I’ll replace this placeholder with useful content once the application is able to authenticate users.

3. Implementing Authentication

Deployd authenticates users using standard HTTP requests. The application sends a POST request to the /users/login URL, which includes username and password values for the authenticating user. The server responds with status code 200 if the authentication attempt is successful and code 401 when the user cannot be authenticated. To implement authentication, I started by defining a controller that makes the Ajax calls and deals with the response. Listing 8-11 shows the contents of the controllers/adminControllers.js file, which I created for this purpose.

Listing 8-11. The Contents of the adminControllers.js File

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;

});

}

});

I use the angular.module method to extend the sportsStoreAdmin module that is created in the admin.html file.

I use the constant method to specify the URL that will be used for authentication and create an authCtrl controller that defines a behavior called authenticate that receives the username and password values as arguments and makes an Ajax request to the Deployd server with the $http.post method (which I describe in Chapter 20). I use the $location service, which I describe in Chapter 11, to programmatically change the path displayed by the browser (and so trigger a URL route change) if the Ajax request is successful.

Tip I have supplied an optional configuration object to the $http.post method, which sets the withCredentials option to true. This enables support for cross-origin requests, which allows Ajax requests to work with cookies that deal with authentication. Without this option enabled, the browser will ignore the cookie that Deployd returns as part of the authentication response and expects to see in subsequent requests. I describe all the options that you can use with the $http service in Chapter 20.

If the server returns an error, I assign the object passed to the error function to a scope variable so that I can display details of the problem to the user. I need to include the JavaScript file that contains the controller in the admin. html file, taking care to ensure that it appears after the script element that defines the module that is being extended. Listing 8-12 shows the change to the admin.html file.

Listing 8-12. Adding a script Element for a Controller to the admin.html File

<!DOCTYPE html>

<html ng-app=”sportsStoreAdmin”>

<head>

<title>Administration</title>

<script src=”angular.js”></script>

<script src=”ngmodules/angular-route.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“sportsStoreAdmin”, [“ngRoute”])

.config(function ($routeProvider) {

$routeProvider.when(“/login”, {

templateUrl: “/views/adminLogin.html”

});

$routeProvider.when(“/main”, {

templateUrl: “/views/adminMain.html”

});

$routeProvider.otherwise({

redirectTo: “/login”

});

});

</script>

<script src=”controllers/adminControllers.js”></script>

</head>

<body>

<ng-view />

</body>

</html>

3.1. Defining the Authentication View

The next step is to create the view that will allow the user to enter a username and password, invoke the authenticate behavior defined by the authCtrl controller, and display details of any errors. Listing 8-13 shows the contents of the views/adminLogin.html file.

Listing 8-13. The Contents of the adminLogin.html File

<div class=”well” ng-controller=”authCtrl”>

<div class=”alert alert-info” ng-hide=”authenticationError”>

Enter your username and password and click Log In to authenticate

</div>

<div class=”alert alert-danger” ng-show=”authenticationError”>

Authentication Failed ({{authenticationError.status}}). Try again.

</div>

form name=”authForm” novalidate>

<div class=”form-group”>

<label>Username</label>

<input name=”username” class=”form-control” ng-model=”username” required />

</div>

<div class=”form-group”>

<label>Password</label>

<input name=”password” type=”password” class=”form-control” ng-model=”password” required />

</div>

<div class=”text-center”>

<button ng-click=”authenticate(username, password)” ng-disabled=”authForm.$invalid” class=”btn btn-primary”>

Log In

</button>

</div>

</form>

</div>

This view uses techniques I introduced for the main SportsStore application and that I describe in depth in later chapters. I use the ng-controller directive to associate the view with the authCtrl controller. I use the AngularJS support for forms and validation (Chapter 12) to capture the details from the user and prevent the Log In button from being clicked until values for both the username and password have been entered. I use the ng-model directive (Chapter 10) to assign the values entered to the scope. I use the ng-show and ng-hide directives (Chapter 11) to prompt the user to enter credentials and to report on an error. Finally, I use the ng-click directive (Chapter 11) to invoke the authenticate behavior on the controller to perform authentication.

You can see how the view is displayed by the browser in Figure 8-9. To authenticate, enter the username (admin) and password (secret) that Deployd is expecting and click the button.

4. Defining the Main View and Controller

Once the user is authenticated, the ng-view directive displays the adminMain.html view. This view will be responsible for allowing the administrator to manage the contents of the product catalog and see the queue of orders.

Before I start to define the functionality that will drive the application, I need to define placeholder content for the views that will display the list of products and orders. First, I created views/adminProducts.html, the content of which is shown in Listing 8-14.

Listing 8-14. The Contents of the adminProducts.html File

<div class=”well”>

This is the product view

</div>

Next, I create the views/adminOrders.html file, for which I have defined a similar placeholder, as shown in Listing 8-15.

Listing 8-15. The Contents of the adminOrders.html File

<div class=”well”>

This is the order view

</div>

I need the placeholders so I can demonstrate the flow of views in the admin application. The URL routing feature has a serious limitation: You can’t nest multiple instances of the ng-view directive, which makes it slightly more difficult to arrange to display different views within the scope of ng-view. I am going to demonstrate how to address this using the ng-include directive as a slightly less elegant—but perfectly functional—alternative. I started by defining a new controller in the adminControllers.js file, as shown in Listing 8-16.

Listing 8-16. Adding a New Controller in the adminControllers.js File

angular.module(“sportsStoreAdmin”)

.constant(“authllrl”, “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;

});

}

})

.controller(“mainCtrl”, function($scope) {

$scope.screens = [“Products”, “Orders”];

$scope.current = $scope.screens[0];

$scope.setScreen = function (index) {

$scope.current = $scope.screens[index];

}>

$scope.getScreen = function () {

return $scope.current == “Products”

? “/views/adminProducts.html” : “/views/adminOrders.html”;

};

});

The new controller is called mainCtrl, and it provides the behaviors and data I need to use the ng-include directive to manage views, as well as generate the navigation buttons that will switch between the views. The setScreen behavior is used to change the displayed view, which is exposed through the getScreen behavior.

You can see how the controller functionality is consumed in Listing 8-17, which shows how I have revised the adminMain.html file to remove the placeholder functionality.

Listing 8-17. Revising the adminMain.html File

<div class=”panel panel-default row” ng-controller=”mainCtrl”>

<div class=”col-xs-3 panel-body”>

<a ng-repeat=”item in screens” class=”btn btn-block btn-default”

ng-class=”{‘btn-primary’: item == current }” ng-click=”setScreen($index)”>

{{item}}

</a>

</div>

<div class=”col-xs-8 panel-body” >

<div ng-include=”getScreen()” />

</div>

</div>

This view uses the ng-repeat directive to generate a elements for each value in the scope screens array. As I explain in Chapter 10, the ng-repeat directive defines some special variables that can be referred to within the elements it generates, and one of those, $index, returns the position of the current item in the array. I use this value with the ng-click directive, which invokes the setScreen controller behavior.

The most important part of this view is the use of the ng-include directive, which I introduced in Chapter 7 to display a single partial view and which I describe properly in Chapter 10. The ng-include directive can be passed a behavior that is invoked to obtain the name of the view that should be displayed, as follows:

<div ng-include=”getScreen()” />

I have specified the getScreen behavior, which maps the currently selected navigation value to one of the views I defined at the start of this section. You can see the buttons that the ng-repeat directive generates—and the effect of clicking them—in Figure 8-10. This isn’t as elegant or robust as using the URL routing feature, but it is functional and is a useful technique in complex applications where a single instance of the ng-view directive doesn’t provide the depth of control over views that is required.

5. Implementing the Orders Feature

I am going to start with the list of orders, which is the simplest to deal with because I am only going to display a read-only list. In a real e-commerce application, orders would go into a complex workflow that would involve payment validation, inventory management, picking and packing, and—ultimately—shipping the ordered products. As I explained in Chapter 6, these are not features you would implement using AngularJS, so I have omitted them from the SportsStore application. With that in mind, I have added a new controller to the adminControllers.js file that uses the $http service to make an Ajax GET request to Deployd to get the orders, as shown in Listing 8-18.

Listing 8-18. Adding a Controller to Obtain the Orders in the adminControllers.js File

angular.module(“sportsStoreAdmin”)

.constant(“authllrl”, “http://localhost:5500/users/login”)

.constant(“ordersUrl”, “http://localhost:5500/orders”)

.controller(“authCtrl”, function ($scope, $http, $location, authUrl) {

// …controller statements omitted for brevity…

})

.controller(“mainCtrl”, function ($scope) {

// …controller statements omitted for brevity…

})

.controller(“ordersCtrl”, function ($scope, $http, ordersUrl) {

$http.get(ordersUrl, {withCredentials : true})

.success(function (data) {

$scope.orders = data;

})

.error(function (error) {

$scope.error = error;

});

$scope.selectedOrder;

$scope.selectOrder = function(order) {

$scope.selectedOrder = order;

};

$scope.calcTotal = function(order) {

var total = 0;

for (var i = 0; i < order.products.length; i++) {

total +=

order.products[i].count * order.products[i].price;

}

return total;

}

});

I have defined a constant that defines the URL that will return a list of the orders stored by the server. The controller function makes an Ajax request to that URL and assigns the data objects to the orders property on the scope or, if the request is unsuccessful, assigns the error object. Notice that I set the withCredentials configuration option when calling the $http.get method, just as I did when performing authentication. This ensures that the browser includes the security cookie back to Deployd to authenticate the request.

The rest of the controller is straightforward. The selectOrder behavior is called to set a selectedOrder property, which I will use to zoom in on the details of an order. The calcTotal behavior works out the total value of the products in an order.

To take advantage of the ordersCtrl controller, I have removed the placeholder content from the adminOrders. html file and replaced it with the markup shown in Listing 8-19.

Listing 8-19. The Contents of the adminOrders.html File

<div ng-controller=”ordersCtrl”>

<table class=”table table-striped table-bordered”>

<tr><th>Name</th><th>City</th><th>Value</th><th></th></tr>

<tr ng-repeat=”order in orders”>

<td>{{order.name}}</td>

<td>{{order.city}}</td>

<td>{{calcTotal(order) | currency}}</td>

<td>

<button ng-click=”selectOrder(order)” class=”btn btn-xs btn-primary”>

Details

</button>

</td>

</tr>

</table>

<div ng-show=”selectedOrder”>

<h3>Order Details</h3>

<table class=”table table-striped table-bordered”>

<tr><th>Name</th><th>Count</th><th>Price</th></tr>

<tr ng-repeat=”item in selectedOrder.products”>

<td>{{item.name}}</td>

<td>{{item.count}}</td>

<td>{{item.price| currency}} </td>

</tr>

</table>

</div>

</div>

The view consists of two table elements. The first table shows a summary of the orders, along with a button element that invokes the selectOrder behavior to focus on the order. The second table is visible only once an order has been selected and displays details of the products that have been ordered. You can see the result in Figure 8-11.

6. Implementing the Products Feature

For the products feature, I am going to perform a full range of operations on the data so that the administrator not only can see the products but create new ones and edit and delete existing ones. If you turn to the Deployd dashboard, select the Products collection, and click the API button, you will see details of the RESTful API that Deployd provides for working with data using HTTP requests. I get into the details of RESTful APIs properly in Chapter 21, but the short version is that the data object you want is specified using the URL, and the operation you want to perform is specified by the HTTP method of the request sent to the server. So, for example, if I want to delete the object whose id attribute is 100, I would sent a request to the server using the DELETE HTTP method and the URL /products/100.

You can use the $http service to work with a RESTful API, but doing so means you have to expose the complete set of URLs that are used to perform operations throughout the application. You can do this by defining a service that performs the operations for you, but a more elegant alternative is to use the $resource service, defined in the optional ngResource module, which also has a nice way of dealing with defining the URLs that are used to send requests to the server.

6.1. Defining the RESTful Controller

I am going to start by defining the controller that will provide access to the Deployd RESTful API via the AngularJS $resource service. I created a new file called adminProductController.js in the controllers folder and used it to define the controller shown in Listing 8-20.

Listing 8-20. The Contents of the adminProductController.js File

angular.module(“sportsStoreAdmin”)

.constant(“productUrl”, “http://localhost:5500/products/”)

.config(function($httpProvider) {

$httpProvider.defaults.withCredentials = true;

})

.controller(“productCtrl”, function ($scope, $resource, productUrl) {

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

$scope.listProducts = function () {

$scope.products = $scope.productsResource.query();

}

$scope.deleteProduct = function (product) {

product.$delete().then(function () {

$scope.products.splice($scope.products.indexOf(product), 1);

});

}

$scope.createProduct = function (product) {

new $scope.productsResource(product).$save().then(function (newProduct) {

$scope.products.push(newProduct);

$scope.editedProduct = null;

});

}

$scope.updateProduct = function (product) {

product.$save();

$scope.editedProduct = null;

}

$scope.startEdit = function (product) {

$scope.editedProduct = product;

}

$scope.cancelEdit = function () {

$scope.editedProduct = null;

}

$scope.listProducts();

});

I am not going to go too deeply into the code for this listing because I cover the topic fully in Chapter 21. But there some important themes that are worth explaining now, so I’ll cover just the highlights.

First, the $resource service is built on top of the features provided by the $http service, and that means I need to enable the withCredentials option that I used earlier to get authentication to work properly. I don’t have access to the requests made by the $http service, but I can change the default settings for all Ajax requests by calling the config method on the module and declaring a dependency on the provider for the $http service, like this:

.config(function($httpProvider) {

$httpProvider.defaults.withCredentials = true;

})

As I explain in Chapter 18, services can be created in several different ways, and one option includes defining a provider object that can be used to change the way that the service works. In this case, the provider for the $http service, which is called $httpProvider, defines a defaults property that can be used to configure settings for all Ajax requests. See Chapter 20 for details of the default values that can be set through the $httpProvider object.

The most important part of this example, however, is the statement that creates the access object that provides access to the RESTful API:

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

The first argument passed into the $resource call defines the URL format that will be used to make queries. The :id part, which corresponds to the map object that is the second argument, tells AngularJS that if the data object it is working with has an id property, then it should be appended to the URL used for the Ajax request.

The URLs and HTTP methods that are used to access the RESTful API are inferred from these two arguments, which means I don’t have to make individual Ajax calls using the $http service.

The access object that is the result from using the $resource service has query, get, delete, remove, and save methods that are used to obtain and operate on the data from the server (methods are also defined on individual data objects, as I explain in Chapter 21). Calling these methods triggers the Ajax request that performs the required operation.

Tip The methods defined by the access object don’t quite correspond to the API defined by Deployd, although Deployd is flexible enough to accept the requests that the $resource service makes. In Chapter 21, I show you how you can change the $resource configuration to fully map onto any RESTful API.

Most of the code in the controller presents these methods to the view in a useful way that works around a wrinkle in the $resource implementation. The collection of data objects returned by the query method isn’t automatically updated when objects are created or deleted, so I have to include code to take care of keeping the local collection in sync with the remote changes.

Tip The access object doesn’t automatically load the data from the server, which is why I call the query method directly at the end of the controller function.

6.2. Defining the View

To take advantage of the functionality defined by the controller, I have replaced the placeholder content in the adminProducts.html view with the markup shown in Listing 8-21.

Listing 8-21. The Contents of the adminProduct.html File

<style>

#productTable { width: auto; }

#productTable td { max-width: 150px; text-overflow: ellipsis;

overflow: hidden; white-space: nowrap; }

#productTable td input { max-width: 125px; }

</style>

<div ng-controller=”productCtrl”>

<table id=”productTable” class=”table table-striped table-bordered”>

<tr>

<th>Name</th><th>Description</th><th>Category</th><th>Price</th><th></th>

</tr>

<tr ng-repeat=”item in products” ng-hide=”item.id == editedProduct.id”>

<td>{{item.name}}</td>

<td class=”description”>{{item.description}}</td>

<td>{{item.category}}</td>

<td>{{item.price | currency}}</td>

<td>

<button ng-click=”startEdit(item)” class=”btn btn-xs btn-primary”>

Edit

</button>

<button ng-click=”deleteProduct(item)” class=”btn btn-xs btn-primary”>

Delete

</button>

</td>

</tr>

<tr ng-class=”{danger: editedProduct}”>

<td><input ng-model=”editedProduct.name” required /></td>

<td><input ng-model=”editedProduct.description” required /></td>

<td><input ng-model=”editedProduct.category” required /></td>

<td><input ng-model=”editedProduct.price” required /></td>

<td>

<button ng-hide=”editedProduct.id”

ng-click=”createProduct(editedProduct)”

class=”btn btn-xs btn-primary”>

Create

</button>

<button ng-show=”editedProduct.id”

ng-click=”updateProduct(editedProduct)”

class=”btn btn-xs btn-primary”>

Save

</button>

<button ng-show=”editedProduct”

ng-click=”cancelEdit()”

class=”btn btn-xs btn-primary”>

Cancel

</button>

</td>

</tr>

</table>

</div>

There are no new techniques in this view, but it shows how AngularJS directives can be used to manage a stateful editor view. The elements in the view use the controller behaviors to manipulate the collection of product objects, allowing the user to create new products and edit or delete existing products.

6.3. Adding the References to the HTML File

All that remains is to add script elements to the admin.html file to import the new module and the new controller and to update the main application module so that it declares a dependency on ngResource, as shown in Listing 8-22.

Listing 8-22. Adding the References to the admin.html File

<!DOCTYPE html>

<html ng-app=”sportsStoreAdmin”>

<head>

<title>Administration</title>

<script src=”angular.js”></script>

<script src=”ngmodules/angular-route.js”></script>

<script src=”ngmodules/angular-resource.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“sportsStoreAdmin”, [“ngRoute”, “ngResource”])

.config(function ($routeProvider) {

$routeProvider.when(“/login”, {

templateUrl: “/views/adminLogin.html”

});

$routeProvider.when(“/main”, {

templateUrl: “/views/adminMain.html”

});

$routeProvider.otherwise({

redirectTo: “/login”

});

});

</script>

<script src=”controllers/adminControllers.js”></script>

<script src=”controllers/adminProductController.js”></script>

</head>

<body>

<ng-view />

</body>

</html>

You can see the effect in Figure 8-12. The user creates a new product by filling in the input elements and clicking the Create button, modifies a product by clicking one of the Edit buttons, and removes a product using one of the Delete buttons.

Source: Freeman Adam (2014), Pro AngularJS (Expert’s Voice in Web Development), Apress; 1st ed. edition.

Leave a Reply

Your email address will not be published. Required fields are marked *