How to make controller wait for promise to resolve from angular service

34,402

Solution 1

If all you want is to handle the response of $http in the service itself, you can add a then function to the service where you do more processing then return from that then function, like this:

function GetCompaniesService(options) {
  this.url = '/company';
  this.Companies = undefined;
  this.CompaniesPromise = $http.get(this.url).then(function(response) {
    /* handle response then */
    return response
  })
}

But you'll still have use a promise in the controller, but what you get back will have already been handled in the service.

var CompaniesOb = new GetCompanies();
CompaniesOb.CompaniesPromise.then(function(dataAlreadyHandledInService) {
  $scope.Companies = dataAlreadyHandledInService;
});

Solution 2

There is no problem to achieve that!

The main thing you have to keep in mind is that you have to keep the same object reference (and in javascript arrays are objects) in your service.

here is our simple HTML:

<div ng-controller = "companiesCtrl"> 
  <ul ng-repeat="company in companies">
     <li>{{company}}</li>
  </ul>
</div>

Here is our service implementation:

serviceDataCaching.service('companiesSrv', ['$timeout', function($timeout){
  var self = this;

  var httpResult = [
    'company 1',
    'company 2',
    'company 3'
  ];

  this.companies = ['preloaded company'];
  this.getCompanies = function() {
    // we simulate an async operation
    return $timeout(function(){
      // keep the array object reference!!
      self.companies.splice(0, self.companies.length);

      // if you use the following code:
      // self.companies = [];
      // the controller will loose the reference to the array object as we are creating an new one
      // as a result it will no longer get the changes made here!
      for(var i=0; i< httpResult.length; i++){
        self.companies.push(httpResult[i]);
      }
      return self.companies;
    }, 3000);                    
}}]);   

And finally the controller as you wanted it:

serviceDataCaching.controller('companiesCtrl', function ($scope, companiesSrv) {
  $scope.companies = companiesSrv.companies;
  companiesSrv.getCompanies();
});

Explanations

As said above, the trick is to keep the reference between the service and the controller. Once you respect this, you can totally bind your controller scope on a public property of your service.

Here a fiddle that wraps it up.

In the comments of the code you can try uncomment the piece that does not work and you will see how the controller is loosing the reference. In fact the controller will keep having a reference to the old array while the service will change the new one.

One last important thing: keep in mind that the $timeout is triggering a $apply() on the rootSCope. This is why our controller scope is refreshing 'alone'. Without it, and if you try to replace it with a normal setTimeout() you will see that the controller is not updating the company list. To work around this you can:

  • don't do anything if your data is fetched with $http as it calls a $apply on success
  • wrap you result in a $timeout(..., 0);
  • inject $rootSCope in the service and call $apply() on it when the asynchronous operation is done
  • in the controller add a $scope.$apply() on the getCompanies() promise success

Hope this helps!

Share:
34,402
Axschech
Author by

Axschech

Updated on July 24, 2022

Comments

  • Axschech
    Axschech almost 2 years

    I have a service that is making an AJAX request to the backend

    Service:

        function GetCompaniesService(options)
        {
            this.url = '/company';
            this.Companies = undefined;
            this.CompaniesPromise = $http.get(this.url);
    
        }
    

    Controller:

    var CompaniesOb = new GetCompanies();
    CompaniesOb.CompaniesPromise.then(function(data){
       $scope.Companies = data;
    });
    

    I want my service to handle the ".then" function instead of having to handle it in my controller, and I want to be able to have my controller act on that data FROM the service, after the promise inside the service has been resolved.

    Basically, I want to be able to access the data like so:

    var CompaniesOb = new GetCompanies();
    $scope.Companies = CompaniesOb.Companies;
    

    With the resolution of the promise being handled inside of the service itself.

    Is this possible? Or is the only way that I can access that promise's resolution is from outside the service?

  • ABOS
    ABOS over 9 years
    Even this is possible, it is considered bad practice. I cannot figure out why Axschech does not like original approach
  • Axschech
    Axschech over 9 years
    @ABOS the idea is to keep the data (and it's state) in the service, not in the controller. The original example, and this one both do the opposite. Assigning the data to the scope of the controller puts the state in the controller's hands and no the service.
  • Axschech
    Axschech over 9 years
    Sorry, I don't see how this is any different from my original post, it's just a bit of different syntax.
  • ABOS
    ABOS over 9 years
    I see your point now. But what is the purpose of keeping data in service, for sharing data? I would not do so even this is the case. I would rather store data on controller and if I do want to share data/ put data on service, I just call service's setSharedData(sharedData) method. This makes service code more testable and reusable
  • M.K. Safi
    M.K. Safi over 9 years
    No, it's not just a different syntax. The then function in the service will get executed before the then function in the controller, giving you a chance to process the response before the controller gets access to it.
  • Axschech
    Axschech over 9 years
    @ABOS I do understand that principle, but the idea is to maintain the state of the data (it is going to be changed client side after being pulled from the server). I just feel like it's easier to keep referencing the service's version, than using a controller object. Otherwise the service is just a shell for the $http.get()
  • Axschech
    Axschech over 9 years
    Right but unfortunately I still have to access it via the controller inside a promise. That's what I'm trying to avoid. It's kind of half of the solution.
  • M.K. Safi
    M.K. Safi over 9 years
    Yes, you'll have to access it via the promise syntax in the controller. Angular templates used to automatically unwrap promises where you would've been able to do the syntax that you said you prefer. But they've deprecated then removed that feature entirely. Now you have to unwrap the promise yourself as in my example.
  • Axschech
    Axschech over 9 years
    I'll leave this open to see if anyone has any other ideas. If not I guess this is the answer!
  • changtung
    changtung about 9 years
    it is still pending promise. How can i return from function with promise, when then is done in that function, beeing 100% sure that my return variable contains http data.
  • georgeawg
    georgeawg about 7 years
  • georgeawg
    georgeawg about 7 years
  • georgeawg
    georgeawg about 7 years
    The answer uses a technique that is needlessly complicated, error prone and considered an anti-pattern. There is no need to construct a promise with $q.defer as both the $timeout and $http services already return promises.
  • Piou
    Piou about 7 years
    indeed, I will edit the answer then. Though the goal here was not to show the promise pattern but exposing a collection getter and hold the reference in the service in order to have the component using this collection "change aware".