AngularJS Services for Making Ajax Requests

The $http service is used to make and process Ajax requests, which are standard HTTP requests that are performed asynchronously. Ajax is at the heart of modern web applications, and the ability to request content and data in the background while the user interacts with the rest of the application is an important way of creating a rich user experience. To demonstrate making an Ajax request using the $http service, I have created a simple example application that, as yet, doesn’t have any data. Listing 20-2 shows the contents of the ajax.html file, which I added to the angularjs folder.

Listing 20-2. An Application Without Data in the ajax.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Ajax</title>

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

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

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

<script>

angular.module(“exampleApp”, [])

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

$scope.loadData = function () {

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<div class=”panel-body”>

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

<thead><tr><th>Name</th><th>Category</th><th>Price</th></tr></thead>

<tbody>

<tr ng-hide=”products.length”>

<td colspan=”3″ class=”text-center”>No Data</td>

</tr>

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

<td>{{name}}</td>

<td>{{category}}</td>

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

</tr>

</tbody>

</table>

<p><button class=”btn btn-primary”

ng-click=”loadData()”>Load Data</button></p>

</div>

</div>

</body>

</html>

The example consists of a table with a placeholder row that uses the ng-hide directive to control its visibility, based on the number of items in a scope array called products. The data array is not defined by default and so the placeholder is displayed. The table includes a row to which I have applied the ng-repeat directive, which will generate a row for each product data object when the array is defined.

I have added a button that uses the ng-click directive to call a controller behavior called loadData. The behavior is currently defined as an empty function, but this is where I will make the Ajax request using the $http service. You can see the initial state of the example application in Figure 20-1; currently, clicking the button has no effect.

I want to show the application before and after I use the $http service to emphasize just how little additional code is required to make the Ajax request and process the response. Listing 20-3 shows the ajax.html file after I used the $http service.

Listing 20-3. Using the $http Service to Make an Ajax Request in the ajax.htm File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Ajax</title>

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

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

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

<script>

angular.module(“exampleApp”, [])

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

$scope.loadData = function () {

$http.get(“productData.json”).success(function (data) {

$scope.products = data;

});

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<div class=”panel-body”>

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

<thead><tr><th>Name</th><th>Category</th><th>Price</th></tr></thead>

<tbody>

<tr ng-hide=”products.length”>

<td colspan=”3″ class=”text-center”>No Data</td>

</tr>

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

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

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

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

</tr>

</tbody>

</table>

<p><button class=”btn btn-primary”

ng-click=”loadData()”>Load Data</button></p>

</div>

</div>

</body>

</html>

I have declared a dependency on the $http service and added three lines of code. One of the differences in working with Ajax in an AngularJS application as opposed to, say, jQuery, is that you apply the data that you obtain from the server to the scope, which then automatically refreshes its bindings to update the HTML elements in the application. As a consequence, the code that would be required in a jQuery application to process the data and manipulate the DOM to display it is not required. Despite this, the basic mechanism for making Ajax requests will be familiar to you if you have used jQuery. There are two stages—making the request and receiving the response—that I describe in the following sections.

1. Making the Ajax Request

There are two ways to make a request using the $http service. The first—and most common—is to use one of the convenience methods that the service defines, which I have described in Table 20-2 and which allows you to make requests using the most commonly needed HTTP methods. All of these methods accept an optional configuration object, which I describe in the “Configuring Ajax Requests” section later in this chapter.

The other way to make an Ajax request is to treat the $http service object as a function and pass in a configuration object. This is useful when you require one of the HTTP methods for which there is not a convenience method available. You pass in a configuration object (which I describe later in this chapter) that includes the HTTP method you want to use. I’ll show you how to make Ajax requests in this way in Chapter 21 when I talk about RESTful services, but in this chapter I am going to focus on the convenience methods.

From the table, you can see that I made a GET request without a configuration object in Listing 20-3, as follows:

$http.get(“productData.json”)

For the URL, I specified productData.json. A URL like this will be a requested relative to the main HTML document, which means that I don’t have to hard-code protocols, hostnames, and ports into the application.

2. Receiving Ajax Responses

Making a request is only the first part of the Ajax process, and I also have to receive the response when it is ready.

The A in Ajax stands for asynchronous, which means that the request is performed in the background, and you will be notified when a response from the server is received at some point in the future.

AngularJS uses a JavaScript pattern called promises to represent the result from an asynchronous operation, such as an Ajax request. A promise is an object that defines methods that you can use to register functions that will be invoked when the operation is complete. I’ll get into promises in more detail when I describe the $q service later in this chapter, but the promise objects returned from the $http methods in Table 20-2 define the methods shown in Table 20-3.

The success and error methods pass their functions a simplified view of the response from the server. The success function is passed the data that the server sends, and the error function is passed a string that describes the problem that occurred. Further, if the response from the server is JSON data, then AngularJS will parse the JSON to create JavaScript objects and pass them to the success function automatically.

It is this feature that I used in Listing 20-3 to receive the data in the productData.json file and add it to the scope, as follows:

$http.get(“productData.json”).success(function (data) {

$scope.products = data;

});

Within my success function, I assign the data object that AngularJS has created from the JSON response to the products property on the scope. This has the effect of removing the placeholder row in the table and causing the ng-repeat directive to generate rows for each item received from the server, as shown in Figure 20-2.

Tip The result from both the success and error methods is the promise object itself, which allows calls to these methods to be chained together in a single statement.

2.1. Getting More Response Details

Using the then method on the promise object allows you to register both a success and error function in a single method call. But also, more importantly, it provides access to more detailed information about the response from the server. The object that the then method passes to its success and error functions defines the properties I have described in Table 20-4.

In Listing 20-4, you can see how I have used the then method to register a success function (the error function is optional) and write some of the response details to the console.

Listing 20-4. Using the promise.then Method in the ajax.html File

<script>

angular.module(“exampleApp”, [])

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

$scope.loadData = function () {

$http.get(“productData.json”).then(function (response) {

console.log(“Status: ” + response.status);

console.log(“Type: ” + response.headers(“content-type”));

console.log(“Length: ” + response.headers(“content-length”));

$scope.products = response.data;

});

}

});

