0

I want two different controllers to run different functions after some promises are resolved in a service (i dont want this service to make an http request each time a controller needs the data, I only want one http request).

I have a service that makes a request and gets a promise. I want controller1 to see this resolution and then run some code. I then want controller2 to also see that this promise resolves and run some code (basically multiple then() methods that run on the same promise but from different files). How can I go about doing this?

All the examples I have seen have one controller running code after a certain promise resolves, but not multiple controllers listening for the same promise to resolve.

here is some code im borrowing from this article (ill add a 'mother controller' to illustrate my example, I dont want the son service to ever make his http call twice): http://andyshora.com/promises-angularjs-explained-as-cartoon.html

son service

app.factory('SonService', function ($http, $q) {
    return {
        getWeather: function() {
            // the $http API is based on the deferred/promise APIs exposed by the $q service
            // so it returns a promise for us by default
            return $http.get('http://fishing-weather-api.com/sunday/afternoon')
                .then(function(response) {
                    if (typeof response.data === 'object') {
                        return response.data;
                    } else {
                        // invalid response
                        return $q.reject(response.data);
                    }

                }, function(response) {
                    // something went wrong
                    return $q.reject(response.data);
                });
        }
    };
});

father Controller:

 // function somewhere in father-controller.js
        var makePromiseWithSon = function() {
            // This service's function returns a promise, but we'll deal with that shortly
            SonService.getWeather()
                // then() called when son gets back
                .then(function(data) {
                    // promise fulfilled
                    if (data.forecast==='good') {
                        prepareFishingTrip();
                    } else {
                        prepareSundayRoastDinner();
                    }
                }, function(error) {
                    // promise rejected, could log the error with: console.log('error', error);
                    prepareSundayRoastDinner();
                });
        };

Mother Controller:

var makePromiseWithSon = function() {
            SonService.getWeather()
                // then() called when son gets back
                .then(function(data) {
                    // promise fulfilled
                    if (data.forecast==='good') {
                        workInTheGarden();
                    } else {
                        sweepTheHouse();
                    }
                }, function(error) {
                    // promise rejected, could log the error with: console.log('error', error);
                    sweepTheHouse();
                });
        };
Goku
  • 1,565
  • 1
  • 29
  • 62
  • 1
    As long as it's in the same context, and you can make a reference to the promise that is available within both controllers, it shouldn't be much of an issue, however the question seems way to vague to be answerable ? – adeneo Dec 02 '15 at 21:14
  • Yes, just create multiple controllers that use the same promise from a service. It will work out of the box. – Bergi Dec 02 '15 at 21:18
  • @Bergi, can you look at my edits? Are you saying i can make the mother controller and the sun service wont make an extra http call for it? basically one http request and every controller can use it when its resolved? reason I care is because the request is doing a large query. – Goku Dec 02 '15 at 21:25
  • 2
    No, they won't - the (shared) service doesn't return the same promise every time here, a new promise (and request) is created whenever you call `getWeather`. [You can easily cache it](http://stackoverflow.com/a/18745499/1048572) though. – Bergi Dec 02 '15 at 21:28
  • What about just using "cache: true" on the http-request. Wouldn't that solve the problem of multiple requests without having to fiddle with exposing the promise to multiple controllers? – Etse Dec 02 '15 at 21:36
  • @Bergi lets say fatherController calls sonService.getWeather() and immediately after (before the promise has time to resolve) motherController calls sonService.getWeather(). will there be two separate http calls, or will the first resolution be used for both controllers? – Goku Dec 04 '15 at 14:42
  • @adamkim: If you cache the promise (at the time of the call) there will be only one promise and one request, regardless of the time of the resolution. If you don't cache the promise, there will be as many promises and requests as there are calls. – Bergi Dec 04 '15 at 14:46

2 Answers2

1

To have your factory service only get the url once, store the httpPromise in your factory service.

app.factory('SonService', function ($http) {
    var weatherPromise;
    function getWeather() {
      return $http.get('http://fishing-weather-api.com/sunday/afternoon')
                .then(function(response) {
                    if (typeof response.data === 'object') {
                        return response.data;
                    } else {
                        // invalid response
                        throw response;
                    }

                }, function(response) {
                    // something went wrong
                    throw response;
                });
    }
    function sonService() {
      if (!weatherPromise) {
        //save the httpPromise
        weatherPromise = getWeather();
      }
      return weatherPromise;
    }
    return sonService;
});
georgeawg
  • 48,608
  • 13
  • 72
  • 95
1

The simple answer, in a non-angular-specific (but really easy to apply to Angular) way, is to create a service which caches ON-OUTBOUND-REQUEST (rather than caching return values, like most systems would).

function SearchService (fetch) {
  var cache = { };
  return {
    getSpecificThing: function (uri) {
      var cachedSearch = cache[uri];
      if (!cachedSearch) {
        cachedSearch = fetch(uri).then(prepareData);
        cache[uri] = cachedSearch;
      }
      return cachedSearch;
    }
  };
}


function A (searchService) {
   var a = this;
   Object.assign(a, {
     load: function ( ) {
       searchService.getSpecificThing("/abc").then(a.init.bind(a));
     },
     init: function (data) { /* ... */ }
   });
}

function B (searchService) {
  var b = this;
  Object.assign(b, {
    load: function ( ) {
      searchService.getSpecificThing("/abc").then(b.init.bind(b));
    },
    init: function (data) { /* ... */ }
  });
}


var searchService = SearchService(fetch);
var a = new A(searchService);
var b = new B(searchService);

a.load().then(/* is initialized */);
b.load().then(/* is initialized */);

They're sharing the same promise, because the service they were talking to cached and returned the same promise.

If you wanted to be safe, you could cache a promise and then return new instances of promises which resolve (or reject) based on the cached promise.

// instead of
return cachedSearch;

// replace it with
return Promise.resolve(cachedSearch);

Each user is now getting a new instance, every time you make a request, but each instance is also passing or failing based on the original cached call.
And of course you can take it further, and put a time-limit on the cache, or have hooks to invalidate the cache, or whatever...

Converting this to Angular is also a snap

  • SearchService is a service
  • A and B are controllers
  • use $http instead of fetch (though fetch is really pretty)
  • in fetch( ).then(prepareData) you'd be converting data from JSON on success;
    in $http, you'd be returning response.data because your users don't want to have to do that
    either way, you're performing that operation exactly once, per outbound call, so cache it, too
  • use $q (and q methods) instead of native Promise
  • use angular.extend, instead of Object.assign
  • You're done; you've now ported that whole concept into Angular AND VanillaJS
Norguard
  • 26,167
  • 5
  • 41
  • 49