Ren
Ren

Reputation: 1503

AngularJS Service - Make $resource return data right away instead of a promise so clients don't have to use .then()

This is a simple problem but seems tricky due to asynchronous nature of promises.

With in my data service, I want to ensure that data is retrieved back from the server before moving on to next step as other functions/clients depend on that data.

I don't want clients to register callbacks or use .then() and certainly not use $timeout when requesting the data.

How can I make sure that the controller and service that depend on my Data service get the data right away when requested? I've explained my problem using the code below. I would very much like to continue on my current approach if possible.

AngularJS Version: 1.4+

My Current Approach

//Data Service

App.factory('HealthyFoodService', function($resource, $q) {
      var fruitsPromise = $resource('api/food/fruits', {}).query().$promise;
      var veggiesPromise = $resource('api/food/veggies',{}).query().$promise;

      var fruitsData, veggiesData;

//Ensure $q populates the data before moving on to next statement.    

        $q.all([fruitsPromise, veggiesPromise]).then(function(data) {
          fruitsData = data[0];
          veggiesData = data[1];
        }

        function getCitrusFruits() {
          var citrusFruits;
          var allFrutis = fruitsData; 
//code breaks here because fruitsData is still undefined when called from a controller or another service.              
          //some logic for this function
          return citrusFruits;
        }

        function getLeafyVeggies() {
          var leafyVeggies;
          var allVeggies = veggiesData; 
//code breaks here because veggieData is still undefined when called from a controller or another service. 
          //some logic for this function
          return leafyVeggies;
        }

        function getLeafyVeggyByName(name) {
          //this function is called from other services and controllers.
          var leafyVeggies = getLeafyVeggies();
          return leafyVeggies[name];
        }

        return {
          getCitrusFruits: getCitrusFrutis,
          getLeafyVeggies: getLeafyVeggies,
          getLeafyVeggyByName: getLeafyVeggyByName
        });

Below are the two clients. One is a controller and another is a service. They both need the data right away as following statements depend on the returned data.

      //Controller
      App.controller('LeafyVeggieController', function(HealthyFoodService) {
        //Ideally I just'like to do something like below instead of calling `.then()` and registering callbacks.    

        var leafyVeggies = FoodService.getLeafyVeggies();
        //leafyVeggies is undefined because data is not available yet;
      });

      //Another service depending on HealthyFoodService- similar scenario 
      App.factory('LeafyVeggieReportService', function(HealthyFoodService) {

            function generateLeafyVeggieReport() {
              var name = 'spinach';
              var veggieInfo = HealthyFoodService.getLeafyVeggieByName(spinach);
              //veggieInfo is undefined 
              //logic that need data.
            });

My Previous Approach

Below is how I had it partially working before but I wasn't happy about using .then() everytime I needed the data.(Even with in the same service)

App.factory('HealthyFoodService', function($resource, $q) {
  //resource variables;

  function getLeafyVeggies() {
    return $q.all([veggiesPromise]).then(function(data) {
      //logic
      return leafyVeggies;
    });
  }

  function getLeafyVeggieByName() {
    var leafyVeggies = getLeafyVeggies().then(function(data) {
        return data;
      }
      //some logic
      //still causes issues when called from another service because above call doesn't get the data right away.
    }

    return {
      getLeafyVeggies: getLeafyVeggies,
      getLeafyVeggieByName: getLeafyVeggieByName
    }

    //controller
    App.controller('LeafyVeggieController', function(HealthyFoodService) {
      var leafyVeggies = HealthyFoodService.getLeafyVeggies().then(function(data) {
        return data;
      });
      //controller related logic
    });

Update

I'm using ui-router as well, so I'm aware that I can use resolve:{} in $stateProvider to inject the data directly into the controller. The puzzle is how to get the data when I make a request from another service or from another function with in the same service without having to use .then().


Solution

Using $q.all([]) in my client services that depend on my Data service has done the trick for me. I have used $q.all([]) whenever I'm in a situation where I need all the data to be present before start processing the logic.

I still have to use .then() on my clients, but by using $q.all([]), I can still slightly simulate a synchronous flow without breaking any asynchronous principles.

Upvotes: 0

Views: 405

Answers (2)

Patrick Gray
Patrick Gray

Reputation: 75

Try having the service hold your data and have the controller reference that data, instead of trying to pass it to your controller scope. Resolve the promise inside the service like so:

App.factory("HealthyFoodService", function($resource,$q) {
  var service = {};
  service.data = {
    fruitsData: [],
    veggiesData: []
  }

  $resource('api/food/fruits', {}).query().$promise.then(function(data) {
    $service.data.fruitsData = data[0];
    $service.data.veggiesData = data[1];
  })

  service.getCitrusFruits = function() {
    var citrusFruits;
    // Perform some logic on service.data.fruitsData
    return citrusFruits;
  }

  return service;
})

In you controller, you can talk to the service like so:

App.controller("FruitController", function($scope, HealthyFoodService) {
  // Now you can access the service data directly via $scope.hfs.fruitsData
  $scope.hfs = HealthyFoodService.data;
  // Or we can create a local copy of the data using the getCitrusFruits function
  $scope.citrusFruits = HealthyFoodService.getCitrusFruits();
})

Upvotes: 0

swestner
swestner

Reputation: 1917

I don't think this is possible. There is inherent latency in network operations that you need to wait for. Not doing so results in the application continuing before the data is available.

That being said, most of the native model binding operations will implicitly wait on promises, so there would be no need to .then if no further manipulation of the data is necessary before passing to the view. You can also use the transformResponse method of ngResource to help with this.

Another option might be to shift the complexity to the resolve methods in the route config. In that case you would handle the .then in the resolve and pass the resolved data to your controller. This will keep your controllers cleaner, but still requires you resolve the promises in the route config.

Upvotes: 0

Related Questions