Processing $http response in service
Asked Answered
D

12

233

I recently posted a detailed description of the issue I am facing here at SO. As I couldn't send an actual $http request, I used timeout to simulate asynchronous behavior. Data binding from my model to view is working correct, with the help of @Gloopy

Now, when I use $http instead of $timeout (tested locally), I could see the asynchronous request was successful and data is filled with json response in my service. But, my view is not updating.

updated Plunkr here

Dispensation answered 20/9, 2012 at 3:36 Comment(0)
M
417

Here is a Plunk that does what you want: http://plnkr.co/edit/TTlbSv?p=preview

The idea is that you work with promises directly and their "then" functions to manipulate and access the asynchronously returned responses.

app.factory('myService', function($http) {
  var myService = {
    async: function() {
      // $http returns a promise, which has a then function, which also returns a promise
      var promise = $http.get('test.json').then(function (response) {
        // The then function here is an opportunity to modify the response
        console.log(response);
        // The return value gets picked up by the then in the controller.
        return response.data;
      });
      // Return the promise to the controller
      return promise;
    }
  };
  return myService;
});

app.controller('MainCtrl', function( myService,$scope) {
  // Call the async method and then do stuff with what is returned inside our own then function
  myService.async().then(function(d) {
    $scope.data = d;
  });
});

