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

Community
  • 1
  • 1
bsr
  • 57,282
  • 86
  • 216
  • 316

12 Answers12

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;
    });
  };
});
Pete BD
  • 10,151
  • 3
  • 31
  • 30
  • 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? – Joe Hanink Dec 11 '12 at 04:25
  • @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: https://github.com/angular-app/Samples/tree/master/1820EN_09_Code/06_unique-email_directive – Pete BD Dec 12 '12 at 20:05
  • 13
    Is there any way to still call the success and error methods in the controller after the service has intercepted with `then`? – andyczerwonka Jan 10 '13 at 02:31
  • 1
    you can chain ´.then(success,error)` if your service do not intercept error – Guillaume86 Apr 27 '13 at 11:31
  • 2
    @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. – GFoley83 May 16 '13 at 22:05
  • 1
    @GFoley83 - 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...) – Pete BD May 18 '13 at 06:33
  • @PeteBD Got a plunker or jsfiddle handy? – GFoley83 May 19 '13 at 21:14
  • 5
    @GFoley83 - here you go: http://plnkr.co/edit/2yH1F4IMZlMS8QsV9rHv?p=preview. If you look at the console you'll see that the request is only made once. – Pete BD May 21 '13 at 07:03
  • @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? – GFoley83 May 21 '13 at 07:37
  • 1
    The value is stored in the deferred object, when it gets resolved. – Pete BD May 22 '13 at 09:42
  • @PeteBD Figured it must have been all right. Didn't know that about deferreds. Thanks again mate. I'll definitely be using this approach. – GFoley83 May 28 '13 at 11:19
  • 3
    @PeteBD I think you can also use `$scope.data = myService.async()` directly in the controller. – Julian Aug 28 '13 at 21:08
  • how would you use this approach using a app.service rather than a app.factory? – Blowsie Oct 15 '13 at 09:18
  • 1
    @Julian - 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. – Pete BD Oct 16 '13 at 09:43
  • 2
    @Blowsie- I have updated the Plunks. Here is the original (updated to 1.2RC3): http://plnkr.co/edit/3Nwxxk?p=preview Here is one using service: http://plnkr.co/edit/a993Mn?p=preview – Pete BD Oct 16 '13 at 09:50
  • @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 for – JDandChips Dec 05 '13 at 16:54
  • I got 404 file not found, solved it with this: [link](http://stackoverflow.com/questions/17626776/why-is-my-json-file-not-found/17631046#17631046) – Red Jul 22 '14 at 05:35
  • @Guillaume86 - why 'if your service do not intercept error'? The approach seems to work even with an error method on $http. – stephent Sep 09 '14 at 04:45
  • 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? – poshest Sep 19 '14 at 12:20
  • Answering my own comment: [deferred bootstrap](https://github.com/philippd/angular-deferred-bootstrap) isn't a bad solution, [discussed for this very problem](http://stackoverflow.com/questions/18018603/injecting-a-resolved-promise-into-service?rq=1). Other solutions welcome! – poshest Sep 25 '14 at 10:12
  • How to refresh this call for new data – kTn Mar 04 '16 at 12:37
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;
  });
});
allenhwkim
  • 27,270
  • 18
  • 89
  • 122
  • In your link, it's `app.factory`, and in your code it's `app.service`. It's supposed `app.factory` in this case. – Re Captcha Jul 18 '14 at 10:58
  • 1
    app.service work too. Also - this to me looks like the most elegant solution. Am I missing something? – user1679130 Aug 20 '14 at 20:29
  • 1
    Seems like every time I've got an Angular problem @allenhwkim has the answer! (3rd time this week- great ng-map component btw) – Yarin Apr 28 '16 at 19:35
  • i just want to know how to put success and error here with status_code – Anuj Dec 23 '16 at 12:47
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();
  });
});
Tosh
  • 35,955
  • 11
  • 65
  • 55
  • 2
    +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 mean – bicycle Oct 21 '13 at 08:55
  • @bicycle 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? – user6123723 Oct 25 '13 at 20:16
  • 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 time – ceth Oct 31 '15 at 13:37
  • 1
    This example is a classic [deferred anti-pattern](http://stackoverflow.com/questions/23803743/what-is-the-explicit-promise-construction-antipattern-and-how-do-i-avoid-it). 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](https://github.com/angular/angular.js/pull/15157). – georgeawg Nov 02 '16 at 16:27
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

Guillaume86
  • 14,341
  • 4
  • 53
  • 53
  • 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 http://stackoverflow.com/questions/12504747/angularjs-processing-asynchronous-data-in-service . I like to process the data returned by $http in different ways in controller. thanks again for your help. – bsr Sep 20 '12 at 11:50
  • you can use promises in services, if you don't like $watch you can do ´promise.then(function(data){ service.data = data; }, onErrorCallback);` – Guillaume86 Sep 20 '12 at 11:53
  • I added a plunker forked from yours – Guillaume86 Sep 20 '12 at 12:02
  • 1
    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 benefit – Guillaume86 Sep 20 '12 at 12:10
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.

HasanAboShally
  • 18,459
  • 7
  • 30
  • 34
  • 4
    deferred.resolve()? Be more precise please and where is the $http call? Also why do you return an object in a .service? –  Apr 02 '14 at 14:40
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;
      });
 });
JhonQO
  • 368
  • 3
  • 7
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!

Gloopy
  • 37,767
  • 15
  • 103
  • 71
  • 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. – bsr Sep 20 '12 at 11:45
  • 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: http://plnkr.co/edit/7FuwlN?p=preview – Gloopy Sep 20 '12 at 16:37
  • 1
    `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. – Mark Rajcok Feb 08 '13 at 16:38
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.

Shadoweb
  • 5,812
  • 1
  • 42
  • 55
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

Whisher
  • 31,320
  • 32
  • 120
  • 201
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.

poshest
  • 4,157
  • 2
  • 26
  • 37
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);
 });
maioman
  • 18,154
  • 4
  • 36
  • 42
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;
             });
         }
    }
}());
Ratheesh
  • 631
  • 8
  • 8