Lesson 4: Adding Data Persistence to Personal Trainer

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:

  • Provisioning a backend to persist workout data: We set up a MongoLab account and use its REST API to access and store workout data.
  • Understanding the $http service: $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.
  • Implementing, loading, and saving workout data: We use the $http service to load and store workout data into MongoLab databases.
  • Creating and consuming promises: We touched upon promises in the earlier Lessons. In this Lesson, not only do we consume promises (part of HTTP invocation) but we also see how to create and resolve our own promises.
  • Working with cross-domain access: As we are interacting with a MongoLab server in a different domain, you learn about browser restrictions on cross-domain access. You also learn how JSONP and CORS help us make cross-domain access easy and about AngularJS JSONP support.
  • Using the $resource service for RESTful endpoints: The $resource service is an abstraction built over $http to support the RESTful server endpoints. You learn about the $resource service and its usage.
  • Loading and saving exercise data with $resource: We change parts of the system to employ the $resource service to load and save exercise data.
  • Request/response interceptors: You learn about interceptors and how they are used to intercept calls in a request/response pipeline and alter the remote invocation flow.
  • Request/response transformers: Similar to interceptors, transformers function at the message payload level. We explore the working of transformers with examples.

Let's get the ball rolling.

AngularJS and server interactions

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.

Setting up the persistence store

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.

Note

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:

  1. Go to https://mongolab.com and sign up for a MongoLab account by following the instructions on the website.
  2. Once the account is provisioned, login and create a new Mongo database by clicking on the Create New button in the home page.

    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:

    Setting up the persistence store
  3. Create the database and make a note of the database name that you create.
  4. Once the database is provisioned, open the database and add two collections to it from the Collection tab:
    • exercises: This stores all Personal Trainer exercises
    • workouts: This stores all Personal Trainer workouts

    Collections in the MongoDB world equate to a database table.

    Note

    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.

  5. Once the collections are added, add yourself as a user to the database from the User tab.
  6. The next step is to determine the API key for the MongoLab account. The provisioned API key has to be appended to every request made to MongoLab. To get the API key, perform the following steps:
    1. Click on the username (not the account name) in the top-right corner to open the user profile.
    2. In the section titled API Key, the current API key is displayed; copy it.

The datastore schema is complete; we now need to seed these collections.

Seeding the database

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:

Seeding the database

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.

$http service basics

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.

Note

HTTP responses in the - range 200-299 are considered successful. Responses in the range of 40x and 50x are treated as failure and result in the error callback function being 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:

  • For standard 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.
  • For 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.

Note

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.

Personal Trainer and server integration

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.

Loading exercise and workout data

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.

Note

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 name
    • PUT: This updates the specific item (id) in the collection name
    • DELETE: This deletes the item with a specific ID from the collection name

Note

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.

Note

Before we start, please download the working copy of Personal Trainer from Lesson03/checkpoint7. It contains the complete implementation for Personal Trainer including exercise building, which was left as a Do-it-yourself (DIY) assignment for everyone to try.

Loading exercise and workout lists from a server

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:

Loading exercise and workout lists from a server

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 the WorkoutService provider

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.

Note

There was this issue while writing this Lesson. The newer versions of Angular 1.3 and above have fixed this issue. We will still continue with the approach outline given next as it works irrespective of the version of Angular.

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.

Note

Look at the complete implementation in Lesson04/checkpoint1 if you are having difficulty in retrieving/showing data.

Also remember to replace the database name and API key before running the code from checkpoint1.

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):

Implementing the WorkoutService provider

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 application models

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:

  • Our model defines any functions
  • A stored model stored is different from its representation in code
  • The same model class is used to represent data from different sources (this can happen for mashups where we pull data from disparate sources)

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:

Mapping server data to application models

The two major differences between a model and server data are as follows:

  • The model defines the totalWorkoutDuration function.
  • The exercises array representation also differs. The exercises array of a model contains the Exercise object (the details property) whereas the server data stores just the exercise identifier or name.

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:

  • Firstly, inside the 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.
  • Secondly, this is a good example of promise chaining in action. The $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:
    Mapping server data to application models

    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.

    Note

    The promise returned by then is resolved by the return value of its success and error callback function.

    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.

  • Lastly, we are using the promise function 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.

Note

Since we are using then instead of success, the callback function receives a single object with all four properties config, data, header, and status.

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.

Understanding promise chaining

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);});

Note

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.

Note

The $q.when function returns a promise that resolves to a value 1.

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.

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.

Note

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.

Fixing workout and 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 custom promises

Creating and resolving a standard promise involves the following steps:

  1. Create a new 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.
  2. Return the promise object by calling defer.promise at the end of the function call.
  3. Any time in the future, use a 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:

  • A promise once resolved cannot be changed. A promise is like a return statement that gets called in the future. But once a promise is resolved, the value cannot change.
  • We can call then of the exiting promise object any number of times, irrespective of whether the promise has been resolved or not.
  • Calling 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.

The $q "when" function

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 workout and exercise detail pages continued…

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.

Route resolutions and promises

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.

Note

The checkpoint2 folder under Lesson04 contains the working implementation for what we have covered thus far.

It is now time to fix, create, and update scenarios for the exercises and workouts.

Performing CRUD on exercises/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.

