I want to limit as much possible the "flickering" in my AngularJS application. I use resolve: from the router (works with ngRouter and ui-router) to load the data so that everything needed to display my page is available before changing the route.
Common example:
.state('recipes.category', {
url: '/:cat',
templateUrl: '/partials/recipes.category.html',
controller: 'RecipesCategoryCtrl as recipeList',
resolve: {
category: ['$http','$stateParams', function ($http, $stateParams) {
return $http.get('/recipes/' + $stateParams.cat).then(function(data) { return data.data; });
}]
}
});
Now my RecipesCategoryCtrl controller can load category and the promise will be directly resolved.
Is there a way to embed the loading code directly inside my controller? Or somewhere else more "clean"? I don't like having too much logic inside the route definition…
Something like:
.state('recipes.category', {
url: '/:cat',
templateUrl: '/partials/recipes.category.html',
controller: 'RecipesCategoryCtrl as recipeList',
resolve: 'recipeList.resolve()' // something related to RecipesCategoryCtrl and "idiomatic" AngularJS
});
Maybe this is not what you are looking for, but you can move some logic from the router to your controller using like:
//prepare content
$scope.$on('$viewContentLoading', function(event) {
//show a progress bar whatever
//fetch/request your data asynchronously
//when promise resolves, add your data to $scope
});
//remove spinning loader when view is ready
$scope.$on('$viewContentLoaded', function(event) {
//remove progress bar
});
Basically the user is sent to the page first, then the dynamic content is loaded, then you show the full page.
This might be completely off topic, but I am using this approach and works ace. It is also a good practice to display the new view first and then get the data. There is a really nice video here explaining why. The video is about SPA with Phonegap, but there are lots of tips about SPA in general. The interesting part (for this specific case) is at 1hr 1 minute in roughly.
Edit: if $viewContentLoading
does not get fired, look here. You might need to place all your logic inside $viewContentLoaded
This is what I am currently doing:
$scope.$on('$viewContentLoaded', function(event) {
//show loader while we are preparing view
notifications.showProgressDialog();
//get data
getData().then(function(data) {
//bind data to view
$scope.data = data;
//remove spinning loader as view is ready
notifications.hideProgressDialog();
});
});
I am still not 100% happy with $scope.data = data;
as if my data object is big, I might hide the progress dialog before the binding with the view is finished, therefore some flickering could occur. The solution is to use custom directives handling scope.$last
, see this answer (even though binding to $stateChangeSuccess
could be enough, look here)
This is how ui-router currently works when changing state/view:
$viewContentLoading
gets broadcasted$viewContentLoaded
is emitted.$viewContentLoaded
(when it is setup as the delegate to dispatch those events of course).This is common problem, which often occurs in applications using ngRoute or uiRouter. In such cases I usually use caching.
For example if you use Active Record like pattern for communication with our business layer you can proceed as follows:
//...
.state('users', {
url: '/users',
templateUrl: '/partials/users.html',
controller: 'UsersCtrl',
resolve: {
users: ['Users', function (Users) {
return Users.getList();
}]
}
})
.state('user', {
url: '/users/:id',
templateUrl: '/partials/user.html',
controller: 'UserCtrl',
resolve: {
users: ['Users', '$stateParams', function (Users, $stateParams) {
return Users.get($stateParams.id);
}]
}
});
myModule.factory('User', function ($q, $http) {
var cachedUsers = null;
function User() {
}
User.getList = function () {
if (cachedUsers) {
return $q.when(cachedUsers);
} else {
return $http.get('/users')
.then(function (resp) {
cachedUsers = resp.data;
return cachedUsers;
});
}
};
User.get = function (id) {
if (cachedUsers && cachedUsers[id]) {
return $q.when(cachedUsers[id]);
} else {
return $http.get('/users/' + id)
.then(function (resp) {
cachedUsers = cachedUsers || {};
cachedUsers[id] = resp.data;
return cachedUsers[id];
});
}
};
return User;
});
myModule.controller('UsersCtrl', function ($scope, users) {
$scope.users = data;
});
myModule.controller('UserCtrl', function ($scope, user) {
$scope.users = data;
});
This way your application caches the result from the request and in each subsequent route change it gets the requested value by the in-memory cache. Since this is a dummy example I'd recommend you to use the built-in AngularJS caching mechanism since it takes advantage of different HTTP headers.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With