Wednesday 13 March 2013

Moving AJAX calls to a custom service in Angular JS

In my last post on Angular JS, we moved the data into an ASP.NET Web API controller and invoked the data using $http, a service in Angular JS that abstracts AJAX calls. But the code resided in the controller and that is not good. Responsibility of the controller should be to sit between data and view, but not to call services using AJAX. So, let’s move the AJAX calls to a separate component.

The AJAX calls made using $http service are executed asynchronously. They return promise objects, using which their status can be tracked. As we will be moving them into a custom service, the controller depending on them should be notified with the status of completion. The notification can be sent using the promise object returned by the service functions or, we can create our own deferred object using $q service.

I discussed about deferred and promise in jQuery earlier in a post. Basic usage of the pattern remains the same with $q as well, except a small difference syntax. Following snippet is shows the way of creating a deferred object, calling success and failure functions using $q:

function myAsyncFunc(){
//Creating a deferred object
var deferred = $q.defer();
operation().success(function(){
    //Calling resolve of deferred object to execute the success callback
    deferred.resolve(data);
  }).error(function(){
    //Calling reject of deferred object to execute failure callback
    deferred.reject();
  });
  //Returning the corresponding promise object
  return deferred.promise;
}

While calling the above function, a callback to be called on success and a callback to be handled on failure should be attached. It might look as follows:
myAsyncFunction().then(function(data){
    //Update UI using data or use the data to call another service
  },
  function(){
    //Display an error message
  });

The functions responsible for making AJAX calls should follow this pattern as the functions in $http are executed asynchronously. To notify the dependent logic about state of execution of the functionality, we have to use the promise pattern.

As we will be enclosing this functionality in a custom service which requires $http for AJAX and $q for promise, these dependencies will be injected at the runtime when the module is loaded. Following snippet demonstrates the implementation of a function to retrieve items in shopping cart calling Web API service:

angular.module('shopping', []).
  factory('shoppingData',function($http, $q){
    return{
      apiPath:'/api/shoppingCart/',
      getAllItems: function(){
        //Creating a deferred object
        var deferred = $q.defer();

        //Calling Web API to fetch shopping cart items
        $http.get(this.apiPath).success(function(data){
          //Passing data to deferred's resolve function on successful completion
          deferred.resolve(data);
      }).error(function(){

        //Sending a friendly error message in case of failure
        deferred.reject("An error occured while fetching items");
      });

      //Returning the promise object
      return deferred.promise;
    }
  }
}

Following is the controller consuming the above custom service:
function ShoppingCartCtrl($scope, shoppingData) {
  $scope.items = [];

  function refreshItems(){
    shoppingData.getAllItems().then(function(data){
      $scope.items = data;
    },
    function(errorMessage){
      $scope.error=errorMessage;
    });
  };

  refreshItems();
};

As you see parameters of the controller, the second parameter shoppingData will be injected by the dependency injector during runtime. The function refreshItems follows the same convention as the snippet we discussed earlier. It does the following:
  • On successful completion of getAllItems() function, it assigns data to a property of $scope object which will be used to bind data
  • If the execution fails, it assigns the error message to another property of $scope object using which we can display the error message on the screen

The controller is free from any logic other than updating data that is displayed on the UI.

Functions to add a new item and remove an existing item also follow similar pattern. You can find the complete code in the following github repo: AngularShoppingCart

Happy coding!

24 comments:

  1. awsome post...really,found such a good post after a long time...gr8 work..

    ReplyDelete
  2. Good explanation of $q and defer, thank you.

    ReplyDelete
  3. In the 3rd example you are missing a curly brace that should be on line 23.

    ReplyDelete
  4. Hello Ravi:

    I came across your posts on angular deferred and promises. I have been struggling to get this done and I am very new to angular. I followed what was posted on your blog but that didn't help. I seem to be doing exactly what you have specified but it won't work. I am using service as opposed to factory in your example but for the sake of trial I even tried using a factory but that won't work either.

    And here is the question I posted on stackoverflow. Can you please help by pointing what I am doing wrong?

    http://stackoverflow.com/questions/18755201/angularjs-http-get-to-get-json-not-working-in-the-service-layer

    ReplyDelete
  5. Jeez, spent hours and hours trying to figure out how to use promises to do callbacks from service to controller and found heaps upon heaps of poorly explained crap until ran into this. You sir are awesome ! Here, have a virtual hug *bear hug* :D

    ReplyDelete
  6. Hi, good post, but i made all http methods is generic in factory function and i pass the url from controller if i calling same method with different url its not working and in controller two method are getting same response.
    like:--
    shoppingData.getAllItems('/api/shoppingCart/').then(function(data){
    $scope.items = data;
    },

    shoppingData.getAllItems('/api/somethingElse/').then(function(data){
    $scope.someThing = data;
    },

    ReplyDelete
    Replies
    1. Veeru,

      Can you post your factory and a dummy controller in a fiddle (jsfiddle.com) or plunk (plnkr.co) and share the link? It will be even better of you can ask the question on stackoverflow.com, as it will be public and will help a number of other developers as well.

      Delete
  7. Hi ravi good post
    if you call service xhr inside a service authentification and you want to access it
    in your controller how would you approach it?

    ReplyDelete
    Replies
    1. Gabster,

      This gist might help you: https://gist.github.com/robfarmergt/7510013#file-angularlogin. Let me know if you need more help.

      Delete
  8. Finally a tutorial with a realistic scenario. Thanks!

    ReplyDelete
  9. This comment has been removed by the author.

    ReplyDelete
  10. Doesn't $http.get return a promise anyway, in which case you could just return that and do away with $q altogether? Or have I missed something fundamental? :)

    ReplyDelete
    Replies
    1. James, Sorry for the slow response.
      I agree that the example in this post doesn't explain clearly why to go for the $q promise. You can use custom promise to have better control. For example, if you want to send the error received from the REST API to an endpoint for logging on the server, you can do it in the service; as controller id not supposed to do it. This is just one example, there are a number of such scenarios where custom promise is useful.

      Delete
  11. Yep, that is my question also! :) It shows how to use $q lib though.

    ReplyDelete
  12. You just save me the day, I didn't know anything about the deferred object. I was looking all day for this solution. Thx man!

    ReplyDelete
  13. Hello!
    i'm having this error:
    "Failed to load resource: net::ERR_CONNECTION_REFUSED"

    and my path is like this:

    apiPath: 'http://localhost/appCardapioWs/api/app_bebidaApi/'

    when I try this link on my browser, it works..

    What may I'm doing wrong?

    ReplyDelete
  14. Me too. Have you find a solution ?

    ReplyDelete
  15. It is a very good post. I found it very helpful. Thanks :)

    ReplyDelete

Note: only a member of this blog may post a comment.