Angular provides full, integrated support for Ajax. If you are familiar with jQuery's Ajax API, you'll be happy to know that Angular's $http
service holds closely to its conventions. If you're not familiar with jQuery Ajax, don't worry. Using $http
is intuitive and should be easy to pick up.
Here is the agenda for this chapter:
You'll soon see how easy it is to pull off a real-life, round-trip Ajax interaction between the server and the user.
As usual, the examples will load hosted versions of Angular and the Bootstrap CSS framework.
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.js"></script>
We need to initialize a module, as was covered in depth in the earlier chapter on Modules. There is no need to require an additional module to use $http
, since it is included in the core Angular library.
angular.module('app', []);
We then pass the name of our root app
module to the ng-app
directive. Our choice of the name app
is a convention, but it is not otherwise significant.
<body ng-app="app">
<!-- Other examples in this chapter will be inserted here. -->
</body>
We are ready to go. The first thing we want to do is to load data from a backend server.
Loading data is properly done using the HTTP GET method. Sending a GET is easy; probably the hardest thing is knowing where to send it. The root URL for our live, RESTful backend API service is http://www.scriptybooks.com/api/v1
. We will store this root URL as a service that can be accessed elsewhere in our application via dependency injection.
angular.module('app')
.value('url', 'http://www.scriptybooks.com/api/v1');
We inject the $http
service into a controller by adding a parameter named $http
to the controller function. The example end point at http://www.scriptybooks.com/api/v1/surveys/1/questions/1 returns a single, JSON-encoded resource. It looks like this:
{
id: 1,
content: "What is the hardest part of learning to use Angular?",
position: 1
}
We inject the root URL for our backend into the controller, then append the remaining resource path (/surveys/1/questions/1
) for the complete URL.
angular.module('app')
.controller('QuestionController', function($scope, $http, url) {
$http.get(url + '/surveys/1/questions/1')
.success(function(data) {
$scope.question = data;
});
});
Our template for this controller displays the question
data, specifically the attribute named content
. The template also displays a notice
property, which may be set by the controller to communicate the status of the request handling. The controller above does not assign anything to the notice
property, but later examples that handle errors will make use of it.
<div ng-controller="QuestionController">
<p class="text-danger">{{notice}}</p>
<label>{{question.content}}</label>
</div>
The get
function used in the controller shown above is a convenience wrapper for the underlying $http
function. The following controller is functionally equivalent to the previous example, but is more difficult to read. You should prefer the convenience wrappers (get
, post
, delete
, and so on) to the base method shown below, unless you need to do something special.
angular.module('app')
.controller('QuestionController', function($scope, $http, url) {
var options = {
method: 'GET',
url: url + '/surveys/1/questions/1'
};
$http(options)
.success(function(data) {
$scope.question = data;
});
});
If the GET request to the server is successful, you should see a question displayed in the output of the above examples. However, requests to backend services can fail for a variety of reasons. Our current controller provides no error handling, although our template does provide a place to display an error notice. To cause an error in the example above, simply replace the 1
at the end of the path with an invalid value, such as BADPATH
. What do you think about the user experience? Can we improve it?
We need to register an error handler that will inform the user when something goes wrong.
Like jQuery.ajax(), the $http
service supports a method chaining style that makes it easy to configure success
and error
callbacks without passing them in as arguments to the inital call to $http
or its method-specific convenience wrappers such as get
. This style is made possible behind the scenes by $q
, which is Angular's CommonJS promises implementation.
In my opinion, you don't need to understand much about $q
in order to use $http
, although if you want to learn more about how it works, I recommend looking over the documentation for Kris Kowal's Q, the original Promises/A implementation from which $q
borrows heavily with only a few differences. Recently, $q
has been expanded to include compatibility with native ES6 Harmony promises. Too much information to process at once? Don't worry. The goal of this section is to give you just enough experience with $q
to manage typical HTTP responses.
Let's chain an error
callback to our existing success callback.
angular.module('app')
.controller('QuestionController', function($scope, $http, url) {
$http.get(url + '/surveys/1/questions/BADPATH')
.success(function(data, status, headers, config) {
$scope.question = data;
})
.error(function(data, status, headers, config) {
$scope.notice = status + " " + data.error;
});
});
By changing the path to end in BADPATH
, we cause the server to send a 404
error.
The success
and error
methods are actually convenience wrappers on the $http
API for the more generalized then
method on the $q
API. The then
method accepts two callbacks, for fulfillment and rejection (think success and error.) Callbacks registered with then
are invoked with a single argument, instead of the four HTTP-specific arguments passed to success
and error
callbacks. With a promise instance originating in the $http
method, such as this one, the single argument is an object representing the HTTP response.
angular.module('app')
.controller('QuestionController', function($scope, $http, url) {
$http.get(url + '/surveys/1/questions/badID')
.then(function(response) {
$scope.question = response.data;
},function(response) {
$scope.notice = response.status + " " + response.data.error;
});
});
Is there a significant reason to use then
instead of success
and error
? Probably the most important is that then
executes callbacks serially, with each receiving the output of the one before. Calls to success
and error
can be chained for convenience, but the chained handlers are all called with exactly the same arguments. Unless you are able to modify the arguments passed to the callbacks in the chain, in pipeline fashion, you can't really take a layered approach to your application and encapsulate details, such as your usage of the $http
API or the specifics of the server's JSON serialization. Therefore, then
has definite advantages for a real-world application.
Let's refactor our example by moving our $http
code into a service that will be used by our controller. Notice that the only significant change is that in this service, which is no longer the end of the chain, we need access to the $q
service, in order to invoke reject
in the case of an error response.
angular.module('app')
.factory('question', function($q, $http, url) {
return function(id) {
return $http.get(url + '/surveys/1/questions/' + id)
.then(function(response) {
return response.data;
},function(response) {
return $q.reject(response.status + " " + response.data.error);
});
};
});
The messy job of dealing with the $http
API and extracting data from the response is now encapsulated in this service. As a result, a controller that uses the new question
service is extremely simple.
angular.module('app')
.controller('QuestionController', function($scope, question) {
question(1)
.then(function(question) {
$scope.question = question;
},function(notice) {
$scope.notice = notice;
});
});
The example above shows how a promise that originates in the $http
API can be passed along to clients without exposing full access to the $http
API. It's elegant, powerful stuff.
Now that we've mastered loading and displaying data from the example remote backend, it is time to send some data of our own in the other direction. The data that we loaded was a survey question. The data that we send will be your answer to that question.
The HTTP POST request, of course, is the correct way to create new data on a server. In this case, again, the new data will be your answer. The answer
service shown below is a function that invokes the $http
API's post
function, using the input that is passed in via its answer
parameter.
angular.module('app')
.factory('answer', function($q, $http, url) {
return function(question, answer) {
var data = {
answer: {
question_id: question.id,
content: answer
}
};
return $http.post(url + '/answers', data)
.then(function(response) {
return response.data;
},function(response) {
return $q.reject(response.status + " " + response.data.error);
});
};
});
In the SurveyController
below, the invocation of the question
service uses the same code as the final QuestionController
example, above. In addition, it adds a new submit
callback function that invokes the new answer
service.
angular.module('app')
.controller('SurveyController', function($scope, $http, url, question, answer) {
//
question(1)
.then(function(question) {
$scope.question = question;
},function(notice) {
$scope.notice = notice;
});
// Callback to be attached to the form submit button
$scope.submit = function() {
answer($scope.question, $scope.answer)
.then(function(data) {
$scope.notice = "Success. Your answer was saved with id: " + data.id;
$scope.answer = '';
},function(notice) {
$scope.notice = notice;
});
};
});
By exposing the submit
function on the scope, we make it available for use in the template as a callback that can be bound to an input. In Angular's expression language, this is done with parentheses in the style of a function invocation statement: ng-click="submit()"
.
Notice that although the template below contains an HTML form
element, the submit
function in the controller does not use this form at all, relying instead on the bound scope property answer
to populate the POST data. Although the form isn't needed by our Angular code, its presence may be important for styling as well as HTML5 features, such as the required
directive on the textarea
input, so it's usually best to keep it around.
<form ng-controller="SurveyController">
<div class="alert alert-info" role="alert">
{{notice}}
</div>
<div class="form-group">
<label>{{question.content}}</label>
<textarea class="form-control" ng-model="answer" required>
</textarea>
</div>
<button type="submit" class="btn btn-default btn-sm" ng-click="submit()">
Submit
</button>
</form>
If you typed an answer and clicked Submit
, you will have noticed that the server responds with the message CSRF verification failed
. This is because our POST request is missing a custom header that is required by the server. Don't worry, you will get to submit your answer soon. Hold on to it, please.
It's no accident that our example backend expects a custom header; it's a requirement that I added on purpose to force us to dig a bit deeper into the $http
service.
The custom header we need to set is named X-CSRF-Token
and is defined by Ruby on Rails' cross-site request forgery (CSRF) protection. Angular provides its own XSRF protection, which uses a X-XSRF-TOKEN
header instead. (CSRF and XSRF are the same thing, both acronyms refer to cross-site request forgery.) Angular's XSRF protection is easy to use on the client-side, but may require you to make changes to the server, as in the case of Rails. However, since our goal is to learn how to customize Angular, not Rails or some other backend, we will solve the mis-match by configuring Angular to supply the custom header expected by our Rails backend.
The first thing we need is the CSRF protection token provided by the server.
angular.module('app')
// In the real world, this token value must be obtained dynamically.
.constant('X_CSRF_TOKEN', 'X7XdcSTOaQ7AvB85WXez6uIm/auP+3Zd6wT8JB/0MPg=');
Our example backend uses a static, unchanging token, which is not realistic and offers very little protection. However, it allows us to skip over the details of obtaining the token for now, and jump right into setting the header. There are several ways that your Angular application might obtain the token that is dynamically generated by the backend. Probably the most common is to read it from a cookie. In other cases, the backend might include it as a response header or in the DOM. In a later example, we will read the token from a response header, rather than use the hardcoded token above.
The most straightforward way to set a header is on a per-request basis. As you may recall from the beginning of the chapter, the underlying $http
function lets us specify additional configuration that is not available in helper functions such as get
and post
. Let's switch back to using $http
so that we can set its headers
option.
angular.module('app')
.factory('answer', function($q, $http, url, X_CSRF_TOKEN) {
return function(question, answer) {
var options = {
method: 'POST',
url: url + '/answers',
data: {
answer: {
question_id: question.id,
content: answer
}
},
headers: {
'X-CSRF-Token': X_CSRF_TOKEN
}
};
return $http(options)
.then(function(response) {
return response.data;
},function(response) {
return $q.reject(response.status + " " + response.data.error);
});
};
});
Try again to submit a survey answer to the live example backend. If successful (and you should be), you will see a notice that reads: "Your answer was saved with id: ...". Proof from the server that our example is working! If you want to test the CSRF protection, edit the token value in the example by deleting a few characters, and submit another answer. You should see that the CSRF verification failed.
Link here from end of Services chpt
Setting the header directly in each request will result in duplicated code if we have more than one service that sends POST requests. How can we ensure that all POST requests include the X-CSRF-Token
header and token?
The module.config
function allows us to configure services such as $http
as they are loaded by Angular. The trick is to know the name of the configuration service, or provider, for the service. In the case of $http
, the provider is quite logically named $httpProvider
. By writing a module config
callback that is injected with the $httpProvider
reference, we can add the custom header to the defaults.headers.post
configuration object. You can test that it works by answering the survey question once again!
angular.module('app')
.config(function($httpProvider, X_CSRF_TOKEN) {
$httpProvider.defaults.headers.post['X-CSRF-Token'] = X_CSRF_TOKEN;
});
There are several other properties that you can set on the defaults
object in addition to headers, including defaults.cache
, defaults.xsrfCookieName
, and defaults.xsrfHeaderName
. Hey, wait a second! You mean there's a way to configure Angular's XSRF protection to use the example backend's custom header? Yes, there is, although it would still require modifying a backend such as this one that does not set the CSRF protection token on the cookie.
Since this backend instead sends the CSRF token as an HTTP response header, how can we get access to an HTTP response in order to read and store the value?
The $http
service provides interceptors as a way to insert callbacks into the HTTP request/response lifecycle. These are callbacks that typically will be run after your Angular controllers or services send an HTTP request, and/or before they receive a response.
For our CSRF example, we need to read and store a CSRF protection token that the server sets as a header in every authenticated response. Since you are not actually authenticated (book readers don't want to be bothered with user accounts and passwords, do they?), the example server for this book simply sends everyone the same token. In the real world, only authenticated users would receive a token, and each would receive a different one.
Since the interceptor is a singleton and our Angular application does not support multiple users, we can use a simple variable, csrfToken
, to store the value. The token value is stored by a response
callback, shown below, which handles the server's response to our question
service's request. Later, after the answer
sends its POST request, the request
callback gets the chance to set the token value in the header.
angular.module('app')
.factory('csrfTokenInterceptor', function () {
var csrfToken = null;
return {
response: function (response) {
if (response.headers('X-CSRF-Token')) {
csrfToken = response.headers('X-CSRF-Token');
}
return response;
},
request: function (config) {
if (config.method == 'POST' && csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
}
}
});
The interceptor service shown above will not be invoked unless we also register it as an interceptor using the $httpProvider
reference. Do not forget this important step. Angular is not the extreme sort of convention over configuration framework that will handle this for you just because you ended the service's name with Interceptor
.
angular.module('app')
.config(function ($httpProvider) {
$httpProvider.interceptors.push('csrfTokenInterceptor');
});
Interceptors have a lot of different uses. For example, this book does not cover how to integrate server-side authentication with Angular, but an interceptor is typically the right place to verify that the user is authenticated with the server before actually sending a request that requires authentication, or otherwise redirect to a client-side login route if the user is not authenticated.
In this chapter, we learned the basics of using the $http
service to send GET and POST requests, as well as a little bit about configuring $http
to handle custom HTTP headers. This chapter also introduced Angular's $q
service, which is used by $http
to offer promises-style asynchronous programming.