Avoiding UI flickering on route changes

While an application transitions between different screens, we usually need to get and display markup for a new screen as well as fetch the corresponding data (model). It turns out that there are two slightly different strategies that we can use to render a new screen, which are as follows:

  • Display new markup as soon as possible (even if corresponding data are not yet ready) and then repaint the UI upon arrival of the data from the back-end.
  • Make sure that all the requests to the back-end are completed and the data are ready before displaying markup for a new route

The first approach is the default one. For a route with both the templateUrl and the controller properties defined, angularjs will match the route and render the contents of the partial, even if the data requested by a controller are not ready by this time. Of course, angularjs will automatically repaint the partial when the data finally arrives (and is bound to the scope), but users might notice an unpleasant flickering effect. The UI flickering happens due to the same partial being rendered twice in a short timespan, firstly without data and then again when the data are ready.

The angularjs routing system has excellent, built-in support for the second approach where the route change (and UI repaint) is postponed until both the partial and all the requested data are ready. By using the resolve property on a route definition object we can enumerate asynchronous dependencies for a route’s controller. angularjs will make sure that all these dependencies are resolved before the route change (and controller instantiation) takes place.

To illustrate the basic usage of the resolve property, let’s rewrite our “edit user” route as follows:

.when('/admin/users/:userid', {
  templateUrl: 'tpls/users/edit.html'
  controller: 'EditUserCtrl',
   resolve: {
        user: function($route, Users) {
          return Users.getById($route.current.params.userid);
        }
    }
})

The resolve property is an object, where keys declare new variables that will be injected into the route’s controller. The values of those variables are provided by a dedicated function. This function can also have dependencies injected by the angularjs DI system. Here we are injecting the $route and Users services to retrieve and return user’s data.

These resolve functions can either return simple JavaScript values, objects, or promises. When a promise is returned, angularjs will delay the route change until the promise is resolved. Similarly if several resolve functions return promises, the angularjs routing system will make sure that all the promises are resolved before proceeding with the route change.

Note

Functions in the resolve section of a route definition can return promises. The actual route change will take place, if and only if all the promises are successfully resolved.

Once all these route-specific variables (defined in the resolve section) are resolved, they are injected into the route’s controller as follows:

.controller('EditUserCtrl', function($scope, user){
    $scope.user = user;
    ...
})

This is an extremely powerful pattern, as it allows us to define variables that are local to a given route and have those variables injected into a route’s controller. There are multiple practical applications of this pattern and in the sample SCRUM application we are using it to reuse the same controller with different values for the user variable (either created in place or retrieved from a back-end). In the following code snippet, we can see an extract from the sample SCRUM application:

$routeProvider.when('/admin/users/new', {
    templateUrl:'admin/users/users-edit.tpl.html',
    controller:'UsersEditCtrl',
    resolve:{
      user: function (Users) {
        return new Users();
      }
    }
  });

  $routeProvider.when('/admin/users/:userId', {
    templateUrl:'admin/users/users-edit.tpl.html',
    controller:'UsersEditCtrl',
    resolve:{
      user: function ($route, Users) {
        return Users.getById($route.current.params.userId);
      }
    }
  });

Defining local variables on a route level (in the resolve section) means that controllers defined as part of a route can be injected with those local variables. This greatly improves our ability to unit test the controllers’ logic.

Preventing route changes

There are times where we might want to block a route change, based on certain conditions. For example, consider the following route to edit a user’s data:

/users/edit/:userid

We need to decide what should happen if a user with the specified identifier doesn’t exist. One reasonable expectation would be that users of an application can’t navigate to a route pointing to a non-existing item.

As it turns out the resolve property of a route definition has a built-in support for blocking route navigation. If a value of one of the resolve keys is a promise that is rejected, angularjs will cancel route navigation and won’t change the view in the UI.

Note

If one of the promises returned in the resolve section of a route definition is rejected the route change will be canceled and the UI won’t be updated.

It should be noted that the URL in a browser’s address bar won’t be reverted if route change is canceled. To see what it means in practice, let’s assume that a list of users is displayed under the /users/list URL. All the users in a list might have links pointing to their edit forms (/users/edit/:userid). Clicking on one of those links will change the browser’s address bar (so it will become something like /users/edit/1234) but it is not guaranteed that we will be able to edit user’s details (user might have been deleted in the meantime, we don’t have sufficient access rights, and so on). If the route’s navigation is canceled the browser’s address bar won’t be reverted and will still read /users/edit/1234, even if UI will be still reflecting, content of the /users/list route.

Note

The Browser’s address bar and the displayed UI might get out of sync if route navigation is canceled due to a rejected promise.

 

Chapter 2 of 2Next