AngularJS $http call in a Service, return resolved data, not promises

iQ. picture iQ. · Dec 1, 2014 · Viewed 20.8k times · Source

I want to know if it is possible to make a service call that uses $http so it returns data directly without returning a promise? I have tried to use the $q and defer without any luck.

Here is what I mean:

I have a service:

angular.module('myModule').factor('myService', ['$http', '$q',
    function($http, $q) {

        // Public API
        return {
            myServiceCall: function() {
                return $http.get('/server/call');
            }
        };
    }
]);

And this is how I would call it:

// My controller:
myService.myServiceCall().then(function(data) {
  $scope.data = data;
});

I want to avoid that and instead wish to have:

$scope.data = myService.myServiceCall();

I want it to be fully resolved at that line, is it possible?

I have tried some combinations of $q, defer and 'then' methods but can't seem to get it right since the method returns immediately.

Edit:

If you are wondering why, the main reason is that I wanted to simplify the controller code, this is easily done with ngResource as those calls are automatically resolved in templates, so I wanted to avoid the need to do the the whole '.then' everytime.

It's not that I don't like the Async nature, most of our code leverages it, it's just in some cases, having a synchronous way is helpful.

I think for now as some of you have pointed out, I will use the $resource for a near enough solution.

Answer

GregL picture GregL · Dec 2, 2014

There are two ways I can think of to do this. The first is definitely better than the second.

The first way: using the resolve property of routes

You can use the resolve property of the route configuration object to specify that a promise must be resolved and the resolved value be used for the value of one of your controller dependencies, so that as soon as your controller runs, the value is immediately available.

For example, say you have the following service:

app.service('things', ['$http', '$q', function ($http, $q) {
    var deferred = $q.defer();

    $http({
        method: 'GET',
        url: '/things',
        cache: true
    }).success(function (data) {
        deferred.resolve(data);
    }).error(function (msg) {
        deferred.reject(msg);
    });

    return deferred.promise;
}]);

Then you could define your route something like this:

$routeProvider.when('/things', {
    controller: ['$scope', 'thingsData', function ($scope, thingsData) {
        $scope.allThings = thingsData;
    }],
    resolve: {
        thingsData: ['things', function (things) {
            return things;
        }]
    }
});

The key in the resolve object corresponds to the name of the second dependency for the controller, so even though it returns a promise, that promise will be resolved before the controller is run.

The second way: Make the request well before you need the data

The second way, which is not at all ideal, and will likely bite you on the bum at some point, is to make the request for the data early on, such as when your app is initialised, and then actually get the data later on, say on a page that requires 3 clicks to get to from the home page. Fingers crossed, if the stars have aligned, your http request will have returned by then and you will have the data to use.

angular.module('myModule').factory('myService', ['$http', '$q', function($http, $q) {
    var data = [];

    function fetch() {
        $http.get('/server/call').then(function (d) { data = d; });
    }

    // Public API
    return {
        fetchData: fetch,
        myServiceCall: function () {
            return data;
        }
    };
}]);

Then, in your module's run function, make the request:

angular.module('myModule').run(['myService', function (myService) {
    myService.fetchData();
});

Then, in a controller for a page that is guaranteed to run at least 3 seconds after the app starts, you could do as you suggested:

 angular.module('myModule').controller('deepPageCtrl', ['$scope', 'myService', function ($scope, myService) {
      $scope.data = myService.myServiceCall();
      if (angular.isArray($scope.data) && $scope.data.length === 0) {
           panic("data isn't loaded yet");
      }
  }]);

Bonus, equally bad third way: Synchronous AJAX request

Do as ricick suggests, and use the synchronous AJAX request possible with jQuery's .ajax() method.