Fixing and creating a new workout

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.

Note

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.

Note

In standard create entity cases, unique ID generation is done on the server (mostly by the database). The response to the create entity then contains the generated ID. In such a case, we need to update the model object before we return data to the calling code.

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.

Note

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.

Note

Check Lesson04/checkpoint3 for an up-to-date implementation if you are having issues running your local copy.

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:

Fixing and creating a new workout

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 access and AngularJS

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:

  • JSON with Padding (JSONP)
  • Cross-origin resource sharing (CORS)

A common way to circumvent this same-origin policy is to use the JSONP technique.

Using JSONP to make cross-domain requests

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:

  • Firstly, we can only make GET requests (which is obvious as these requests originate due to script tags)
  • Secondly, the server also needs to implement part of the solution that involved wrapping the response in a function callback as seen before
  • Then there is always a security risk involved as JSONP depends upon dynamic script generation and injection
  • Error handling too is not reliable because it is not easy to determine whether a script load failed due to some reason

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

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.

Note

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.

Getting started with $resource

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.

RESTful API 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.

Note

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:

  • Resources are defined using URLs. These are some of the resources:
    • There is a collection resource with the URL format as http://myserver.com/resources
    • There is a collection item resource with the URL format http://myserver.com/resources/id, where id identifies a specific resource in the collection
  • The HTTP verb GET is used to retrieve data for collection or the collection item resource
  • HTTP POST is used to create a new resource
  • HTTP PUT is used to update a resource
  • HTTP DELETE is used to delete a resource

Go 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.

$resource basics

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 parameterized

    If 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

    {}

    /users

    {name:'david'}

    /users/david

    {search:'david'}

    /users?search=david

    {name:'david', search:'out'}

    /users/david?search=out

    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.

Understanding $resource 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.

$resource action invocation

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:

  • A Resource class object (the resource object): This is returned when the isArray action configuration is false, for example, the get action
  • An empty array: This is returned when the isArray action configuration is true, for example, the query action

This 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.

Using $resource to access exercise data

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 hidden cost of hiding asynchronicity

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.

Note

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.

Note

I am not advocating that you don't use $resource; in fact it is a great service that can help eliminate a sizable amount of code otherwise required with the $http implementation. But everyone using $resource should be aware of the peculiarities involved.

We now need to fix CRUD operation for exercises.

Exercising CRUD with $resource

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 $resource(url, param, actions).

This is created as part of action execution.

Here is an example:

exercise = WorkoutService
.Exercises.get({ id: name });

Actions (querying)

Exercises.get({id:name});
Exercise.query();

exercise.$get({id:name});
exercise.$query();

Actions (CRUD)

Exercise.save({}, data);
Exercise.update({id:name},
data);
Exercise.delete({id:name});

exercise.$save({});
exercise.$update({id:name});
exercise.$delete({id:name});

Action returns

This returns the Resource object or array, with the $promise and $resolved properties.

This returns a promise object.

Note

Use resource objects when the operation performed is in the context of a single item, such as update and delete operations. Otherwise, stick to the $resource service.

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.

Note

If you are having issues, Lesson04/checkpoint4 contains the complete working implementation.

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.

The $resource service with non-RESTful endpoints

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.
  • Because $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/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:

  • Before a request is sent.
  • After there is a request error. A request error may sound strange but, in a pipeline mode when the request travels through the pipeline function and any one of them rejects the request (for reasons such as data validation), the request lands up on an error pipeline with the rejection reason.
  • After receiving the response from the server.
  • On receiving an error from the server, or from a response pipeline component that may still reject a successful response from the server due to some technicalities.

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.

Note

The Angular $httpProvider function is something that we have used here for the first time. Like any provider, it too allows us to configure $http service behavior at the configuration stage.

To see an interceptor in action, let's implement one!

Using an interceptor to pass the API key

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.

Note

The update code is available in Lesson04/checkpoint5 for reference.

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.

AngularJS request/response 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:

  • Globally for all requests/responses. The standard JSON string-object transformers are registered at a global level. To register global transformer function we need to push or shift a function either to the $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.

    Note

    The $http service too contains $http.defaults, which is equivalent to $httpProvider.defaults. This allows us to change these configurations at any time during app execution.

  • Locally on a specific $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.

Note

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.

Note

A word of caution

Using a local transform function with specific $resource or $http overrides any global transformation function.

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.

Handling routing failure for rejected promises

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:

  • Firstly, the app route does not change. If you refresh the page using the browser, the complete content is cleared.
  • Secondly, a $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.

Handling workouts not found

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.

Handling workouts not found

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.

Note

Checkout the implementation done so far in Lesson04/checkpoint6.

Another major implementation that is pending is fixing of 7 Minute Workout as currently it caters only to one workout routine.

Fixing the 7 Minute Workout app

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:

  1. Removing the hardcoded workout and exercises used in 7 Minute Workout from the controller.
  2. Fixing the start page to show all available workouts and allowing users to select a workout to run.
  3. Fixing the workout route configuration to pass the selected workout name as the route parameter to the workout page.
  4. Loading the selected workout data using 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.

Fixing the 7 Minute Workout app
Fixing the 7 Minute Workout app
Fixing the 7 Minute Workout app
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.201.17