3

I have the following service:

app.service('Library', ['$http', function($http) {

    this.fonts = [];
    this.families = [];

    // ... some common CRUD functions here ...

    // Returns the font list
    this.getFonts = function() {
        if(_.isEmpty(this.fonts)) this.updateFonts();
        return this.fonts;
    };

    // Returns the family list
    this.getFamilies = function() {
        if(_.isEmpty(this.families)) this.updateFamilies();
        return this.families;
    };

    // Update the font list
    this.updateFonts = function() {
        var self = this;
        $http.get(BACKEND_URL+'/fonts').success(function(data) {
            self.fonts = data;
            console.log('Library:: fonts updated', self.fonts);
        });
    };

    // Update the family
    this.updateFamilies = function() {
        var self = this;
        $http.get(BACKEND_URL+'/families').success(function(data) {
            var sorted = _.sortBy(data, function(item) { return item });
            self.families = sorted;
            console.log('Library:: families updated', self.families);
        });
    };
}]);

And the following main controller code:

app.controller('MainController', ['$scope', '$state', 'Cart', 'Library', function($scope, $state, Cart, Library) {
    console.log('-> MainController');

    // Serve the right font list depending on the page
    $scope.fonts = $state.is('home.cart') ? Cart.getFonts() : Library.getFonts();
    $scope.families = Library.getFamilies();

}]);

The problem is, that when the view requests the content of $scope.fonts, it's still empty.

How to update $scope.fonts and $scope.families when the loading is over?

I could use $scope.$watch but I'm sure there is a cleaner way to do it...

7 Answers 7

2

This really is what promises were made for. Your service should return a promise that is to be resolved. You could also simplify your service:

app.service('Library', ['$http', '$q', function($http, $q) {
    var self = this;

    self.families = [];

    // Returns the family list
    self.getFamilies = function() {
        var deferred = $q.defer();

        if(_.isEmpty(self.families)) {
            $http.get(BACKEND_URL+'/families').success(function(data) {
                var sorted = _.sortBy(data, function(item) { return item });
                self.families = sorted;
                deferred.resolve(self.families);
                console.log('Library:: families updated', self.families);
            });
        } else {
            deferred.resolve(self.families);
        }

        return deferred.promise;
    };
}]);

And then in your controller, use the promises then method:

app.controller('MainController', ['$scope', '$state', 'Cart', 'Library', function($scope, $state, Cart, Library) {
    console.log('-> MainController');

    // Serve the right font list depending on the page
    $scope.fonts = $state.is('home.cart') ? Cart.getFonts() : Library.getFonts();
    Library.getFamilies().then(function(result) {
        $scope.families = result;
    });
}]);

This is untested because of the $http, but here is a demo using $timeout:

JSFiddle

Sign up to request clarification or add additional context in comments.

2 Comments

$http is a promise implementation already, there is no need to wrap it inside another one.
This is true, and a good disclaimer to have. The OP is storing and returning the result for future calls. An argument could be made for caching, but the objective is to show the usefulness of promises. This could certainly be optimized using return $q.when(self.families); but the solution above better demonstrates how to use promises.
1

Consider passing a callback function.

Service:

this.getFonts = function(callback) {
    if(_.isEmpty(this.fonts)) this.updateFonts(callback);
    return this.fonts;
};

this.updateFonts = function(callback) {
    var self = this;
    $http.get(BACKEND_URL+'/fonts').success(function(data) {
        self.fonts = data;
        console.log('Library:: fonts updated', self.fonts);
        callback(data);
    });
};

Controller:

Library.getFonts(function (data) { $scope.fonts = data; });

This could be tidied up a bit, since a callback eliminates the need for some of this code, but it'll serve as an example.

2 Comments

That's a way to do it, but won't the script be stuck if the request times out?
Nope, as far as the execution goes it stops at the HTTP call and returns to whatever called it. Then the callback function is triggered if the data comes back - if it doesn't then no harm done.
1

Thanks for all the answers! I ended up using a mix of callback and promise, as follow:

app.service('Library', function($http) {

    // Returns the font list
    this.getFonts = function(callback) {
        if(_.isEmpty(self.fonts)) return self.updateFonts(callback);
        else return callback(self.fonts);
    };

    // Update the font list
    this.updateFonts = function(callback) {
        return $http.get(BACKEND_URL+'/fonts').success(function(data) {
            self.fonts = data;
            callback(data);
        });
    };
});

And, in the controller:

app.controller('MainController', function(Library) {
    Library.getFonts(function(fonts) { $scope.fonts = fonts });
});

I tried all your suggestions, but this is the best one working with the rest of my code.

Comments

0

In your this.getFonts function (and your other functions), you call the data from this, which points to the function instead of the controller scope you want. Try the following instead:

var self = this;

self.fonts = [];
self.families = [];

// ... some common CRUD functions here ...

// Returns the font list
self.getFonts = function() {
    if(_.isEmpty(self.fonts)) self.updateFonts();
    return self.fonts; // <-- self.fonts will point to the fonts you want
};

Comments

0

I would try wrapping your getScope and getFonts bodies that you are calling in a

$scope.$apply(function(){ ...body here... });

Comments

0

Make sure you declare self = this outside any functions.

Assign the call to the value you want to store the data in and then return it.

var self = this;
self.data = [];

this.updateFonts = function() {

    self.fonts = $http.get(BACKEND_URL+'/fonts').success(function(data) {
        return data.data
    });
    return self.fonts
};

Comments

0

Since you're using ui-router (i saw a $state). You can use a resolve in your state and return a promise.

Doc : https://github.com/angular-ui/ui-router/wiki

Exemple :

$stateProvider.state('myState', {
      resolve:{
         // Example using function with returned promise.
         // This is the typical use case of resolve.
         // You need to inject any services that you are
         // using, e.g. $http in this example
         promiseObj:  function($http){
            // $http returns a promise for the url data
            return $http({method: 'GET', url: '/someUrl'});
         }
      },   
       controller: function($scope,promiseObj){
          // You can be sure that promiseObj is ready to use!
          $scope.items = promiseObj.data;
      }
}

In your case you'll need to turn your this.getFonts and getFamilies into promises

this.getFonts = function(){
  return $http.get(BACKEND_URL+'/fonts').success(function(data) {
            self.fonts = data;
            console.log('Library:: fonts updated', self.fonts);
        });
}

There is many many way to do this, but in my opinion the resolve way is the best.

1 Comment

Thanks. It works well but doesn't fit well in my project's architecture and the rest of my code.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.