It's now time to talk to the server! There is no fun in creating a workout, adding exercises, and saving it, to later realize that all our efforts are lost because the data is not persisted anywhere. We need to fix this.
Seldom are applications self-contained. Any consumer app, irrespective of the size, has parts that interact with elements outside its boundary. And with web-based applications, the interaction is mostly with a server. Apps interact with the server to authenticate, authorize, store/retrieve data, validate data, and perform other such operations.
This Lesson explores the constructs that AngularJS provides for client-server interaction. In the process, we add a persistence layer to Personal Trainer that loads and saves data to a backend server.
The topics we cover in this Lesson include:
$http
is the core service in Angular to for interacting with a server over HTTP. You learn how to make all types of GET
, POST
, PUT
, and DELETE
requests with the $http
service.$http
service to load and store workout data into MongoLab databases.$resource
service is an abstraction built over $http
to support the RESTful server endpoints. You learn about the $resource
service and its usage.$resource
service to load and save exercise data.Let's get the ball rolling.
Any client-server interaction typically boils down to sending HTTP requests to a server and receiving responses from a server. For heavy apps of JavaScript, we depend on the AJAX request/response mechanism to communicate with the server. To support AJAX-based communication, AngularJS exposes two framework services:
$http
: This is the primary component to interact with a remote server using AJAX. We can compare it to the ajax
function of jQuery as it does something similar.$resource
: This is an abstraction build over $http
to make communication with RESTful (http://en.wikipedia.org/wiki/Representational_state_transfer) services easier.Before we delve much into the preceding service we need to set up our server platform that stores the data and allows us to manage it.
For data persistence, we use a document database, MongoDB (https://www.mongodb.org/), hosted over MongoLab (https://mongolab.com/) as our data store. The reason we zeroed in MongoLab is because it provides an interface to interact with the database directly. This saves us the effort of setting up server middleware to support the MongoDB interaction.
It is never a good idea to expose the data store/database directly to the client, but, in this case, since your primary aim is to learn about AngularJS and client-server interaction, we take this liberty and will directly access the MongoDB instance hosted in MongoLab.
There is also a new breed of apps that are built over noBackend solutions. In such a setup, frontend developers build apps without the knowledge of the exact backend involved. Server interaction is limited to making API calls to the backend. If you are interested in knowing more about these noBackend solutions, do checkout http://nobackend.org/.
Our first task is to provision an account on MongoLab and create a database:
On the database creation screen, you need to make some selection to provision the database. See the following screenshot to select the free database tier and other options:
exercises
: This stores all Personal Trainer exercisesworkouts
: This stores all Personal Trainer workoutsCollections in the MongoDB world equate to a database table.
MongoDB belongs to a breed of databases termed document databases. The central concepts here are documents, attributes, and their linkages. And, unlike traditional databases, the schema is not rigid.
We will not be covering what document databases are and how to perform data modeling for document-based stores in this book. Personal Trainer has a limited storage requirement and we manage it using the preceding two document collections. We may not even be using the document database in its true sense.
The datastore schema is complete; we now need to seed these collections.
The Personal Trainer app already has a predefined workout and a list of 12 exercises. We need to seed the collections with this data.
Open seed.js
from Lesson04/checkpoint1/app/js
from the companion codebase. It contains the seed JSON script and detailed instructions on how to seed data into the MongoLab database instance.
Once seeded, the database will have one workout in the workouts collection and 12 exercises in the exercises collection. Verify this on the MongoLab site, the collections should show this:
Everything has been set up now, let's start our discussion with the $http
service and implement workout/exercise persistence for the Personal Trainer app.
The $http
service is the primary service for making an AJAX request in AngularJS. The $http
service provides an API to perform all HTTP operations (actions) such as GET
, POST
, PUT
, DELETE
, and some others.
HTTP communication is asynchronous in nature. When making HTTP requests, a browser does not wait for the response to arrive before continuing processing. Instead, we need to register some callback functions that are invoked in the future when the response arrives from the server. The AngularJS Promise API helps us streamline this asynchronous communication and we use it extensively while working with the $http
service, as you will see later in this Lesson.
The basic $http
syntax is:
$http(config)
The $http
service takes a configuration object as a parameter and returns a promise. The config
object contains a set of properties that affect the remote request behavior. These properties include arguments such as the HTTP action type (GET
, POST
, PUT
,…), the remote server URL, query string parameters, headers to send, and a number of other such options.
The exact configuration option details are available in framework documentation for the $http
service at https://code.angularjs.org/1.3.3/docs/api/ng/service/$http. As we work through the Lesson, we will use some of these configurations in our implementation too.
A $http
invocation returns a promise object. Other than the standard Promise API functions (such as then
), this object contains two extra callback functions: success
and error
, that get invoked based on whether the HTTP request was completed successfully or not.
Here is a simple HTTP request using $http
:
$http({method: 'GET', url: '/endpoint'}). success(function(data, status, headers, config) { // called when http call completes successfully }). error(function(error, status, headers, config) { // called when the http call fails. // The error parameter contains the failure reason. });
The preceding code issues an HTTP GET
request to /endpoint
and when the response is available either the success
or error
callback is invoked.
The callback functions (success
or error
) are invoked with four arguments:
data
or error
: This is the response returned from the server. It can be the data returned or an error if the request fails.status
: This is the HTTP status code for the response.headers
: This is used for the HTTP response headers.config
: This is the configuration object used during the original $http
invocation.The $http(config)
syntax for making an AJAX request is very uncommon. The service has a number of shortcut methods to make a specific type of HTTP request. These include:
$http.get(url, [config])
$http.post(url, data, [config])
$http.put(url, data,[config])
$http.delete(url, [config])
$http.head(url,[config])
$http.jsonp(url, [config])
All these function take the same (optional) config
object as the last parameter.
An interesting thing about the standard $http
configuration is that these settings make JSON data handling easy. The end effect of this is:
GET
operations, if the response is JSON, the framework automatically parses the JSON string and converts it into a JavaScript object. The end result is that the first argument of the success
callback function (data
) contains a JavaScript object, not a string value.POST
and PUT
, objects are automatically serialized and the corresponding content type header is set (Content-Type: application/json
) before the request is made.Does that mean that $http
cannot handle other formats? That is far from true. The $http
service is the generic AJAX service exposed by the Angular framework and can handle any format of request/response. Every AJAX request that happens in AngularJS is done by the $http
service directly or indirectly. For example, the remote views that we load for the ng-view
or ng-include
directives use the $http
service under the hood.
Checkout the jsFiddle web page at http://jsfiddle.net/cmyworld/doLhmgL6/ where we use the $http
service to post data to a server in a more traditional format that is associated with the standard post form. ('Content-Type': 'application/x-www-form-urlencoded'
).
It is just that Angular makes it easy to work with JSON data, helping us to avoid writing boilerplate serialization/deserialization logic, and setting HTTP headers, which we normally do when working with JSON data.
With this backgrounder on the $http
service, we now are in a position to implement something useful using $http
. Let's add some workout persistence.
As described in the previous section, client-server interaction is all about asynchronicity. As we alter our Personal Trainer app to load data from the server, this pattern becomes self-evident.
In the preceding Lesson, the initial set of workouts and exercises was hardcoded in the WorkoutService
implementation itself. Let's see how to load this data from the server first.
Earlier in this Lesson, we seeded our database with a data form, the seed.js
file. We now need to render this data in our views. The MongoLab REST API is going to help us here.
The MongoLab REST API uses an API key to authenticate access request. Every request made to the MongoLab endpoints needs to have a query string parameter apikey=<key>
where key
is the API key that we provisioned earlier in the Lesson. Remember, the key is always provided to a user and associated with his/her account. Avoid sharing your API keys with others.
The API follows a predictable pattern to query and update data. For any MongoDB collection, the typical endpoint access pattern is one of the following (given here is the base URL: https://api.mongolab.com/api/1/databases):
/<dbname>/collections/<name>?apiKey=<key>
. This has the following requests:GET
: This action gets all objects in the given collection name.POST
: This action adds a new object to the collection name
. MongoLab has an _id
property that uniquely identifies the document (object). If not provided in the posted data, it is autogenerated./<dbname>/collections/<name>/<id>?apiKey=<key>
. This has the following requests:GET
: This gets a specific document/collection item with a specific ID (a match done on the _id
property) from the collection namePUT
: This updates the specific item (id
) in the collection nameDELETE
: This deletes the item with a specific ID from the collection name
For more details on the REST API interface, visit the MongoLab REST API documentation at http://docs.mongolab.com/restapi/#insert-multidocuments.
Now, we are in a position to start implementing exercise/workout list pages.
To pull exercise and workout lists from the MongoLab database, we have to rewrite our WorkoutService
service methods, getExercises
and getWorkouts
.
Open services.js
from app/js/shared
and change the getExercises
function to this:
service.getExercises = function () { var collectionsUrl = "https://api.mongolab.com/api/1/databases/<dbname>/collections"; return $http.get(collectionsUrl + "/exercises", { params: { apiKey: '<key>'} }); };
Replace the tokens: <dbname>
and <key>
with the DB name and API key of the database that we provisioned earlier in the Lesson.
Also remember to add the $http
dependency in the WorkoutService
declaration.
The new function created here just builds the MongoLab URL and then calls the $http.get
function to get the list of exercises. The first parameter we have is the URL to connect to and the second parameter is the config
object.
The params
property of the config
object allows us to add query string parameters to the URL. We add the API key (?apiKey=98dkdd
) as a query string for API access.
Now that the getExercises
function is updated, and the new implementation returns a promise, we need to fix the upstream callers.
Open exercise.js
placed under WorkoutBuilder
and fix the ExerciseListController
by replacing the existing init
function implementation (the code inside init
) with these lines:
WorkoutService.getExercises().success(function (data) { $scope.exercises = data; });
We use the HTTP promise success callback to bind the exercises list in a controller. We can clearly observe the asynchronous behavior of $http
and the promise-based callback in action as we set the exercises
data after receiving a server response in a callback function.
Go ahead and load the exercise list page (#/builder/exercises
) and make sure the exercise list is loading from the server. The browser network logs should log requests such as this:
Exercises are loading fine, but what about workouts? The workout list page can also be fixed on similar lines.
Update the getWorkouts
function of WorkoutService
to load data from the server. The getWorkouts
implementation is similar to getExercises
except that the collection name now becomes workouts
. Then fix the init
function of WorkoutListController
along the same lines as the preceding init
function and we are done.
That was easy! We can fix all other get
scenarios in a similar manner. But before we do that, there is still scope to improve our implementation.
The first problem with getExercises
/getWorkouts
is that the DB name and API key are hardcoded and will cause maintenance issues in the future. The best way is to inject these values into WorkoutService
through some kind of mechanism.
With our past experience and learnings, we know that, if we implement this service using provider
, we can pass configuration data required to set up the service at the configuration stage of app bootstrapping. This allows us to configure the service before use. Time to put this theory to practice!
Implementing WorkoutService
as a provider will help us to configure the database name and API key for the service at the configuration stage.
Copy and replace the updated WorkoutService
definition from the services.js
file in Lesson04/checkpoint1/app/js/shared
. The service has now been converted into a provider implementation.
The service has a configure
method that sets up the database name, the API key, and the collection URL address, as given here:
this.configure = function (dbName, key) { database = database; apiKey = key; collectionsUrl = apiUrl + dbName + "/collections"; }
The functions: setupInitialExercises
, setupInitialWorkouts
, and init
have also been removed as the data will now come from the MongoLab server.
The implementation of the getExercise
and getWorkouts
functions has been updated to use the configured parameters:
service.getExercises = function () { return $http.get(collectionsUrl + "/exercises", { params: { apiKey: apiKey } }); };
And finally, the service
object creation has been moved into the $get
function of the provider. $get
is the factory function responsible for creating the actual service.
Let's update the config
function of the app
module and inject the MongoLab configuration into WorkoutService
(using WorkoutServiceProvider
).
Open the app.js
file, inject the new provider dependency WorkoutServiceProvider
with the other provider dependencies, and call its configure method with your database name and API key:
WorkoutServiceProvider.configure("<mydb>", "<mykey>");
We now have a better WorkoutService
implementation as it allows the calling code to configure the service before use.
The provider implementation may look overtly complex as this could be achieved by creating a constant service like this:
angular.module('app').constant('dbConfig', { database: "<dbname>", apiKey: "<apikey>" });
And then inject the implementation into the existing WorkoutService
implementation.
The advantage of the provider approach is that the configuration data is not globally exposed. Had we used a constant service such as dbConfig
, any other service/controller could have got hold of the database name and API key by injecting the dbConfig
service, which would be less than desirable.
The preceding provider refactoring is still not complete and we can verify this by refreshing the workout list page. There will be an unknown provider WorkoutServiceProvider
error in the browser developer console.
We have just hit a bug with Angular that causes the config
function module to execute before provider registration. This happened because the script registration for app.js
precedes the service.js
registration in index.html
.
There is already a bug (https://github.com/angular/angular.js/issues/7139) logged against this issue and the current workaround is to call the config
function at the end, after all provider/service registrations. This requires us to move the config
function implementation to a new file.
Copy the updated app.js
and config.js
(new file) files from Lesson04/checkpoint1
and update your local copy. Once copied, update the configure
function of WorkoutServiceProvider
in the config.js
file with your database name and API key. And finally, add a reference to config.js
in the script declaration section of index.html
at the end.
Refresh the workout/exercise list page and the workout and exercise data is loaded from the database server.
This looks good and the lists are loading fine. Well, almost! There is a small glitch in the workout list page. We can easily spot it if we look carefully at any list item (in fact there is only one item):
The workout duration calculations are not working anymore! What could be the reason? We need to look back on how these calculations were implemented. The WorkoutPlan
service (in model.js
) defines a function totalWorkoutDuration
that does the math for this.
The difference is in terms of the workout array that is bound to the view. In the previous Lesson, we created the array with model objects that were created using the WorkoutPlan
service. But now, since we are retrieving data from the server, we bind a simple array of JavaScript objects to the view, which for obvious reasons has no calculation logic.
We can fix this problem by mapping a server response into our model class objects and returning that to any upstream caller.
Mapping server data to our model and vice versa may be unnecessary if the model and server storage definition match. If we look at the Exercise
model class and the seed data that we have added for the exercise in MongoLab, they do match and hence mapping becomes unnecessary.
Mapping server response to model data becomes imperative if:
WorkoutPlan
is a prime example of an impedance mismatch between a model representation and its storage. Look at the following screenshot to understand these differences:
The two major differences between a model and server data are as follows:
This clearly means loading and saving a workout requires model mapping. And for consistency, we plan to map data for both the exercise and the workout.
Change the getExercises
implementation in WorkoutService
to this:
service.getExercises = function () { return $http.get(collectionsUrl + "/exercises", { params: { apiKey: apiKey} }).then(function (response) { return response.data.map(function (exercise) { return new Exercise(exercise); })}); };
And since the return value for the getExercises
function now is not a promise object returned by $http
(see the following discussion) but a standard promise, we need to use the then
function instead of the success
function wherever we are calling getExercises
.
Change the init
implementation in both ExercisesNavController
and ExerciseListController
to this:
var init = function () {
WorkoutService.getExercises().then(function (data) {
$scope.exercises = data;
});
};
Look back at the highlighted code for the updated getExercises
implementation. There are a number of interesting things going on here that you should understand:
then
success callback function (the first parameter), we call the Array.map
function to map the list of exercises received from a server to the Exercise
object array. The Array.map
function is generally used to map from one array to another array. Check out the MDN documentation for the Array.map
(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) function to know more about how it works.$http.get
function returns the first promise; we attach a then
callback to it that itself returns a promise that is finally returned to the calling code. We can visualize this with the help of the following diagram:In future, when the first $http.get
promise is resolved, the then
callback is invoked with the exercise list from the server. The then
callback processes the response and returns a new array of the Exercise
objects. This return value feeds into the promise resolution for the next callback in the line defined in ExerciseListController
.
The then
function of ExercisesController
finally assigns the Exercise
objects received to the exercises
variable. The promise resolution data has been highlighted in the preceding diagram above the dotted arrows.
Promise chaining acts like a pipeline for response flow; this is a very powerful pattern and can be used to do some nifty stuff as we have previously done.
then
instead of the $http.get success
callback due to a subtle difference between what success
and then
returns. The success
function returns the original promise (in this case, a promise is created on calling $http.get
) whereas then
returns a new promise that is resolved with the return value of the success or error functions that we attach to then
.Before we continue any further, let's learn a bit more about promise chaining with some simpler examples. It's a very useful and powerful concept.
Promise chaining is about feeding the result of one promise resolution into another promise. Since promises wrap asynchronous operations, this chaining allows us to organize asynchronous code in a chained manner instead of nested callbacks. We saw an example of promise chaining earlier. The exercises were retrieved (the first asynchronous operation), transformed (the second asynchronous operation), and finally bound to the view (the third asynchronous operation), all using promise chaining. The previous diagram also highlights this.
Such chaining allows us to create chains of any length as long as the methods involved in the chain return a promise. In this case, both the $http.get
and then
functions return a promise.
Let's look at a much simpler example of chaining in action. The code for this example is as follows:
var promise = $q.when(1); var result = promise .then(function (i) { return i + 1;}) .then(function (i) { return i + 1;}) .then(function (i) { return i + 1;}); .then(function (i) { console.log("Value of i:" + i);});
I have created a jsFiddle (http://jsfiddle.net/cmyworld/9ak1gahe/) too to demonstrate the working of promise chaining.
The preceding code uses promise chaining, and every chained function increments the value passed to it and passes it along to the next promise in the chain. The final value of i
in the last then
function is 4
.
As described earlier, such chaining is possible due to the fact that the then
function itself returns a promise. This promise is resolved to the return value of either the success
or error
callback. Look at the preceding code; there is a return statement in every success callback (except the last).
The behavior of promise chaining when it comes to the error
callback may surprise us. An example will be the best way to illustrate that too. Consider this code:
var errorPromise = $q.reject("error"); var resultError = errorPromise.then(function (data) { return "success"; }, function (e) { return "error"; }); resultError.then(function (data) { console.log("In success with data:" + data); }, function (e) { console.log("In error with error:" + e); });
The $q.reject
function creates a promise that is rejected with the value as error
. Hence, the resultError
promise is resolved with the return value error
(return error
).
The question now is, "What should the resultError.then
callback print?" Well, it prints In success with data: error, since the success
callback is invoked not error
. This happened because we used a standard return
call in both the success
and error
callbacks for errorPromise.then (or resultError)
.
If we want the promise chain to fail all along, we need to reject the promise in every error
callback. Change the resultError
promise to this:
var resultError = errorPromise.then(function (data) {
return "success";
}, function (e) {
return $q.reject(e);
});
The correct error callback in the next chained then
is called, and the console logs In error with error: error
.
By returning $q.reject(e)
in the error
callback, the resolved value of the resultError
promise will be a rejected promise ( $q.reject
returns a promise that is always rejected).
Promise chaining is a very powerful concept, and mastering it will help us write more compact and well organized code. We will be extensively using promise chaining throughout this Lesson to handle server response and to transform and load data.
Let's get back to where we left off, loading exercise and workout data from the server.
As we fixed the getExercises
implementation in WorkoutService
earlier, we can implement other get operations for exercise- and workout-related stuff. Copy the service implementation for the getExercise
, getWorkouts
, and getWorkout
functions of WorkoutService
from Lesson02/checkpoint2/app/js/shared/services.js
.
The getWorkout
and getExercise
functions use the name of the workout/exercise to retrieve results. Every MongoLab collection item has an _id
property that uniquely identifies the item/entity. In the case of our Exercise
and WorkoutPlan
object, we use the name of the exercise for unique identification, and hence the name and _id
property always match.
Pay special attention to implementation for both the getWorkouts
and getWorkout
functions because there is a decent amount of data transformation happening in both the functions due to the model and data storage format mismatch.
The getWorkouts
function is similar to getExercises
except it creates the WorkoutPlan
object and the exercises
array is not mapped to the list of class objects of Exercises,
instead server structure of {name:'name', duration:value}
is used as it is.
The getWorkout
function implementation involves a good amount of data mapping. This is how the getWorkout
function now looks:
service.getWorkout = function (name) { return $q.all([service.getExercises(), $http.get(collectionsUrl + "/workouts/" + name, { params: { apiKey: apiKey } })]) .then(function (response) { var allExercises = response[0]; var workout = new WorkoutPlan(response[1].data); angular.forEach(response[1].data.exercises, function (exercise) { exercise.details = allExercises.filter(function (e) { return e.name === exercise.name; })[0]; }); return workout; }); };
There is a lot happening inside getWorkout
that we need to understand.
The getWorkout
function starts the execution by calling the $q.all
function. This function is used to wait over multiple promise calls. It takes an array of promises and returns a promise. This aggregate promise is resolved or rejected (an error) when all promises within the array are either resolved or at least one of the promises is rejected. In the preceding case, we pass an array with two promises: the first is the promise returned by the service.getExercises
function and the second is the http.get
call (to get the workout with a specific identifier).
The $q.all
function callback parameter response is also an array corresponding to the resolved values of the input promise array. In our case, response[0]
contains the list of exercises and response[1]
contains workout collection responses received from the server (response[1].data
contains the data part of the HTTP response).
Once we have the workout details and the complete list of exercises, the code just after this updates the exercises
array of the workout to the correct Exercise
class object. It does this by searching the allExercises
array for the name of the exercise as available in the workout.exercises
array item returned from the server. The end result is that we have a complete WorkoutPlan
object with the exercises
array setup correctly.
These WorkoutService
changes warrant fixes in upstream callers too. We have already fixed both ExercisesNavController
and ExerciseListController
. Fix the WorkoutListController
object along similar lines. The getWorkout
and getExercise
functions are not directly used by the controller but by our builder services. Let's now fix the builder services together with the workout/exercise detail pages.
We fix the workout detail page and I will leave it to you to fix the exercise detail page yourself as it follows a similar pattern.
ExeriseNavController,
used in the workout detail page navigation rendering, is already fixed so let's jump onto fixing WorkoutDetailsController
.
WorkoutDetailController
does not load workout details directly but is dependent on the resolve
route (see route configuration in config.js
) invocation; when the route changes, this injects the selected workout (selectedWorkout
) into the controller. The resolve selectedWorkout
function in turn is dependent upon WorkoutBuilderService
to load the workout, new or existing. Therefore the first fix should be WorkoutBuilderService
.
The function that pulls workout details is startBuilding
. Update the startBuilding
implementation to the following code:
service.startBuilding = function (name) { var defer = $q.defer(); if (name) { WorkoutService.getWorkout(name).then(function (workout) { buildingWorkout = workout; newWorkout = false; defer.resolve(buildingWorkout); }); } else { buildingWorkout = new WorkoutPlan({}); defer.resolve(buildingWorkout); newWorkout = true; } return defer.promise; };
In the preceding implementation, we use the $q
service of the Promise API to create and resolve our own promise. The preceding scenario required us to create our own promise because creating new workouts and returning is a synchronous process, whereas loading the existing workout is not. To make the return value consistent, we return promises in both the new workout and edit workout cases.
To test the implementation, just load any existing workout detail page such as 7minWorkout
under #/builder/workouts/
. The workout data should load with some delay.
This is the first time we are actually creating our own promise and hence it's a good time to delve deeper into this topic.
Creating and resolving a standard promise involves the following steps:
defer
object by calling the $q.defer()
API function. The defer
object is like an (conceptually) action that will complete some time in the future.defer.promise
at the end of the function call.defer.resolve(data)
function to resolve the promise with a specific data
or defer.reject(error)
object to reject the promise with the specific error
function. The resolve
and reject
functions are part of the defer API. The resolve
function implies work is complete whereas reject
means there is an error.The preceding startBuilding
function follows the same pattern.
An interesting thing about the preceding startBuilding
implementation is that, in the case of the else
condition, we immediately resolve the promise by calling defer.resolve
with a new workout object instance, even before we have returned a promise to the caller. The end result is that, in the case of a new workout, the promise is immediately resolved once the startBuilding
function completes.
The ability to create and resolve our own custom promise is a powerful feature. Such an ability is very useful in scenarios that involve invocation and coordination of one or more asynchronous methods before a result can be delivered. Consider a hypothetical example of a service function that gets product quotes from multiple e-commerce platforms:
getProductPriceQuotes(productCode) { var defer = $q.defer() var promiseA = getQuotesAmazon(productCode); var promiseB = getQuotesBestBuy(productCode); var promiseE = getQuotesEbay(productCode); $q.all([promiseA, promiseB, promiseE]) .then(function (response) { defer.resolve([buildModel(response[0]), buildModel(response[1]), buildModel(response[2])]); }); defer.promise; }
The getProductPriceQuotes
service function needs to make asynchronous requests to multiple e-commerce sites, collate the date received, and return the data to the user. Such a coordinated effort can be managed by the Promise/defer API. In the preceding sample, we use the $q.all
function that can wait on multiple promises to get resolved. Once all the remote calls are complete, the then
success callback is invoked. The hypothetical buildModel
function is used to build a common Quote
model as the response can vary from one e-commerce platform to another. The defer.resolve
function finally collates the new model data and returns it in an array. A well-coordinated effort!
When it comes to creating and using the defer/Promise API there are some rules/guidance that come in handy. These include:
then
of the exiting promise object any number of times, irrespective of whether the promise has been resolved or not.then
on the existing resolved/rejected promise invokes the then
callback immediately.Other than creating our own promise and resolving it, there is another way to achieve the same behavior. We can use another Promise API function: $q.when
.
We will be super greedy and try to shave some more lines from the startBuilding
implementation by using the $q.when
function. Creating custom promising just to support a uniform return type (a promise) maybe an overkill here. The $q.when
function exists for this very purpose.
The when
function takes an argument and returns a promise:
when(value);
The value
can be a normal JavaScript object or a promise. The promise returned by when
is resolved with the value if it is a simple JavaScript type or with the resolved promise value if value
is a promise. Let's see how to use when
in startBuilding
.
Replace the existing startBuilding
implementation with this one:
service.startBuilding = function (name) { if (name) { return WorkoutService.getWorkout(name) .then(function (workout) { buildingWorkout = workout; newWorkout = false; return buildingWorkout; }); } else { buildingWorkout = new WorkoutPlan({}); newWorkout = true; return $q.when(buildingWorkout); } };
The changed code has been highlighted in the preceding code. And it is the else
condition where we use $q.when
to return a new WorkoutPlan
object, through a promise.
We have reduced some lines of code from startBuilding
and it still works fine. We now also have an understanding of $q.when
and where can it be used. It's time to complete the workout detail page fixes.
Fixing startBuilding
is enough to make the workout detail page load data. We can verify this and make sure the new workout and existing workout scenarios are loading data correctly.
We do not need to write a callback implementation in our WorkoutDetailController
. Why? Because the route resolve configuration takes care of it. We touched upon the resolve
route in the last Lesson when we used it to inject the selectedWorkout
object into WorkoutDetailController
. Let's try to understand how this refactoring for asynchronous calls and promise implementation has affected the resolve
function.
If we look at the new $routeProvider.when
configurations for the Workout Builder page (in the edit case), the selectedWorkout
function of resolve
has just one line now:
return WorkoutBuilderService.startBuilding($route.current.params.id);
As you learned in the previous Lesson, the resolve
configuration is used to inject dependencies into a controller before it is instantiated. In the preceding case, the return value now is a promise object, not a fully constructed WorkoutPlan
object.
When a return value of a resolve function is promise, Angular routing infrastructure waits for this promise to resolve, before loading the corresponding route. Once the promise is resolved, the resolved data is injected into the controller as it happens with standard return values. In our implementation too, the selected workout is injected automatically into the WorkoutDetailController
once the promise is resolved. We can verify this by double-clicking on the workout name tile on the list page; there is a visible delay before the Workout Builder page is loaded.
The clear advantage with the $routeProvider.when resolve
property is that we do not have to write asynchronous (then
) callbacks in the controller as we did to load the workout list in WorkoutListController
.
The exercise detail page too needs fixing, but since the implementation that we have shared does not use resolve
for the exercise detail page, we will have to implement the promise-based callback pattern to load the exercise in the init
controller function. The checkpoint2
folder under Lesson04
contains the fixes ExerciseBuilderService
and ExerciseDetailController
that you can copy to load exercise details, or you can do it yourself and compare the implementation.
It is now time to fix, create, and update scenarios for the exercises and workouts.
When it comes to the create, read, update, and delete (CRUD) operations, all save, update, and delete functions need to be converted to the callback promise pattern.
Earlier in the Lesson we detailed the endpoint access pattern for CRUD operations in a MongoLab collection. Head back to that section and revisit the access patterns. We need it now as we plan to create/update workouts.
Before we start the implementation, it is important to understand how MongoLab identified a collection item and what our ID generation strategy is . Each collection item in MongoDB is uniquely identified in the collection using the _id
property. While creating a new item, either we supply an ID or the server generates one itself. Once _id
is set, it cannot be changed. For our model, we will use the name
property of the exercise/workout as the unique ID and copy the name into the _id
field (hence, there is no autogeneration of _id
). Also, remember our model classes do not contain this _id
field, it has to be created before saving the record for the first time.
Let's fix the workout creation scenario first.
Taking the bottom-up approach, the first thing that needs to be fixed is WorkoutService
. Update the addWorkout
function as shown in the following code:
service.addWorkout = function (workout) { if (workout.name) { var workoutToSave = angular.copy(workout); workoutToSave.exercises = workoutToSave.exercises.map(function (exercise) { return { name: exercise.details.name, duration: exercise.duration } }); workoutToSave._id = workoutToSave.name; return $http.post(collectionsUrl + "/workouts", workoutToSave, { params: { apiKey: apiKey }}) .then(function (response) { return workout }); }}
In getWorkout
, we had to map data from the server model to our client model; the reverse has to be done here. Since we do not want to alter the model that is bound to the view, the first thing we do is make a copy of the workout.
Next, we map the exercises array (workoutToSave.exercises
) to a format that is more compact for server storage. We only want to store the exercise name and duration in the exercises
array on the server.
We then set the _id
property as the name of the workout to uniquely identify it in the database of the Workouts
collection.
A word of caution
The simplistic approach of using the name of the workout/exercise as a record identifier (or id
) in MongoDB will break for any decent-sized app. Remember that we are creating a web-based application that can be simultaneously accessed by many users. Since there is always the possibility of two users coming up with the same name for a workout/exercise, we need a strong mechanism to make sure names are not duplicated.
Another problem with the MongoLab REST API is that, if there is a duplicate POST
request with the same id
field, one will create a new document and the second will update it, instead of the second failing. This implies that any duplicate checks on the id
field on the client side still cannot safeguard against data loss. In such a scenario, assigning autogeneration of the id
value is preferable.
Lastly, we call the post
function of the $http
API, passing in the URL to connect to, data to send, and extra query string parameter (apiKey
). The last return
statement may look familiar as we again perform promise chaining to return the workout object as part of the promise resolution.
Why not try to implement the update operation? The updateWorkout
function can be fixed in the same manner, the only difference being that the $http.put
function is required:
return $http.put(collectionsUrl + "/workouts/" + workout.name, workoutToSave, { params: { apiKey: apiKey } });
The preceding request URL now contains an extra fragment (workout.name
) that denotes the identifier of the collection item that needs to be updated.
The MongoLab PUT
API request creates the document passed in as the request body, if not found in the collection. While making the PUTrequest, make sure that the original record exists. We can do this by making a GET
request for the same document first, and confirm that we get a document before updating it.
The last operation that needs to be fixed is deleting the workout. Here is a trivial implementation where we call the $http.delete
API to delete the workout referenced by a specific URL:
service.deleteWorkout = function (workoutName) { return $http.delete(collectionsUrl + "/workouts/" + workoutName, { params: { apiKey: apiKey } }); };
With that it's time now to fix WorkoutBuilderService
and WorkoutDetailController
. The save
function of WorkoutBuilderService
now looks like this:
service.save = function () { var promise = newWorkout ? WorkoutService.addWorkout(buildingWorkout) : WorkoutService.updateWorkout(buildingWorkout); promise.then(function (workout) { newWorkout = false; }); return promise; };
Most of it looks the same as it was earlier except that newWorkout
is flipped in the then
success callback and this returns a promise.
Finally, WorkoutDetailController
also needs to use the same callback pattern for handling save
and delete
, as shown here:
$scope.save = function () { $scope.submitted = true; // Will force validations if ($scope.formWorkout.$invalid) return; WorkoutBuilderService.save().then(function (workout) { $scope.workout = workout; $scope.formWorkout.$setPristine(); $scope.submitted = false; }); } service.delete = function () { if (newWorkout) return; // A new workout cannot be deleted. return WorkoutService.deleteWorkout(buildingWorkout.name); }
And that's it. We can now create new workouts, update existing workouts, and delete them too. That was not too difficult!
Let's try it out; open the new Workout Builder page, create a workout, and save it. Also try to edit an existing workout. Both scenarios should work seamlessly.
There is something interesting happening on the network side while we make POST
and PUT
requests to save data. Open the browsers network log console (F12) and see requests being made. The log looks something like this:
There is an OPTIONS request made to the same endpoint before the actual POST
is done. The behavior that we witness here is termed as a prefight request. And this happens because we are making a cross-domain request to api.mongolab.com
.
It is important to understand the cross-domain behavior of the HTTP request and the constructs AngularJS provides to make cross-domain requests.
Cross-domain requests are requests made for resources in a different domain. Such requests when originated from JavaScript have some restrictions imposed by the browser; these are termed as same-origin policy restrictions. This restriction stops the browser from making AJAX requests to domains that are different from the script's original source. The source match is done strictly based on a combination of protocol, host, and port.
For our own app, the calls to https://api.mongolab.com
are cross-domain invocations as our source code hosting is in a different domain (most probably something like http://localhost/....
).
There are some workarounds and some standards that help relax/control cross-domain access. We will be exploring two of these techniques as they are the most commonly used ones. These are as follows:
A common way to circumvent this same-origin policy is to use the JSONP technique.
The JSONP mechanism of remote invocation relies on the fact that browsers can execute JavaScript files from any domain irrespective of the source of origin, as long as the script is included via the <script>
tag. In fact, a number of framework files that we are loading in Personal Trainer come from a CDN source (ajax.googleapis.com
) and are referenced using the script
tag.
In JSONP, instead of making a direct request to a server, a dynamic script
tag is generated with the src
attribute set to the server endpoint that needs to be invoked. This script tag, when appended to the browser's DOM, causes a request to be made to the target server.
The server then needs to send a response in a specific format wrapping the response content inside a function invocation code (this extra padding around response data gives this technique the name JSONP).
The $http
.jsonp
function of AngularJS hides this complexity and provides an easy API to make JSONP requests. The jsFiddle link at http://jsfiddle.net/cmyworld/v9y4uby2/ highlights how JSONP requests are made. jsFiddle uses the Yahoo Stock API to get quotes for any stock symbol.
The getQuote
method in the fiddle looks like this:
$scope.getQuote = function () { var url = "https://query.yahooapis.com/v1/public/yql?q=select_*_from_yahoo.finance.quote_where_symbol_in_(%22" + $scope.symbol + "%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys&callback=JSON_CALLBACK"; $http.jsonp(url).success(function (data) { $scope.quote = data; }); };
To make a JSONP request using AngularJS, the jsonp
function requires us to augment the original URL with an extra query string parameter callback=JSON_CALLBACK
verbatim. Internally, the jsonp
function generates a dynamic script tag and a function. It then substitutes the JSON_CALLBACK
token with the function name generated and makes the remote request.
Open the preceding jsFiddle page and enter symbols such as GOOG
, MSFT
, or YHOO
to see the stock quote service in action. The browser network log for requests looks like this:
https://query.yahooapis.com/... &callback=angular.callbacks._1
Here, angular.callbacks._1
is the dynamically generated function. And the response looks like this:
angular.callbacks._1({"query": …});
The response is wrapped in the callback function. Angular parses and evaluates this response, which results in the invocation of the angular.callbacks._1
callback function. Then, this function internally routes the data to our success
function callback.
Hope this explains how JSONP works and what the underlying mechanism of a JSONP request is. But JSONP has its limitations, as given here:
GET
requests (which is obvious as these requests originate due to script tags)At the end, we must realize JSONP is more of a workaround that a solution. As we moved towards Web 2.0, where mashups became commonplace and more and more service providers decided to expose their API over the Web, a far better solution/standard emerged: CORS.
Cross-origin resource sharing (CORS) provides a mechanism for the web server to support cross-site access control, allowing browsers to make cross-domain requests from scripts. With this standard, the consumer application (such as Personal Trainer) is allowed to make some types of requests termed as simple requests without any special setup requirements. These simple requests are limited to GET
, POST
(with specific MIME types), and HEAD
. All other types of requests are termed as complex requests.
For complex requests, CORS mandates that the request should be preceded with a HTTP OPTIONS
request (also called a preflight request), that queries the server for HTTP methods allowed for cross-domain requests. And only on successful probing is the actual request made.
You can learn more about CORS from the MDN documentation available at https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS.
The best part about CORS is that the client does not have to make any adjustment as in the case of JSONP. The complete handshake mechanism is transparent to calling code and our AngularJS AJAX calls work without any hitch.
CORS requires configurations to be made on the server, and the MongoLab servers have already been configured to allow cross-domain requests. The preceding POST
request to MongoLab caused the preflight OPTIONS
request.
We have now covered the $http
service and cross-domain invocation topics. The next topic that needs our attention is the $resource
service.
Our discussion on the $resource
service should start with understanding why we require $resource
. The $http
service seems to be capable of performing all types of server interactions. Why is this abstraction required and against what type of system does the $resource
service work?
To answer all these questions, we have to introduce a new breed of service (server side, not Angular services): RESTful services.
"There is an API for that!"
Apple did not coin this, but this indeed is a reality now. There is an API for everything. Almost all of the public and private services (Google, Facebook, Twitter, and so on) out there have an API. And if the API works over HTTP, there is a pretty good chance that the API is RESTful in nature. We don't have to look far; MongoLab too has a RESTful API interface and we have used it!
Representational State Transfer (REST) is an architectural style that defines the components of a system as resources. Actions are defined at the resource level and the server controls how the process flows dynamically using the concept of hypermedia.
We will not be cover details about RESTful services here, but will concentrate our efforts on how AngularJS helps us consume RESTful services. If you are interested in discovering how a true RESTful service behaves, go through this excellent InfoQ article at http://www.infoq.com/articles/webber-rest-workflow. A fascinating read!
Most of the API interfaces that set out to be RESTful may not be a true RESTful service but may satisfy only a few constraints of a RESTful service. The RESTful service over HTTP has at least these common traits:
http://myserver.com/resources
http://myserver.com/resources/id
, where id
identifies a specific resource in the collectionGET
is used to retrieve data for collection or the collection item resourcePOST
is used to create a new resourcePUT
is used to update a resourceGo a few sections back to the Loading exercise and workout data section and look at the MongoLab service endpoint access patterns; they are consistent with what we have defined earlier.
AngularJS provides the $resource
service that specifically targets server implementations that have RESTful HTTP endpoints. In coming sections, we explain how $resource
works and implement part of our Personal Trainer app using the $resource
service.
The $resource
service is an abstraction built over the $http
service, and makes consuming RESTful services (server-based) easy. A resource in AngularJS is defined as follows:
$resource(url, [paramDefaults], [actions]);
The parameters used are:
url
: This specifies the endpoint URL. This URL can be parameterized with parameterized arguments prefixed with :
: For example, these are valid URLs:/collection/:identifier
: This indicates a URL with a parameterized identifier fragment/:collection/:identifier
: This indicates a URL with collection and identifier parameterizedIf the parameter value is not available during invocation, the parameter is removed from the URL. See the following examples to understand how this URL parameterization works.
paramDefaults
: This parameter serves a dual purpose. For parameterized URLs, paramDefaults
provides a default replacement whereas any extra values in the paramDefaults
object are added to a query string.Consider a resource url /users/:name.
The following table details the resultant URL based on the paramDefaults
passed:
The paramDefaults value |
The Resultant URL |
---|---|
|
|
|
|
|
|
|
|
As we will learn later, these parameters can be overridden during actual action invocation.
actions
: This parameter is nothing but a JavaScript function attached to the $resource
object to perform a specific task. The $resource
object comes with a standard set of operations that are common to every resource such as get
, query
, save
, and delete
. This actions
parameter is used to extend the default list of actions with our own custom action or alter any predefined action.The actions
parameter takes an object hash, with the key being action name
and the value being a config
object. This is the same config
object that is used with the $http
service (passed in as the second parameter to $http
).
Creating a resource with the preceding resource declaration statement actually creates a Resource
class. This Resource
class encapsulates the configuration that we have defined while creating it. To make HTTP requests using this class, we need to invoke the action methods that are available on the class, including the custom ones that we define.
Let's look at some concrete examples on how to invoke resource actions and also try to understand a bit more about the third parameter to resource creation, actions
.
To understand how to invoke resource actions and the role the actions
parameter plays while defining a resource, let's look at an example. Consider this resource usage:
var Exercises = $resource('https://api.mongolab.com/api/1/databases/angularjsbyexample/collections/exercises/:param,{},{update:{action:PUT'}});
This statement creates a Resource
class named Exercises
with a total of six class-level actions namely get
, save
, update
, query
, remove
, and delete
. Five of these actions are standard actions defined on any resource. The sixth one, update
, has been added to this resource class by passing in the actions
parameter (the third argument). The actions
parameter declaration looks like this:
actions:{action1: config, action2 : config, action3 : config}
This line defines three actions and configurations for those actions. The config
object is the same object passed as a parameter to $http
.
In the preceding scenario, the config
object passed in for the update
action has only one property action
(not to be confused with $resource
actions parameter), which specifies the HTTP action verb to use on invocation of the action method: update
.
For the five default actions on $resource
the standard config
is:
{ 'get': {method:'GET'},
'save': {method:'POST'},
'query': {method:'GET', isArray:true},
'remove': {method:'DELETE'},
'delete': {method:'DELETE'}
};
The HTTP verb on these actions makes perfect sense and complies with the RESTful URL access pattern. The surprising part is the omission of the update
action or an action that does the HTTP PUT
operation. Hence, when defining a RESTful endpoint, we may require to augment the action list with a PUT
based update
action. The first example described previously does this.
In the preceding configuration, the isArray
attribute on the query
action seems interesting. To understand the behavior of isArray
, we need to see how resource actions are invoked.
The resource statement in the preceding section just creates a resource class named Exercises
. To actually invoke a server operation, we need to invoke one of the six action methods defined in the Exercises
class. Here are some sample invocations:
Exercises.query();// get all workouts Exercises.get({id:'test'}); // get exercise with id 'test' Exercises.save({},workout); // save the workout
For action methods based on GET
, the general syntax is as follows:
Exercises.actionName([parameters],[successcallback], [errorcallback]);
And for POST
actions (save
and update
), the general syntax is as follows:
Exercises.actionName([parameters], [postData], [successcallback], [errorcallback]);
For POST
actions, there is an extra postData
parameter to post the actual payload to the server.
The last two parameters: successcallback and errorcallback get called when the response is received based on the response status.
When a resource action is invoked, it returns either of these:
Resource
class object (the resource object): This is returned when the isArray
action configuration is false
, for example, the get
actionisArray
action configuration is true
, for example, the query
actionThis is in sharp contrast to the $http
invocation that returns a promise.
And if we keep holding the returned value, then AngularJS fills this object or array with the response received from a server in future. This behavior results in code that is devoid of callback pattern implementation. For example, we can load exercises in ExerciseListController
using this statement:
$scope.exercises = Exercises.query();
The preceding query
invocation immediately returns an empty array. In future, when the response arrives, it is pushed into the array. And due to the super awesome data-binding infrastructure that Angular has, any view bindings for the exercises
array get automatically refreshed.
Another interesting thing about the isArray
action configuration is that a misconfigured isArray
attribute can cause response parsing issues. The isArray
attribute helps AngularJS decide whether to de-serialize the response as an array or object. If configured incorrectly, Angular throws errors such as this:
"Error in resource configuration. Expected response to contain an object but got an array"
Alternatively, it throws errors such as this:
"Error in resource configuration. Expected response to contain an array but got an object"
It is very easy to reproduce these errors. Let's try these calls in this way:
Exercises.get(); // Returns an array Exercises.query({params:'plank'}); //Returns exercise object
The first statement in the preceding code results in the first error, and the second statement in the second error. Look at the configurations for action methods: get
and query
, to know why there were errors.
Before we move forward, there is something that needs to be reiterated. There is a marked difference between the $resource
and $http
return values. The return value of $http
invocation is always a promise whereas it can be a Resource
class object or an array for $resource
. Due to this reason, binding of the $resource
response is possible to view without involving callbacks.
The resource object or collection returned as part of the action invocation contains some useful properties:
$promise
: This is the underlying promise for the request made. We can wait over it if desired, similar to the $http
promise. Else, we can use the successcallback
or errorcallback
functions that we register when invoking the resource action.$resolved
: This is True
after the preceding promise has been resolved, false
otherwise.Let us change parts of our Personal Trainer app to use server access based on $resource
and put what we have learned into practice.
Until now, we have used $http
for exercise/workout data management. To elaborate on the $resource
behavior, let's change the exercise data load and save this to use the $resource
service.
Open the services.js
file and add the following lines to the WorkoutService
implementation above the service.getExercises
function:
service.Exercises = $resource(collectionsUrl + "/exercises/:id", { apiKey: apiKey}, { update: { method: 'PUT' } });
The statement creates a Resource
class configured with a specific URL and API key. The key is passed in to the default parameter collection.
Go ahead and delete all exercise-related functions from WorkoutService
. These include the service.getExercises
, service.getExercise
, service.updateExercise
, service.addExercise
, and service.deleteExercise
functions. Everything related to the exercise will be done using resources now.
The $resource
function is part of the ngResource
module; therefore, we need to include the module script in index.html
. Add this line to the script section after other AngularJS module declarations:
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.3/angular-resource.js"></script>
Include the ngResource
module dependencies in app.js
, as follows:
angular.module('app', […,'ngResource']);
Finally, add $resource
as a dependency to WorkoutService
. Remember that the dependency needs to be added to the this.$get
function.
These changes have also affected the service.getWorkout
function as it has a dependency on the getExercises
function. To fix it, replace the service.getExercise()
call inside $q.all
with this:
service.Exercises.query().$promise
The query
action returns an empty array that has a predefined $promise
property that $q.all
can wait over.
Let's now fix the upstream caller as we have removed a number of service functions.
To start with, let's fix the exercise list implementation as it is the easiest to fix. Open exercise.js
from the WorkoutBuilder
folder and fix the init
method for ExerciseNavController
. Replace its implementation with this single line:
$scope.exercises = WorkoutService.Exercises.query();
Do the same with ExerciseListController
, replacing the init
function implementation with the preceding code.
The empty array returned by the query
action in the preceding code is filled in the future when the response is available. Once the model exercises
updates, the bound view is automatically updated. No callback is required!
Next, we fix the exercise builder page (#/builder/exercises/new
), the corresponding ExerciseDetailController
object, and downstream services. All $http
calls need to be replaced with $resource
calls. Open services.js
from workoutbuilder
and fix the startBuilding
function in ExerciseBuilderService
in this way:
service.startBuilding = function (name) { if (name) { buildingExercise = WorkoutService.Exercises.get({ id: name }, function (data) { newExercise = false; }); } else { buildingExercise = new Exercise({}); newExercise = true; } return buildingExercise; };
We use the get
action method of the Exercise
resource to get the specific exercise, passing in the name of the exercise ({id:name}
). Remember, the name of the exercise is the exercise identifier.
Before we turn the newExercise
flag to false we need to wait for the response. We make use of the success callback for that. Interestingly, the data
argument to a function and the buildingExercise
variable point to the same resource object.
The else part has been reverted to the older pre-$http
implementation as we do not use promises anymore.
To fix the ExerciseDetailController
implementation, we just need to revert the init
function to the non-callback pattern implementation:
$scope.exercise = ExerciseBuilderService.startBuilding($routeParams.id);
All the get
scenarios on the exercises are fixed now. The code has indeed been simplified. The callbacks that were with the $http
implementation have been eliminated to a large extent. The asynchronous nature of the calls is almost hidden, which is both good and bad. It is good because it simplifies code but it is bad because it hides the asynchronicity. This often leads to an incorrect understanding of behavior and bugs.
The ultimate aim of $resource
is to make consumption of RESTful services easier. It also helps reduce the callback implementation that we need to do otherwise. But this abstraction comes at a cost. For example, consider this piece of code:
$scope.exercises = WorkoutService.Exercises.query(); console.log($scope.exercises.length);
We may think console.log
prints the length of the exercises
array, but that is absolutely incorrect. In fact, $scope.exercises
is an empty array so log
will always show 0
. The array is filled in the future with the data returned from the server. The JavaScript engine does not wait on the first line for the response to arrive. Such code just gives us the illusion that everything runs sequentially, but it does not.
UI data binding still works because the Angular digest cycles are executed when the $resource
service receives a response from the server.
As part of this digest cycle, dirty checks are performed to detect model changes across the app. All these model changes trigger watches that result in UI bindings and interpolation updates. Remember, we covered the topic of digest cycles in Lesson 2, More AngularJS Goodness for 7 Minute Workout.
If any of our operations depend upon when the data is available, we need to implement a callback pattern using promises. We did it with the startBuilding
function where we waited for exercise details to load before setting the newExercise
flag.
We now need to fix CRUD operation for exercises.
The Exercise
resource defined in WorkoutService
already has the save
and update
(custom actions that we added) action. It's now just a matter of invoking the correct action inside the WorkoutBuilderService
functions.
The first ExerciseBuilderService
function we fix is save
. Update the save
implementation with the following code:
service.save = function () { if (!buildingExercise._id) buildingExercise._id = buildingExercise.name; var promise = newExercise ? WorkoutService.Exercises.save({},buildingExercise).$promise : buildingExercise.$update({ id: buildingExercise.name }); return promise.then(function (data) { newExercise = false; return buildingExercise; }); };
In the previous implementation based on the newExercise
state, we call the appropriate resource action. We then pull out the underlying promise and again perform promise chaining to return the same exercise in future using then
.
The save
operation not only uses a Resource
(Exercise
) class but also a Resource
object (buildingExercise
). The preceding code illustrates an important difference between the Resource
class and the resource
object. Remember buildingExercise
is a resource object that we assigned during the invocation of the startBuilding
function in ExerciseDetailController
.
A resource object is typically created when we invoke get
operations on the corresponding Resource
class, such as this:
buildingExercise = WorkoutService.Exercises.get({ id: name });
This operation creates an exercise resource object. And the following operation creates an array:
$scope.exercises = WorkoutService.Exercises.query();
The array is filled with exercise resource objects when the response is received.
The actions defined on a resource object are the same as the Resource
class except that all action names are prefixed with $
. Also, resource object actions can derive data from the resource object itself. For example, in the preceding code, buildingExercise.$update
does not take the payload as an argument whereas the payload is required when using the Exercise.save
action (the second argument).
The following table contrasts the Resource
class and resource object usage:
Resource class |
Resource object | |
---|---|---|
Creation |
This is created using |
This is created as part of action execution. Here is an example:
|
Actions (querying) |
|
|
Actions (CRUD) |
|
|
Action returns |
This returns the |
This returns a promise object. |
Deleting is simple; we just call the $delete
action on the resource object and return the underlying promise:
service.delete = function () { return buildingExercise.$delete({ id: buildingExercise.name }); };
WorkoutDetailController
needs no fixes as the return value for save
and delete
functions on WorkoutBuilderService
is still a promise.
The $resource
function fixes are complete and we can now test our implementation. Try to load and edit exercises and verify that everything looks good.
The $resource
function is a pretty useful service from AngularJS for targeting RESTful HTTP endpoints. But what about other endpoints that might be non-conformant? Well, for non-RESTful endpoints, we can always use the $http
service. Still, if we want to use the $resource
service for the non-RESTful resources, we need to be aware of access pattern differences.
As long as the HTTP endpoint returns and consumes JSON data (or data that can be converted to JSON), we can consume that endpoint using the $resource
service. In such cases, we may need to create multiple Resource
classes to target querying and CRUD-based operations. For example, consider these resources declarations:
$resource('/users/active'); //for querying $resource('/users/createnew'); // for creation $resource('/users/update/:id'); // for update
In such a case, most of the action invocation is limited to the Resource
class, and resource object-level actions may not work.
Such endpoints might not even conform to the standard HTTP action usage. An HTTP POST
request may be used for both saving and updating data. The DELETE
verb may not be supported. There might also be other similar issues.
That sums up all that we plan to discuss on $resource
. Let's end our discussion by summarizing what you have learned thus far:
$resource
is pretty useful for targeting RESTful service interactions. But still it can be used for non-RESTful endpoints.$resource
can reduce a lot of boilerplate code required for server interaction if an endpoint confirms to RESTful access patterns.$resource
action invocation returns a resource object or array that is updated in the future. This is in contrast with $http
invocation that always returns a promise object.$resource
actions return resource objects, we can implement some scenarios without using callback. This still does not mean calls using the $resource
service are synchronous.We have now worked our way through using the $http
and $resource
services. These are more than capable services that can take care of all your server interaction needs. In upcoming sections, we will explore some general usage scenarios and some advance concepts related to the $http
and $resource
services. The first in line is the request/response interceptors.
Request and response interceptors, as the names suggest, can intercept HTTP requests and responses to augment/alter them. The typical use cases for using such interceptors include authentication, global error handling, manipulating HTTP headers, altering endpoint URLs, global retry logic, and some other such scenarios.
Interceptors are implemented as pipeline functions that get called one after another just like the parser and formatter pipelines for NgModelController
(see the previous Lesson).
Interceptions can happen at four places and hence there are four interceptor pipelines. This happens:
Interceptors in Angular are mostly implemented as a service factory. They are then added to a collection of interceptors defined over $httpProvider
during the configuration module stage.
A typical interceptor service factory outline looks something like this:
myModule.factory('myHttpInterceptor', function ($q, dependency1, dependency2) { return { 'request': function (config) {}, 'requestError': function (rejection) {}, 'response': function (response) {}, 'responseError': function (rejection) {} };});
And this is how it is registered at the configuration stage:
$httpProvider.interceptors.push('myHttpInterceptor');
The request
and requestError
interceptors are invoked before a request is sent and the response
and responseError
interceptors are invoked after the response is received. It is not mandatory to implement all four interceptor functions. We can implement the ones that serve our purpose.
A skeleton implementation of interceptors is available in the framework documentation for $http
(https://code.angularjs.org/1.3.3/docs/api/ng/service/$http) under the Interceptors section.
To see an interceptor in action, let's implement one!
The WorkoutService
implementation is littered with API key references within every $http
or $resource
call/declaration. There is code like this everywhere:
$http.get(collectionsUrl + "/workouts", { params: { apiKey: apiKey } })
Every API request to MongoLab requires an API key to be appended to the query string. And, it is quite obvious that if we implement a request interceptor that appends this API key to every request made to MongoLab, we can get rid of this params
assignment performed in every API call.
Time to get in an interceptor! Open services.js
under shared
and add these lines of code at the end of the file:
angular.module('app').provider('ApiKeyAppenderInterceptor', function () { var apiKey = null; this.setApiKey = function (key) { apiKey = key; } this.$get = ['$q', function ($q) { return { 'request': function (config) { if (apiKey && config && config.url.toLowerCase() .indexOf("https://api.mongolab.com") >= 0) { config.params = config.params || {}; config.params.apiKey = apiKey; } return config || $q.when(config); } } }]; });
We create a 'ApiKeyAppenderInterceptor'
provider service (not a factory). The provider function setApiKey
is used to set up the API key before an interceptor is used.
For the factory function that we return as part of $get
, we only implement a request interceptor. The request
interceptor function takes a single argument: config
and has to return the config
object or a promise that resolves to the config
object. The same config
object is used with the $http
service.
In our request interceptor implementation, we make sure that the apiKey
has been set and the request is for api.mongolab.com
. If true
, we update the configuration's param
object with apiKey
and this results in the API key being appended to the query string.
The interceptor implementation is complete but the way we have implemented this interceptor requires some other refactoring.
The WorkoutService
method now does not need the API key, therefore we need to fix the configure
function. Update the config.js
file and add a dependency of ApiKeyAppenderInterceptorProvider
on the config
module function.
Inside the config
function, add the following lines at the start:
ApiKeyAppenderInterceptorProvider.setApiKey("<mykey>"); $httpProvider.interceptors.push('ApiKeyAppenderInterceptor');
Update the configure
method of WorkoutServiceProvider
to this:
WorkoutServiceProvider.configure("angularjsbyexample");
The configure
function declaration in WorkoutServiceProvider
itself needs to be fixed. Open the services.js
file from shared
and fix the configure function as shown here:
this.configure = function (dbName) { database = database; collectionsUrl = apiUrl + dbName + "/collections"; }
The last part is now to actually remove references to the API key from all $http
and $resource
calls. The resource declaration now should look like this:
$resource(collectionsUrl + "/exercises/:id", {}, { update: { method: 'PUT' } });
And for all $http
invocations, get rid of the params
object.
Time to test out the implementation! Load any of the list or details pages and verify them. Also try to add breakpoints in the interceptor code and see how the process flows.
Request/response interception is a powerful feature that can be used to implement any cross-cutting concern related to remote HTTP invocation. If used correctly, it can simplify implementation and reduce a lot of boilerplate code.
Interceptors work at a level where they can manipulate the complete request and response. These work from headers, to the endpoint, to the message itself! There is another related concept that is similar to interceptors but involves only request and response payload transformation and is aptly named AngularJS transformers.
The job of a transformer or a transformer function is to transform the input data from one format to another. These transformers plug into the HTTP request/response processing pipeline of Angular and can alter the message received or sent. A good example of the transformation function usage is AngularJS global transformers that are responsible for converting a JSON string response into a JavaScript object and vice versa.
Since data transformation can be done while making a request or processing a response, there are two transformer pipelines available, one for a request and another for a response.
Transformer functions can be registered:
$httpProvider.defaults.transformRequest
or $httpProvider.defaults.transformResponse
array. As always with a pipeline, order of registration is important. Global transformer functions are invoked for every request made or response received using the $http
service, depending upon the pipeline they are registered in.$http
or $resource
action invocation. The config
object has two properties: transformRequest
and transformResponse
, which can be used to register any transformer function. Such a transformer function overrides any global transformation functions for that action.The $httpProvider.defaults
or $http.defaults
function also contains settings related to default HTTP headers that are sent with every HTTP request.
This configuration can come in handy in some scenarios. For example, if the backend requires some specific headers to be passed with every request, we can use the $http.defaults.headers.common
collection to append this custom header:
$http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w'
Coming back to transformers! From an implementation standpoint, a transformer function takes a single argument, data
, and has to return the transformed data.
Next, we have an implementation for one such transformer that AngularJS uses to convert a JavaScript object to a JSON string. This is a part of the AngularJS framework code:
function(d) { return isObject(d) && !isFile(d) ? toJson(d) : d; }
The function takes data
and transforms it into a string by calling an internal method toJson
and returning the string representation. This transformer is registered in the global request transformer pipeline by the framework.
Local transformation functions are useful if we do not want to use the global transformation pipeline and want to do something specific. The following example shows how to register a transformer at the action or HTTP request level:
service.Exercises = $resource(collectionsUrl + "/exercises/:id", {}, { update: { method: 'PUT' }, get: { transformResponse: function (data) { return JSON.parse(data); } } });
In this Resource
class declaration we register a response transformer for the get
action. This function converts the string input (data
) into an object, something similar to what the global response transformer does.
In the preceding example, the data
variable will contain the string value of a response received from a server instead of the deserialized object. By supplying our custom response transformer to tranformResponse
, we have overridden the default transformer that deserializes JSON response.
If we need to run global transform functions too, we need to create an array of transformers, containing both the global and custom transformers, and assign it to transformRequest
or transformResponse
, something like this:
service.Exercises = $resource(collectionsUrl + "/exercises/:id", {}, { update: { method: 'PUT' }, get: { transformResponse: $http.defaults.transformResponse.concat(function (value) { return doTransform(value); }) } });
The next topic that we take up here is route resolution when promises are rejected.
The Workout Builder page in Personal Trainer depends upon the resolve
route configuration to inject the selected workout into WorkoutDetailController
.
The resolve
configuration has an additional advantage if any of the resolve functions return a promise like the selectedWorkout
function:
return WorkoutBuilderService.startBuilding($route.current.params.id);
When the promise is resolved successfully, the data is injected into the controller, but what happens on promise rejection or error? The preceding promise can fail if we enter a random workout name in the URL such as /builder/workouts/dummy
and try to navigate, or if there is a server error. With a failed promise, two things happen:
$routeChangeError
event is broadcasted on $rootScope
(remember Angular events $emit
and $broadcast
).We can use this event to give visual clues to a user about the path/route not found. Let's try to do it for the Workout Builder route.
We can an some error on the page if the user tries to navigate to a non-existing workout. The error has to be shown at the container level outside the ng-view
directive.
Update index.html
and add this line before the ng-view
declaration:
<label ng-if="routeHasError" class="alert alert-danger">{{routeError}}</label>
Open root.js
and update the event handler for the $routeChangeSuccess
event with the highlighted code:
$scope.$on('$routeChangeSuccess', function (event, current,previous) {
$scope.currentRoute = current;
$scope.routeHasError = false;
});
Add another event handler for $routeChangeError
:
$scope.$on('$routeChangeError', function (event, current, previous, error) { if (error.status === 404 && current.originalPath === "/builder/workouts/:id") { $scope.routeHasError = true; $scope.routeError = current.routeErrorMessage;} });
Lastly, update config.js
by adding the routeErrorMessage
property on the route configuration to edit workouts:
$routeProvider.when('/builder/workouts/:id', {
// existing configuration
topNav: 'partials/workoutbuilder/top-nav.html',
routeErrorMessage:"Could not load the specific workout!",
//existing configuration
Now go ahead and try to load a workout route such as this: /builder/workouts/dummy
; the page should show an error message.
The implementation was simple. We declared model properties routeError
to track the error message and routeHasError
to determine whether the route has an error.
On the $routeChangeSuccess
and $routeChangeError
event handler, we manipulate these properties to produce the desired result. The implementation of $routeChangeError
has extra checks to make sure that the error is only shown when the workout is not found. Take note of the routeErrorMessage
property that we define on the route configuration. We did such route configuration customization in the last Lesson for configuring navigation elements for the active view.
We have fixed routing failure for the Workout Builder page, but the exercise builder page is still pending. And again, I will leave it to you to fix it yourself and compare it with the implementation available in the companion codebase.
Another major implementation that is pending is fixing of 7 Minute Workout as currently it caters only to one workout routine.
As it stands now, the 7 Minute Workout (or Workout Runner) app can only play one specific workout. It needs to be fixed to support execution of any workout plan built using Personal Trainer. There is an obvious need to integrate these two solutions. We already have the groundwork done to commence this integration. We have the shared model services and we have the WorkoutService
to load data—enough to get us started.
Fixing 7 Minute Workout and converting it into a generic Workout Runner roughly involves the following steps:
WorkoutService
and starting the workout.And, of course, we need to rename the 7 Minute Workout part of the app; the name now is a misnomer. I think the complete app can now be called Personal Trainer. We can remove all references to 7 Minute Workout from the view as well.
3.145.201.17