Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid "polluting" the resolve method with ui-router

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
});
like image 418
Thomas Avatar asked Mar 07 '14 16:03

Thomas


2 Answers

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:

  1. $viewContentLoading gets broadcasted
  2. Dependencies under the resolve section are resolved
  3. Controller gets instantiated and resolve dependencies injected.
  4. $viewContentLoaded is emitted.
  5. The controller reacts to $viewContentLoaded (when it is setup as the delegate to dispatch those events of course).
like image 147
Mirko Avatar answered Oct 31 '22 21:10

Mirko


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:

States definition

//...
.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);
    }]
  }
});

Service definition

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;
});

Controllers definition

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.

like image 41
Minko Gechev Avatar answered Oct 31 '22 21:10

Minko Gechev