</script>

In this example, I write the HTTP status code and the Content-Type and Content-Length headers to the console, which produces the following output when the button is clicked:

Status: 200

Type: application/json

Length: 434

AngularJS still automatically processes JSON data when using the then method, which means I can simply assign the value of the data property from the response object to the products property on the controller scope.

2.2. Processing Other Data Types

Although obtaining JSON data is the most common use for the $http service, you may not always have the luxury of working with a data format that AngularJS will process automatically. If this is the case, then AngularJS will pass the success function an object containing the properties shown in Table 20-4, and you will be responsible for parsing the data. To give you a simple example of how this works, I created a simple XML file called productData.xml that contains the same product information as the previous example, but expressed as a fragment of XML. You can see the contents of the productData.xml file in Listing 20-5.

Listing 20-5. The Contents of the productData.xml File

<products>

<product name=”Apples” category=”Fruit” price=”1.20″ expiry=”10″ />

<product name=”Bananas” category=”Fruit” price=”2.42″ expiry=”7″ />

<product name=”Pears” category=”Fruit” price=”2.02″ expiry=”10″ />

<product name=”Tuna” category=”Fish” price=”20.45″ expiry=”3″ />

<product name=”Salmon” category=”Fish” price=”17.93″ expiry=”2″ />

<product name=”Trout” category=”Fish” price=”12.93″ expiry=”4″ />

</products>

The XML fragment defines a products element that contains a set of product elements, each of which uses attribute values to describe a single product. This is typical of the kind of XML that I find myself handling when dealing with older content management systems: The XML is expressed as schema-less fragments, but it is well-formed and consistently generated. In Listing 20-6, you can see how I have updated the ajax.html file to request and process the XML data.

Listing 20-6. Working with an XML Fragment in the ajax.html File

<script>

angular.module(“exampleApp”, [])

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

$scope.loadData = function () {

$http.get(“productData.xml”).then(function (response) {

$scope.products = [];

var productElems = angular.element(response.data.trim()).find(“product”);

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

var product = productElems.eq(i);

$scope.products.push({

name: product.attr(“name”),

category: product.attr(“category”),

price: product.attr(“price”)

});

}

});

}

});

</script>

XML and HTML are closely related—so much so that there is a version of the HTML specification that complies with XML called XHTML. The practical effect of this similarity is that you can use jqLite to process XML fragments as though they were HTML, which is the technique that I have used in this example.

