Ila
Ila

Reputation: 3538

How can I force this AngularJS service to wait until it has a value to return?

I have a service that is called by multiple controllers. It loads data into an object categories:

.service('DataService', ['$http', '$q', function($http, $q) {

    var categories = {};

    // Return public API.
    return({
        setCategory: setCategory,
        getCategory: getCategory,
        getJSON: getJSON,
        categories: categories
    });


    function setCategory(name, category) {
        console.log("setting category");
        console.log(name, category)
        categories[name] = category;
    }

    function getCategory(name) {
        console.log("getCategories:");
        console.log(categories[name]);
        return categories[name];
    }

    function getJSON() {
    //JSON stuff, where categories gets its initial values.
    }

I call getCategory(name) in many places, and in some instances, it is called before categories has populated, e.g:

 $scope.category = DataService.getCategory(name);
 //$scope.category is undefined

How can I build this Service so that getCategories waits until categories is defined before returning its value? Alternately, how can I write the Controller so that getCategories isn't defined until categories has a value? I have tried using a $scope.$watch function in the controller to watch DataService.categories, to no success- it never logs an updated value.

Upvotes: 2

Views: 2042

Answers (2)

Rohan Chandane
Rohan Chandane

Reputation: 367

Inside DataService

yourApp.service('DataService', function($resource, $q) {
var resource = $resource('/api/category/:id', {}, {
    query: {
        method: 'GET',
        isArray: false,
        cache: false
    },
    save: {
        method: 'POST',
        isArray: false,
        cache: false
    }
});
return {
    getCategory: function(id) {
        var deferred = $q.defer();
        resource.query({id: id},
            function(response) {
                deferred.resolve(response);
            },
            function(response) {
                deferred.reject(response);
            }
        );
        return deferred.promise;
    },
    setCategory: function(categoryObj) {
        var deferred = $q.defer();
        resource.save(categoryObj,
            function(response) {
                deferred.resolve(response);
            },
            function(response) {
                deferred.reject(response);
            }
        );
        return deferred.promise;
    },
    getJSON: function() {
        // stuff to do
    }
};
});

Inside DataController:

yourApp.controller('DataCtrl', function($scope, DataService) {

    $scope.handleSuccessResponse = function(response) {
        $scope.data = response;
    };

    $scope.handleErrorResponse = function(response) {
        $scope.error = response;
    };

    DataService.getCategory(123).then($scope.handleSuccessResponse, $scope.handleErrorResponse);
});

Upvotes: 0

Chad Robinson
Chad Robinson

Reputation: 4623

Use the promises you're already injecting in your service. Here is just one of the many possible ways you can do this:

var pendingQueue = [];
var loaded = false;
var self = this;
function getCategory(name) {
    var deferred = $q.defer();

    if (loaded) {
        // Resolve immediately
        console.log('Already loaded categories, resolving immediately...');
        deferred.resolve(self.categories[name]);
        return deferred.promise;
    }

    // Queue the request
    pendingQueue.push({
        promise: deferred.promise,
        name: name
    });

    if (pendingQueue.length === 1) {
        console.log('First request for a category, requesting...

        // We are the FIRST request. Call whatever it takes to load the data.
        // In a 'real' language this wouldn't be thread-safe, but we only have one, so...
        $http.get('/my-data').then(function(data) {
            self.categories = data;
            console.log('Loaded categories', self.categories);
            loaded = true;

            pendingQueue.map(function(entry) {
                entry.promise.resolve(entry.name);
            });

            pendingQueue.length = 0;
        });
    }

    return deferred.promise;
}

Then in your controller:

DataService.getCategory(name).then(function(category) {
    // Do something with category here
});

This will:

  • For the first request, make the async request and then resolve the promise once the data is obtained.

  • For the second - Nth request BEFORE the data is obtained, queue those without making duplicate requests.

  • For requests AFTER the data is obtained, resolve immediately with the requested data.

No error handling is done - you should use deferred.reject() to send those back to the caller, and .catch() / .finally() to handle them in the controller(s).

There are many solutions - this is just one option.

Upvotes: 1

Related Questions