How to populate $ scope variable asynchronously in AngularJS?
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 asks for content $scope.fonts
, it is still empty.
How to update $scope.fonts
and $scope.families
when the download is over?
I could use $scope.$watch
, but I'm sure there is a cleaner way to do this ...
source to share
Thanks for answers! I ended up using a combination of callback and promise as follows:
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've tried all your suggestions, but this is the best one that works with the rest of my code.
source to share
These are actually promises . Your service needs to return a promise that needs to be resolved. You can also simplify your maintenance:
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 method then
:
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 due to $ http, but here is a demo using $ timeout:
source to share
Consider passing a callback function.
Services:
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 can be a bit tweaked as the callback removes the need for some of this code, but it will serve as an example.
source to share
In your function this.getFonts
(and other functions), you are calling data from this
that points to the function instead of the required controller scope. Try this 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
};
source to share
Make sure you declare self = this
outside any functions.
Assign the call to the value where you want to store the data 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
};
source to share
Since you are using ui-router (I saw the $ state). You can use the solution in your state and return the promise.
Doc: https://github.com/angular-ui/ui-router/wiki
Example:
$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 need to include your.getFonts and getFamilies in promises
this.getFonts = function(){
return $http.get(BACKEND_URL+'/fonts').success(function(data) {
self.fonts = data;
console.log('Library:: fonts updated', self.fonts);
});
}
There are many ways to do this, but in my opinion the solution is the best.
source to share