AngularJS Services for REST: Hiding the Ajax Requests

Using the $http service to consume a RESTful API is easy, and it provides a nice demonstration of how different AngularJS features can be combined to create applications. In terms of features, it works just fine, but there are serious problems when it comes to the design of the application that it produces.

The problem is that the local data and the behaviors that manipulate the data on the server are separate and care has to be taken to make sure that they stay synchronized. This runs counter to the way that AngularJS usually work, where data is propagated throughout the application via scopes and can be updated freely. To demonstrate the problem, I have added a new file to the angularjs folder called increment.js, which contains the module shown in Listing 21-9.

Listing 21-9. The Contents of the increments File

angular.module(“increment”, [])

.directive(“increment”, function () {

return {

restrict: “E”, scope: {

value: “=value”

},

link: function (scope, element, attrs) {

var button = angular.element(“<button>”).text(“+”);

button.addClass(“btn btn-primary btn-xs”);

element.append(button); button.on(“click”, function () {

scope.$apply(function () { scope.value++;

})

})

},

}

});

The module in this file, called increment, contains a directive, also called increment, that updates a value when the button is clicked. The directive is applied as an element and uses a two-way binding on an isolated scope to get its data value (a process that I described in Chapter 16). To use the module, I had to add a script element to the products.html file, as shown in Listing 21-10.

Listing 21-10. Adding a script Element to the products.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Products</title>

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

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

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

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

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

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-primary”>

<h3 class=”panel-heading”>Products</h3>

<ng-include src=”‘tableView.html'” ng-show=”displayMode == ‘list'”></ng-include>

<ng-include src=”‘editorView.html'” ng-show=”displayMode == ‘edit'”></ng-include>

</div>

</body>

</html>

I also had to add a dependency for the module in the products.js file, as shown in Listing 21-11.

Listing 21-11. Adding a Module Dependency in the products.js File

angular.module(“exampleApp”, [“increment”])

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

.controller(“defaultCtrl”, function ($scope, $http, baseUrl) {

And, finally, I had to apply the directive to the tableView.html file so that each row in the table has an increment button, as shown in Listing 21-12.

Listing 21-12. Applying the increment Directive to the tableView.html File

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

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

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

<td class=”text-right”>{{item.price | currency}}</td>

<td class=”text-center”>

<button class=”btn btn-xs btn-primary”

ng-click=”deleteProduct(item)”>

Delete

</button>

<button class=”btn btn-xs btn-primary”

ng-click=”editOrCreateProduct(item)”>

Edit

</button>

<increment value=”item.price” />

</td>

</tr>

The effect is shown in Figure 21-8. Clicking the + button increments the price property of the corresponding product object by 1.

The problem can be seen by clicking the Reload button, which replaces the local product data with fresh data from the server. The increment directive didn’t perform the required Ajax update when it incremented the price property, so the local data fell out of sync with the server data.

This may seem like a contrived example, but it arises frequently when using directives written by other developers or provided by a third-party. Even if the author of the increment directive knew that Ajax updates were required, they could not be performed because all of the Ajax update logic is contained in the controller and not accessible to a directive, especially one in another module.

The solution to this problem is to make sure that any changes to the local data automatically cause the required Ajax requests to be generated, but this means that any component that needs to work with the data has to know whether the data needs to be synchronized with a remote server and know how to make the required Ajax requests to perform updates.

AngularJS offers a partial solution to this problem through the $resource service, which makes it easier to work with RESTful data in an application by hiding away the details of the Ajax requests and URL formats. I’ll show you how to apply the $resource service in the sections that follow.

1. Installing the ngResource Module

The $resource service is defined within an optional module called ngResource that must be downloaded into the angularjs folder. Go to http://angularjs.org, click Download, select the version you require (version 1.2.5 is the latest version as I write this), and click the Extras link in the bottom-left corner of the window, as shown in Figure 21-9.

Download the angular-resource.js file into the angularjs folder. In Listing 21-13, you can see how I have added a script element for the new file to the products.html file.

Listing 21-13. Adding a Reference to the products.html File

<head>

<title>Products</title>

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

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

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

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

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

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

</head>

2. Using the $resource Service

In Listing 21-14, you can see how I have used the $resource service in in the products.js file to manage the data that I get from the server without directly creating Ajax requests.

Listing 21-14. Using the $resource Service in the products.js File

angular.module(“exampleApp”, [“increment”, “ngResource”])

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

.controller(“defaultCtrl”, function ($scope, $http, $resource, baseUrl) {

$scope.displayMode = “list”;

$scope.currentProduct = null;

$scope.productsResource = $resource(baseUrl + “: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.displayMode = “list”;

}

$scope.createProduct = function (product) {

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

$scope.products.push(newProduct);

$scope.displayNode = “list”;

});

}

$scope.updateProduct = function (product) {

product.$save();

$scope.displayMode = “list”;

}

$scope.editOrCreateProduct = function (product) {

$scope.currentProduct = product ? product : {};

$scope.displayNode = “edit”;

}

$scope.saveEdit = function (product) {

if (angular.isDefined(product.id)) {

$scope.updateProduct(product);

} else {

$scope.createProduct(product);

}

}

$scope.cancelEdit = function () {

if ($scope.currentProduct && $scope.currentProduct.$get) {

$scope.currentProduct.$get();

}

$scope.currentProduct = {};

$scope.displayMode = “list”;

}

$scope.listProducts();

});