Here is a slightly more complicated version that caches the request so you only make it first time (http://plnkr.co/edit/2yH1F4IMZlMS8QsV9rHv?p=preview):

app.factory('myService', function($http) {
  var promise;
  var myService = {
    async: function() {
      if ( !promise ) {
        // $http returns a promise, which has a then function, which also returns a promise
        promise = $http.get('test.json').then(function (response) {
          // The then function here is an opportunity to modify the response
          console.log(response);
          // The return value gets picked up by the then in the controller.
          return response.data;
        });
      }
      // Return the promise to the controller
      return promise;
    }
  };
  return myService;
});

app.controller('MainCtrl', function( myService,$scope) {
  $scope.clearData = function() {
    $scope.data = {};
  };
  $scope.getData = function() {
    // Call the async method and then do stuff with what is returned inside our own then function
    myService.async().then(function(d) {
      $scope.data = d;
    });
  };
});
Marinemarinelli answered 20/9, 2012 at 13:19 Comment(21)
to test the controller, do you want to inject a mock version of "myService" that has a mocked async() method that returns an object with a mocked then() method containing mock data, or is there a better way?Charlottcharlotta
@JoeHanink: It is not quite the same thing, since it is testing a directive not a controller, but you can see the sort of thing you can do here: github.com/angular-app/Samples/tree/master/1820EN_09_Code/…Marinemarinelli
Is there any way to still call the success and error methods in the controller after the service has intercepted with then?Murat
you can chain ´.then(success,error)` if your service do not intercept errorNorri
@PeteBD If I want to call my myService.async() multiple times from various controllers, how would you organise the service so that is only does the $http.get() for the first request, and all subsequent requests just return a local object array that gets set at the first call to myService.async(). In other words, I want to avoid multiple, needless requests to the JSON service, when really I only need to make one.Outbound
@Outbound - store the promise and return that each time. The promise can only be resolved once but once it has you can still attach then methods to it. (I think...)Marinemarinelli
@PeteBD Got a plunker or jsfiddle handy?Outbound
@Outbound - here you go: plnkr.co/edit/2yH1F4IMZlMS8QsV9rHv?p=preview. If you look at the console you'll see that the request is only made once.Marinemarinelli
@PeteBD Just checked, that's ridiculously easy & totally awesome! So is the response data stored with the promise, when it gets resolved? Where is the data coming from for the subsequent requests?Outbound
The value is stored in the deferred object, when it gets resolved.Marinemarinelli
@PeteBD Figured it must have been all right. Didn't know that about deferreds. Thanks again mate. I'll definitely be using this approach.Outbound
@PeteBD I think you can also use $scope.data = myService.async() directly in the controller.Thief
how would you use this approach using a app.service rather than a app.factory?Luminosity
@Thief - what you are suggesting is called automatic promise unwrapping. It is being deprecated in Angular 1.2 as it can lead to confusing inconsistencies. It is better to unwrap explicitly in your controller.Marinemarinelli
@Blowsie- I have updated the Plunks. Here is the original (updated to 1.2RC3): plnkr.co/edit/3Nwxxk?p=preview Here is one using service: plnkr.co/edit/a993Mn?p=previewMarinemarinelli
@PeteBD You should update this answer to include the example you've left in the comments in which you store the promise for future calls, as I missed that the first time and it's exactly what I was searching forMateriel
I got 404 file not found, solved it with this: linkSeries
@Norri - why 'if your service do not intercept error'? The approach seems to work even with an error method on $http.Cordova
I'm using this for retrieving config data into a config service, which will be used in many other modules and I want to avoid the promise unpacking boilerplate everywhere 'myConfigService.getConfig().then(function(config) { doSomething(config); });. Is there a neat way?Alanalana
Answering my own comment: deferred bootstrap isn't a bad solution, discussed for this very problem. Other solutions welcome!Alanalana
How to refresh this call for new dataGoles
G
82

Let it be simple. It's as simple as

  1. Return promise in your service(no need to use then in service)
  2. Use then in your controller

Demo. http://plnkr.co/edit/cbdG5p?p=preview

var app = angular.module('plunker', []);

app.factory('myService', function($http) {
  return {
    async: function() {
      return $http.get('test.json');  //1. this returns promise
    }
  };
});

app.controller('MainCtrl', function( myService,$scope) {
  myService.async().then(function(d) { //2. so you can use .then()
    $scope.data = d;
  });
});
Garek answered 20/12, 2013 at 21:34 Comment(4)
In your link, it's app.factory, and in your code it's app.service. It's supposed app.factory in this case.Convenient
app.service work too. Also - this to me looks like the most elegant solution. Am I missing something?Terza
Seems like every time I've got an Angular problem @Garek has the answer! (3rd time this week- great ng-map component btw)Pattani
i just want to know how to put success and error here with status_codeThermoelectrometer
S
58

Because it is asynchronous, the $scope is getting the data before the ajax call is complete.

You could use $q in your service to create promise and give it back to controller, and controller obtain the result within then() call against promise.

In your service,

app.factory('myService', function($http, $q) {
  var deffered = $q.defer();
  var data = [];  
  var myService = {};

  myService.async = function() {
    $http.get('test.json')
    .success(function (d) {
      data = d;
      console.log(d);
      deffered.resolve();
    });
    return deffered.promise;
  };
  myService.data = function() { return data; };

  return myService;
});

Then, in your controller:

app.controller('MainCtrl', function( myService,$scope) {
  myService.async().then(function() {
    $scope.data = myService.data();
  });
});
Simonsen answered 20/9, 2012 at 5:35 Comment(4)
+1 i like this one the best as it's more OO than the others. However is there any reason you don't do this this.async = function() { and this.getData = function() {return data} ? I hope you get what i meanStormi
@Stormi I wanted it the same way but it won't work because the promise has to be resolved all the way. If you don't and try to access it as you normally would, you'll get a reference error when accessing the internal data. Hope it makes sense?Campion
If I understand correctly it is necessary to add deffered = $q.defer() inside the myService.async if I want to call myService.async() two or more timeBeforetime
This example is a classic deferred anti-pattern. There is no need to manufacture a promise with $q.defer as the $http service already returns a promise. The promise returned will hang if the $http returns an error. In addition the .success and .error methods are deprecated and have been removed from AngularJS 1.6.Isoniazid
N
23

tosh shimayama have a solution but you can simplify a lot if you use the fact that $http returns promises and that promises can return a value:

app.factory('myService', function($http, $q) {
  myService.async = function() {
    return $http.get('test.json')
    .then(function (response) {
      var data = reponse.data;
      console.log(data);
      return data;
    });
  };

  return myService;
});

app.controller('MainCtrl', function( myService,$scope) {
  $scope.asyncData = myService.async();
  $scope.$watch('asyncData', function(asyncData) {
    if(angular.isDefined(asyncData)) {
      // Do something with the returned data, angular handle promises fine, you don't have to reassign the value to the scope if you just want to use it with angular directives
    }
  });

});

A little demonstration in coffeescript: http://plunker.no.de/edit/ksnErx?live=preview

Your plunker updated with my method: http://plnkr.co/edit/mwSZGK?p=preview

Norri answered 20/9, 2012 at 8:43 Comment(4)
I'll try further along your approach. But, I like to capture the result in service instead of returning. See the question related to this here #12505247 . I like to process the data returned by $http in different ways in controller. thanks again for your help.Dispensation
you can use promises in services, if you don't like $watch you can do ´promise.then(function(data){ service.data = data; }, onErrorCallback);`Norri
I added a plunker forked from yoursNorri
alternatively you can use $scope.$emit from the service and $scope.$on on the ctrl to tell you controller that the data has returned but I don't really see a benefitNorri
C
7

A much better way I think would be something like this:

Service:

app.service('FruitsManager',function($q){

    function getAllFruits(){
        var deferred = $q.defer();

        ...

        // somewhere here use: deferred.resolve(awesomeFruits);

        ...

        return deferred.promise;
    }

    return{
        getAllFruits:getAllFruits
    }

});

And in the controller you can simply use:

$scope.fruits = FruitsManager.getAllFruits();

Angular will automatically put the resolved awesomeFruits into the $scope.fruits.

Clive answered 3/11, 2013 at 15:11 Comment(1)
deferred.resolve()? Be more precise please and where is the $http call? Also why do you return an object in a .service?Piscatory
I
6

I had the same problem, but when I was surfing on the internet I understood that $http return back by default a promise, then I could use it with "then" after return the "data". look at the code:

 app.service('myService', function($http) {
       this.getData = function(){
         var myResponseData = $http.get('test.json').then(function (response) {
            console.log(response);.
            return response.data;
          });
         return myResponseData;

       }
});    
 app.controller('MainCtrl', function( myService, $scope) {
      // Call the getData and set the response "data" in your scope.  
      myService.getData.then(function(myReponseData) {
        $scope.data = myReponseData;
      });
 });
Ixia answered 19/1, 2017 at 23:20 Comment(0)
B
4

When binding the UI to your array you'll want to make sure you update that same array directly by setting the length to 0 and pushing the data into the array.

Instead of this (which set a different array reference to data which your UI won't know about):

 myService.async = function() {
    $http.get('test.json')
    .success(function (d) {
      data = d;
    });
  };

try this:

 myService.async = function() {
    $http.get('test.json')
    .success(function (d) {
      data.length = 0;
      for(var i = 0; i < d.length; i++){
        data.push(d[i]);
      }
    });
  };

Here is a fiddle that shows the difference between setting a new array vs emptying and adding to an existing one. I couldn't get your plnkr working but hopefully this works for you!

Brufsky answered 20/9, 2012 at 4:47 Comment(3)
that didn't work. in console log, I could see d is updated properly in success callback, but not data. May be the function is already executed.Dispensation
This method should definitely work maybe it has something to do with the data type of d not being an array (in asp.net you'd need to access d.d for the array for example). See this plnkr for an example pushing a string into the array on error: plnkr.co/edit/7FuwlN?p=previewBrufsky
angular.copy(d, data) will also work. When a destination is supplied to the copy() method, it will first delete the destination's elements, and then copy in the new ones from the source.Jessee
P
4

Related to this I went through a similar problem, but not with get or post made by Angular but with an extension made by a 3rd party (in my case Chrome Extension).
The problem that I faced is that the Chrome Extension won't return then() so I was unable to do it the way in the solution above but the result is still Asynchronous.
So my solution is to create a service and to proceed to a callback

app.service('cookieInfoService', function() {
    this.getInfo = function(callback) {
        var model = {};
        chrome.cookies.get({url:serverUrl, name:'userId'}, function (response) {
            model.response= response;
            callback(model);
        });
    };
});

Then in my controller

app.controller("MyCtrl", function ($scope, cookieInfoService) {
    cookieInfoService.getInfo(function (info) {
        console.log(info);
    });
});

Hope this can help others getting the same issue.

Positively answered 23/6, 2013 at 11:52 Comment(0)
Q
4

I've read http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/ [AngularJS allows us to streamline our controller logic by placing a promise directly on the scope, rather than manually handing the resolved value in a success callback.]

so simply and handy :)

var app = angular.module('myApp', []);
            app.factory('Data', function($http,$q) {
                return {
                    getData : function(){
                        var deferred = $q.defer();
                        var promise = $http.get('./largeLoad').success(function (response) {
                            deferred.resolve(response);
                        });
                        // Return the promise to the controller
                        return deferred.promise; 
                    }
                }
            });
            app.controller('FetchCtrl',function($scope,Data){
                $scope.items = Data.getData();
            });

Hope this help

Quern answered 13/9, 2013 at 16:22 Comment(4)
doesn't work. the return value of defrred.promise is not a function.Dumont
@PineappleUndertheSea why does it need to be a function? It's a promise object.Fatshan
@PineappleUndertheSea did you mean to use deferred and not defrred ?Waggon
As PeteBD pointed out, this form $scope.items = Data.getData(); is deprecated in AnglularAlanalana
A
2

I really don't like the fact that, because of the "promise" way of doing things, the consumer of the service that uses $http has to "know" about how to unpack the response.

I just want to call something and get the data out, similar to the old $scope.items = Data.getData(); way, which is now deprecated.

I tried for a while and didn't come up with a perfect solution, but here's my best shot (Plunker). It may be useful to someone.

app.factory('myService', function($http) {
  var _data;  // cache data rather than promise
  var myService = {};

  myService.getData = function(obj) { 
    if(!_data) {
      $http.get('test.json').then(function(result){
        _data = result.data;
        console.log(_data);  // prove that it executes once
        angular.extend(obj, _data);
      }); 
    } else {  
      angular.extend(obj, _data);
    }
  };

  return myService;
}); 

Then controller:

app.controller('MainCtrl', function( myService,$scope) {
  $scope.clearData = function() {
    $scope.data = Object.create(null);
  };
  $scope.getData = function() {
    $scope.clearData();  // also important: need to prepare input to getData as an object
    myService.getData($scope.data); // **important bit** pass in object you want to augment
  };
});

Flaws I can already spot are

  • You have to pass in the object which you want the data added to, which isn't an intuitive or common pattern in Angular
  • getData can only accept the obj parameter in the form of an object (although it could also accept an array), which won't be a problem for many applications, but it's a sore limitation
  • You have to prepare the input object $scope.data with = {} to make it an object (essentially what $scope.clearData() does above), or = [] for an array, or it won't work (we're already having to assume something about what data is coming). I tried to do this preparation step IN getData, but no luck.

Nevertheless, it provides a pattern which removes controller "promise unwrap" boilerplate, and might be useful in cases when you want to use certain data obtained from $http in more than one place while keeping it DRY.

Alanalana answered 24/9, 2014 at 17:43 Comment(0)
S
1

As far as caching the response in service is concerned , here's another version that seems more straight forward than what I've seen so far:

App.factory('dataStorage', function($http) {
     var dataStorage;//storage for cache

     return (function() {
         // if dataStorage exists returned cached version
        return dataStorage = dataStorage || $http({
      url: 'your.json',
      method: 'GET',
      cache: true
    }).then(function (response) {

              console.log('if storage don\'t exist : ' + response);

              return response;
            });

    })();

});

this service will return either the cached data or $http.get;

 dataStorage.then(function(data) {
     $scope.data = data;
 },function(e){
    console.log('err: ' + e);
 });
Snack answered 28/9, 2015 at 17:23 Comment(0)
P
0

Please try the below Code

You can split the controller (PageCtrl) and service (dataService)

'use strict';
(function () {
    angular.module('myApp')
        .controller('pageContl', ['$scope', 'dataService', PageContl])
        .service('dataService', ['$q', '$http', DataService]);
    function DataService($q, $http){
        this.$q = $q;
        this.$http = $http;
        //... blob blob 
    }
    DataService.prototype = {
        getSearchData: function () {
            var deferred = this.$q.defer(); //initiating promise
            this.$http({
                method: 'POST',//GET
                url: 'test.json',
                headers: { 'Content-Type': 'application/json' }
            }).then(function(result) {
                deferred.resolve(result.data);
            },function (error) {
                deferred.reject(error);
            });
            return deferred.promise;
        },
        getABCDATA: function () {

        }
    };
    function PageContl($scope, dataService) {
        this.$scope = $scope;
        this.dataService = dataService; //injecting service Dependency in ctrl
        this.pageData = {}; //or [];
    }
    PageContl.prototype = {
         searchData: function () {
             var self = this; //we can't access 'this' of parent fn from callback or inner function, that's why assigning in temp variable
             this.dataService.getSearchData().then(function (data) {
                 self.searchData = data;
             });
         }
    }
}());
Picard answered 23/11, 2016 at 1:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.