The data property of the object passed to the success function returns the contents of the XML file, which I wrap in a jqLite object using the angular.element method. I then use the find method to locate the product elements and use a for loop to enumerate them and extract the attribute values. I described all of the jqLite methods in this example in Chapter 15.

3. Configuring Ajax Requests

The methods defined by the $http service all accept an optional argument of an object containing configuration settings. For most applications, the default configuration used for Ajax requests will be fine, but you can adjust the way the requests are made by defining properties on the configuration object corresponding to Table 20-5.

The most interesting configuration feature is the ability to transform the request and response through the aptly named transformRequest and transformResponse properties. AngularJS defines two built-in transformations; outgoing data is serialized into JSON, and incoming JSON data is parsed into JavaScript objects.

3.1. Transforming a Response

You can transform a response by assigning a function to the transformResponse property of the configuration object. The transform function is passed the data from the response and a function that can be used to obtain header values. The function is responsible for returning a replacement version of the data, which is usually a deserialized version of the format sent by the server. In Listing 20-7, you can see how I have used a transform function to automatically deserialize the XML data contained in the productData.xml file.

Listing 20-7. Transforming a Response in the ajax.html File

<script>

angular.module(“exampleApp”, [])

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

$scope.loadData = function () {

var con-fig = {

transformResponse: function (data, headers) {

if(headers(“content-type”) == “application/xml”

&& angular.isString(data)) {

products = [];

var productElems = angular.element(data.trim()).find(“product”);

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

var product = productElems.eq(i);

products.push({

name: product.attr(“name”),

category: product.attr(“category”),

price: product.attr(“price”)

});

}

return products;

} else {

return data;

}

}

}

$http.get(“productData.xml”, config).success(function (data) {

$scope.products = data;

});

}

});

</script>

I check the value of the Content-Type header to make sure I am working with XML data and check to see that the data value is a string. It is possible to assign multiple transform functions using an array (or via the provider for the $http service, which I describe later in this chapter), so it is important to ensure that a transform function is dealing with the data format it expects.

Caution I am taking a shortcut to make a simpler demonstration in this listing. My code assumes that all XML data received by the request will contain product elements with name, category, and price. This is reasonable in the closed world of book examples, but you should be more careful in real projects and check that you received the kind of data you expected.

Once I am confident that there is XML data to process, I use the jqLite technique I demonstrated earlier to process the XML into an array of JavaScript objects, which I return as the result of the transform function. The effect of the transformation is that I don’t have to process the XML data in the success function.

Tip Notice that I return the original data if the response doesn’t contain XML data or if the data is not a string. This is important because whatever is returned from the transform function will eventually be passed to your success handler functions.

3.2. Transforming a Request

You can transform a request by assigning a function to the transformRequest property of the configuration object. The function is passed the data that will be sent to the server and a function that returns header values (although many headers will be set by the browser just before it makes the request). The result returned by the function will be used for the request, which provides the means for serializing data. In Listing 20-8, you can see how I have written a transform function that will serialize product data as XML.

Tip You don’t need to use a transform function if you want to send JSON data because AngularJS will serialize it for you automatically.

Listing 20-8. Applying a Request Transformation Function in the ajax.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Ajax</title>

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

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

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

<script>

angular.module(“exampleApp”, [])

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

$scope.loadData = function () {

$http.get(“productData.json”).success(function (data) {

$scope.products = data;

});

}

$scope.sendData = function() {

var config = {

headers: {

“content-type”: “application/xml”

},

transformRequest: function (data, headers) {

var rootElem = angular.element(“<xml>”);

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

var prodElem = angular.element(“<product>”);

prodElem.attr(“name”, data[i].name);

prodElem.attr(“category”, data[i].category);

prodElem.attr(“price”, data[i].price);

rootElem.append(prodElem);

}

rootElem.children().wrap(“<products>”);

return rootElem.html();

}

}

$http.post(“ajax.html”, $scope.products, config);

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<div class=”panel-body”>

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

<thead><tr><th>Name</th><th>Category</th><th>Price</th></tr></thead>

<tbody>

<tr ng-hide=”products.length”>

<td colspan=”3″ class=”text-center”>No Data</td>

</tr>

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

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

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

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

</tr>

</tbody>

</table>

<p>

<button class=”btn btn-primary” ng-click=”loadData()”>Load Data</button>