The function signature for the behaviors defined by the controller have remained the same, which is good because it means I don’t have to change any of the HTML elements in order to use the $resource service. The implementation of every behavior has changed, not only because the way that I obtain the data has changed but also because the assumptions that can be made about the nature of the data are different. There is a lot going on in this listing, and the $resource service can be confusing, so I am going to break down what’s going on step-by-step in the sections that follow.

2.1. Configuring the $resource Service

The first thing I have to do is set up the $resource service so that it knows how to work with the RESTful Deployd service. Here is the statement that does this:

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

The $resource service object is a function that is used to describe the URLs that are used to consume the RESTful service. The URL segments that change per object are prefixed with a colon (the : character). If you look back to Table 21-4, you will see that for my example service there is only one variable part of the URL, and that is the id of the product object, which is required when deleting or modifying an object. For the first argument I combine the value of the baseUrl constant with :id to indicate a URL segment that will change, producing a combined value of the following:

http://localhost:5500/products/:id

The second argument is a configuration object whose properties specify where the value for the variable segment will come from. Each property must correspond to a variable segment from the first argument, and the value can be fixed or, as I have done in this example, bound to a property on the data object by prefixing a property name with the @ character.

Tip Most real applications will need multiple segment parts to express more complex data collections. The URL passed to the $resource service can contain as many variable parts as you require.

The result from calling the $resource service function is an access object that can be used to query and modify the server data using the methods that I have described in Table 21-5.

Tip The delete and remove methods are identical and can be used interchangeably.

Notice that the combination of HTTP methods and URLs in Table 21-5 is similar, but not identical, to the API defined by Deployd that I described in Table 21-4. Fortunately, Deployd is flexible enough to work around the differences, but later in the chapter, I’ll show you how to customize the configuration of the $resource service so that it matches exactly.

Tip I have shown the delete and remove methods as requiring a params argument. This is an object that contains additional parameters to be included in the URL sent to the server. All of the methods shown in the table can be used with an initial object like this, but because of an oddity in the $resource code, the delete and remove methods must be called this way, even if the params object has no properties and values.

Don’t worry if you don’t understand the role of actions at the moment; it will become clear soon.

2.2. Listing the REST Data

I assigned the access object returned from invoking the $resource service object to a variable called productResource, which I then use to get the initial snapshot of data from the server. Here is the definition of the listProducts behavior:

$scope.listProducts = function () {

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

}

The access object provides me with the means to query and modify data on the server, but it doesn’t automatically perform any of these actions itself, which is why I call the query method to get the initial data for the application. The query method requests the /products URL provided by my Deployd service to get all of the data objects available.

The result from the query method is a collection array that is initially empty. The $resource service creates the result array and then uses the $http service to make an Ajax request. When the Ajax request completes, the data that is obtained from the server is placed into the collection. This is such an important point that I am going to repeat it as a caution.

Caution The array returned by the query method is initially empty and is populated only when an asynchronous HTTP request to the server has completed.

The asynchronous delivery of the data works nicely with data bindings because they automatically update when the data arrives and the collection array is populated.

2.3. Modifying Data Objects

The query method populates the collection array with Resource objects, which define all of the properties specified in the data returned by the server and some methods that allow manipulation of the data without needing to use the collections array. Table 21-6 describes the methods that Resource objects define.

The $save method is the simplest to work with. Here is how I used it in the updateProduct behavior:

$scope.updateProduct = function (product) {

product.$save();

$scope.displayMode = “list”;

}

All of the Resource object methods perform asynchronous requests and return promise objects that you can use to receive notifications when the request completes or fails.

Note I am blithely assuming that all of my Ajax requests succeed in this example for the sake of simplicity, but you should take care to respond to errors in real projects.

The $get method is also pretty straightforward. I used it in this example to back out from abandoned edits in the cancelEdit behavior, as follows:

$scope.cancelEdit = function () {

if ($scope.currentProduct && $scope.currentProduct.$get) {

$scope.currentProduct.$get();

}

$scope.currentProduct = {};

$scope.displayMode = “list”;

}

Before I call the $get method, I check to see that it is available for me to call and the effect is to reset the edited object to the state stored on the server. This is a different approach to editing from the one I took when using the $http service, where I duplicated local data in order to have a reference point to which I could return when editing was cancelled.

2.4. Deleting Data Objects

The $delete and $remove methods generate the same requests to the server and are identical in every way. The wrinkle in their use is that they send the request to remove an object from the server but don’t remove the object from the collection array. This is a sensible approach, since the outcome of the request to the server isn’t known until the response is received and the application will be out of sync with the server if the local copy is deleted and the request subsequently returns an error.

