Fast Tricks

Show route only after all promises are resolved

, 11 Comments

Check the code on Github

The code is available on GITHUB.
Check the repository route-loading-delay-demo.

This post is about a trick I learned this summer while reading the book AngularJS, by Brad Green.
When a new route is requested in an AngularJS application, it is often necessary to retrieve data from the server to fill the template. If the server request takes too long to complete, it is therefore possible that an incomplete view is presented to the user.
To avoid this problem it is possible to delay the appearance of the new view until all the pending XHR requests have completed. This post will show you how to achieve this effect… in the meantime feel free to take a look at this demo that I prepared.

Digging into the code

Let’s start taking a look at the most important pieces of code (however you can find the complete source of the demo on Github).
In a regular AngularJS web app, the routes are configured via methods in the $routeProvider object, as below:


$routeProvider.when('/library', {
	templateUrl: 'partials/library.html',
	controller: 'LibraryCtrl',
	resolve: {
		books: function(srvLibrary) {
			return srvLibrary.getBooks();
		},
		movies: function(srvLibrary) {
			return srvLibrary.getMovies();
		}
	}
}); 

From this code, we can see that a route information object is passed in as the second parameter to the when method. The above example also includes the resolve property, which indicates which routes we want to appear only after the promises have been resolved.
Quoting from the official docs:

[The resolve property is] an optional map of dependencies which should be injected into the controller. If any of these dependencies are promises, they will be resolved and converted to a value before the controller is instantiated and the $routeChangeSuccess event is fired.

To use this with AngularJS promises, the following is a simple implementation of the methods exposed by the srvLibrary service:


angular.module('myApp.services', []).factory('srvLibrary', ['$http', function($http) {
	var sdo = {
		getBooks: function() {
			var promise = $http({ method: 'GET', url: 'api/books.php' }).success(function(data, status, headers, config) {
				return data;
			});
			return promise;
		},
		getMovies: function() {
			var promise = $http({ method: 'GET', url: 'api/movies.php' }).success(function(data, status, headers, config) {
				return data;
			});
			return promise;
		}
	}
	return sdo;
}]);

Finally, to be able to use the resolved promises data in the controller it is necessary to inject these data into the controller, and attach them to the $scope variable.


angular.module('myApp.controllers', []).controller('LibraryCtrl', 
		['$scope', 'books', 'movies', function($scope, books, movies) {
	$scope.books = books.data;
	$scope.movies = movies.data;
}]);

This is all that is required to delay the loading of a new route until the promises are resolved. As a result, however, a user could be surprised by the fact that nothing happens after they click a link. It is therefore a good idea to implement an immediate visual feedback


var app = angular.module('myApp', ['myApp.services', 'myApp.controllers']);

app.run(['$rootScope', function($root) {
	$root.$on('$routeChangeStart', function(e, curr, prev) { 
		if (curr.$$route && curr.$$route.resolve) {
			// Show a loading message until promises are not resolved
			$root.loadingView = true;
		}
	});
	$root.$on('$routeChangeSuccess', function(e, curr, prev) { 
		// Hide loading message
		$root.loadingView = false;
	});
}]);

The following is one possible way to implement a loading message while waiting for the new route to appear.


<div class="modal" ng-show="loadingView">
	<!-- loadingView is a variable defined in the $rootScope -->

	<!-- The loading animation is inspired by http://codepen.io/joni/details/FiKsd -->
	<ul id="loading">
		<li class="bar" ng-repeat="i in [0,1,2,3,4,5,6,7,8,9]"></li>
	</ul>
</div>

Then when a new route is requested, it is enough to set loadingView = true; if the route itself has pending promises.


// in the body of the controller
$scope.loadingView = true;

Bruno

I'm Bruno Scopelliti, web developer from Bologna, Italy. I use this space to share my experiences, experiments and thoughts. I'm also on Google+, Twitter, and LinkedIn.

More Posts

Standard
  • Julien Bouquillon

    Thanks Bruno ;) how do you deal with route promises that fail? They’re generally mandatory to display the view and there’s nothing in Angular that allows to easily cancel the route change in such cases.

    • http://brunoscopelliti.com/ Bruno Scopelliti

      Hi Julien,
      not sure if this can be the better solution, however you could try to use a response interceptor to provide additional information about the error occurred…
      I wrote about response interceptor time ago ( blog.brunoscopelliti.com/http-response-interceptors )… however this post is a bit outdated (as a commenter pointed out), and the way to use interceptor is different in the latest releases of angularjs.

      • Julien Bouquillon

        Thanks. I’ve read all your great posts :)
        I meant when transitionning to a new view, there’s no way to cancel the view change properly if one of the promises fails. say i want to go to “product detail view” and it needs some product data from a service, if a promise fails i’m just stuck in an invalid state (view displayed but no data).
        My current workaround is to load the dependencies before changing the location, and stays on the current view if one fails. but i think angular should have a solution for this, don’t you ?

        • Louis Sivillo

          Julien, if one of the promises provided via the route resolve fails, Angular will fire a $routeChangeError. You could do something like this to handle such errors:

          $rootScope.$on(‘$routeChangeError’, function(event, cur, prev, rejection) {
          // you could look at rejection and do something depending on the status code.
          $location.path(‘/error’).replace()

          });

          • Julien Bouquillon

            Thanks Louis for the tip ! That’s exactly how we should redirect to the “prev” route when resolve fails

  • http://www.srigi.sk/ Igor Hlina

    Hi Bruno, thx for nice article.
    One note. In your Controller code you are using array-like syntax for DI to avoid issues with minification. Please note that this needed also in first code snippet $routeProvider.when.resolve(). DI kicks in there as well.

  • Biju Murali

    Hi Bruno,
    Very nice article. I was in a search for resolving this issue since my testing team was after me with this issue that the view is loading before the xhr requests are completed. Thanks again for the help.

  • Luiz Filipe

    this code is great, but you cannot inject parameters in the services at resolved object :/

    • Tom

      you can use $route.current.params instead of routeParams

      • Luiz Filipe

        Awesome, thanks!

  • https://codeable.io/ Tomaž Zaman

    Thanks Bruno, very useful, as usual. We ended implementing pace (http://github.hubspot.com/pace/) which automatically detects pushState and XHR which means we could skip the ‘run’ part of your tutorial :)