<button class=”btn btn-primary” ng-click=”sendData()”>Send Data</button>

</p>

</div>

</div>

</body>

</html>

I have added a button element that uses the ng-click directive to call a controller behavior called sendData when clicked. This behavior, in turn, defines a configuration object with a transform function that uses jqLite to generate XML from the request data. (You will have to click the Load Data button first in order to pull in the data so that it can be sent back to the server.)

I submit the data to the server using the $http.post method. I target the ajax.html URL, but the data will be ignored by the server, which will just send the content of the ajax.html file again. I don’t want the contents of the HTML file, so I have not specified a success (or error) function.

Tip Notice that I explicitly set the Content-Type header to application/xml in the configuration object. AngularJS has no way of knowing how a transform function has serialized data, so you must take care to correctly set the header. If you do not, the server may not process the request properly.

4. Setting Ajax Defaults

You can define default settings for Ajax requests through the provider for the $http service, $httpProvider. The provider defines the properties shown Table 20-6.

Tip The defaults object on which many of these properties are defined can also be accessed through the $http.defaults property, which allows the global Ajax configuration to be changed through the service.

The defaults.transformResponse and defaults.transformRequest properties are useful for applying transform functions to all of the Ajax requests made in an application. These properties are defined as arrays, meaning that additions must be made using the push method. In Listing 20-9, you can see how I have applied my XML deserialize function from an earlier example using the $httpProvider.

Listing 20-9. Setting a Global Response Transform Function in the ajax.html File

<script>

angular.module(“exampleApp”, [])

.config(function($httpProvider) {

$httpProvider.defaults.transformResponse.push(function (data, headers) {

if (headers(“content-type”) == “application/xml”

&& angular.isString(data)) {

products = [];

var productElems = angular.element(data.trim()).find(“product”);

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

var product = productElems.eq(i); products.push({

name: product.attr(“name”),

category: product.attr(“category”),

price: product.attr(“price”)

});

}

return products;

} else {

return data;

}

});

})

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

$scope.loadData = function () {

$http.get(“productData.xml”).success(function (data) {

$scope.products = data;

});

}

});

</script>

5. Using Ajax Interceptors

The $httpProvider also provides a feature called request interceptors, which is best thought of as sophisticated alternatives to transform functions. In Listing 20-10, you can see how I have used an interceptor in the ajax.html file.

Listing 20-10. Using an Interceptor in the ajax.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Ajax</title>

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

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

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

<script>

angular.module(“exampleApp”, [])

.config(function ($httpProvider) {

$httpProvider.interceptors.push(function () {

return {

request: function (config) {

config.url = “productData.json”;

return config;

},

response: function (response) {

console.log(“Data Count: ” + response.data.length);

return response;

}

}

});

})

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

$scope.loadData = function () {

$http.get(“doesnotexit.json”).success(function (data) {

$scope.products = data;

});

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<div class=”panel-body”>

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

<thead><tr><th>Name</th><th>Category</th><th>Price</th></tr></thead>

<tbody>

<tr ng-hide=”products.length”>

<td colspan=”3″ class=”text-center”>No Data</td>

</tr>

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

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

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

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

</tr>

</tbody>

</table>

<p><button class=”btn btn-primary”

ng-click=”loadData()”>Load Data</button></p>

</div>

</div>

</body>

</html>

The $httpProvider.interceptor property is an array into which you insert factory functions that return objects with properties from Table 20-7. Each property corresponds to a different type of interceptor, and the functions assigned to the properties have the opportunity to change the request or response.

In the example, the object that my factory method produces defines request and response properties. The function I have assigned to the request property demonstrates how an interceptor can alter a request by forcing the requested URL to be productData.json, irrespective of what was passed to the $http service method. To do this, I set the url property on the configuration object and return it as the result from the function so that it can be passed to the next interceptor or, if my interceptor is the last in the array, so that the request can be made.

For the response interceptor, I have demonstrated how a function can be used to debug the responses received from the server—which is how I find interceptors most useful—by looking at the data property of the response object and writing out how many objects it contains.

My response interceptor relies on the fact that AngularJS uses an interceptor to parse JSON data, which is why I check for an array of objects, rather than a string. This isn’t something you would do in a real project, but I wanted to demonstrate that AngularJS processes the response before the interceptors are applied.

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 *