To work around this, I have used the promise object that these methods return to register a callback handler that synchronizes the local data upon the successful deletion at the server in the deleteProduct behavior, as follows:

$scope.deleteProduct = function (product) {

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

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

});

$scope.displayMode = “list”;

}

2.5. Creating New Objects

Using the new keyword on the access object provides the means to apply the $resource methods to data objects so that they can be saved to the server. I use this technique in the createProduct behavior so that I can use the $save method and write new objects to the database:

$scope.createProduct = function (product) {

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

$scope.products.push(newProduct);

$scope.displayMode = “list”;

});

}

Rather like the $delete method, the $save method doesn’t update the collection array when new objects are saved to the server. I use the promise returned by the $save method to add the object to the collection array if the Ajax request is successful.

3. Configuring the $resource Service Actions

The get, save, query, remove, and delete methods that are available on the collection array and the $-prefixed equivalents on individual Resource objects are known as actions. By default, the $resource service defines the actions I described in Table 21-5, but these are easily configured so that the methods correspond to the API provided by the server. In Listing 21-15, you can see how I have changes the actions to match the Deployd API that I described in Table 21-4.

Listing 21-15. Modifying the Sresource Actions in the products.js File

$scope.productsResource = $resource(baseUrl + “:id”, { id: “i®id” },

{ create: { method: “POST” }, save: { method: “PUT” }});

The $resource service object function can be invoked with a third argument that defines actions. The actions are expressed as object properties whose names correspond to the action that is being defined, or redefined, since you can replace the default actions if need be.

Each action property is set to a configuration object. I have used only one property, method, which sets the HTTP method used for the action. The effect of my change is that I have defined a new action called create, which uses the POST method, and I have redefined the save action so that it uses the PUT method. The result is to make the actions supported by the productsResoures access object more consistent with the Deployd API, separating the requests for creating new objects from those that modify existing objects. Table 21-7 shows the set of configuration properties that can be used to define or redefine actions.

In addition, you can use the following properties to configure the Ajax request that the action will generate (I described the effect of these options in Chapter 20): transformRequest, transformResponse, cache, timeout, withCredentials, responseType, and interceptor.

Actions that are defined in this way are just like the defaults and can be called on the collection array and on individual Resource objects. In Listing 21-16, you can see how I updated the createProduct behavior to use my new create action. (No change is required for the other action I defined since it just changes the HTTP method used by the existing save action.)

Listing 21-16. Using a Custom Action in the products.js File

$scope.createProduct = function (product) {

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

$scope.products.push(newProduct);

$scope.displayMode = “list”;

});

}

4. Creating Sresource-Ready Components

Using the $resource service lets me write components that can operate on RESTful data without needing to know the details of the Ajax requests that are required to manipulate the data. In Listing 21-17, you can see how I have updated the increment directive from earlier in the chapter so that it can be configured to use data obtained from the $resource service.

Listing 21-17. Working with RESTful Data in the increments File

angular.module(“increment”, [])

.directive(“increment”, function () {

return {

restrict: “E”,

scope: {

item: “=item”,

property: “@propertyName”,

restful: “@restful”,

method: “@methodName”

},

link: function (scope, element, attrs) {

var button = angular.element(“<button>”).text(“+”);

button.addClass(“btn btn-primary btn-xs”);

element.append(button); button.on(“click”, function () {

scope.$apply(function () {

scope.item[scope.property]++j

if (scope.restful) {

scope.item[scope.method]();

}

})

})

},

}

});

When creating components that may operate on data provided by the $resource service, you need to provide configuration options not only to enable the RESTful support but also to specify the action method or methods that are required to update the server. In this example, I use the value of an attribute called restful to configure the REST support and method to get the name of the method that should be called when the value is incremented. In Listing 21-18, you can see how I apply these changes in the tableView.html file.

Listing 21-18. Adding Configuration Attributes in the tableView.html File

<div class=”panel-body”>

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

<thead>

<tr>

<th>Name</th>

<th>Category</th>

<th class=”text-right”>Price</th>

<th></th>

</tr>

</thead>

<tbody>

<tr ng-repeat=”item in products’^

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

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

<td class=”text-right”>{{item.price | currency}}</td>

<td class=”text-center”>

<button class=”btn btn-xs btn-primary”

ng-click=”deleteProduct(item)”>

Delete

</button>

<button class=”btn btn-xs btn-primary”

ng-click=”editOrCreateProduct(item)”>

Edit

</button>

<increment item=”item” property-name=”price” restful=”true”

method-name=”$save” />

</td>

</tr>

</tbody>

</table>

<div>

<button class=”btn btn-primary” ng-click=”listProducts()”>Refresh</button>

<button class=”btn btn-primary” ng-click=”editOrCreateProduct()”>New</button>

</div>

</div>

The result is that when you click the + button in a table row, the local value is updated, and the $save method is then called to send the update to the server.

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 *