Using Resolve In AngularJS Routes

Tuesday, May 20, 2014

In a previous post about testing I mentioned that route resolves can make authoring unit tests for a controller easier. Resolves can also help the user experience.

A resolve is a property you can attach to a route in both ngRoute and the more robust UI router. A resolve contains one or more promises that must resolve successfully before the route will change. This means you can wait for data to become available before showing a view, and simplify the initialization of the model inside a controller because the initial data is given to the controller instead of the controller needing to go out and fetch the data.

As an example, let’s use the following simple service which uses $q to simulate the async work required to fetch some data.

app.factory("messageService", function($q){
    return {
        getMessage: function(){
            return $q.when("Hello World!");
        }
    };
});

And now the routing configuration that will use the service in a resolve.

$routeProvider
    .when("/news", {
        templateUrl: "newsView.html",
        controller: "newsController",
        resolve: {
            message: function(messageService){
                return messageService.getMessage();
        }
    }
})

Resolve is a property on the routing configuration, and each property on resolve can be an injectable function (meaning it can ask for service dependencies). The function should return a promise.

When the promise completes successfully, the resolve property (message in this scenario) is available to inject into a controller function. In other words, all a controller needs to do to grab data gathered during resolve is to ask for the data using the same name as the resolve property (message).

app.controller("newsController", function (message) {
    $scope.message = message;
});

You can work with multiple resolve properties. As an example, let’s introduce a 2nd service. Unlike the messageService, this service is a little bit slow.

app.factory("greetingService", function($q, $timeout){
   return {
       getGreeting: function(){
           var deferred = $q.defer();
           $timeout(function(){
               deferred.resolve("Allo!");
           },2000);
           return deferred.promise;
       }
   }
});

Now the resolve in the routing configuration has two promise producing functions.

.when("/news", {
    templateUrl: "newsView.html",
    controller: "newsController",
    resolve: {
        message: function(messageService){
            return messageService.getMessage();
        },
        greeting: function(greetingService){
            return greetingService.getGreeting();
        }
    }
})

And the associated controller can ask for both message and greeting.

app.controller("newsController", function ($scope, message, greeting) {
    $scope.message = message;
    $scope.greeting = greeting;
});

Composing Resolve

Although there are benefits to moving code out of a controller, there are also drawbacks to having code inside the route definitions. For controllers that require a complicated setup I like to use a small service dedicated to providing resolve features for a controller. The service relies heavily on promise composition and might look like the following.

app.factory("newsControllerInitialData", function(messageService, greetingService, $q) {
    return function() {
        var message = messageService.getMessage();
        var greeting = greetingService.getGreeting();

        return $q.all([message, greeting]).then(function(results){
            return {
                message: results[0],
                greeting: results[1]
            };
        });
    }
});

Not only is the code inside a service easier to test than the code inside a route definition, but the route definitions are also easier to read.

.when("/news", {
    templateUrl: "newsView.html",
    controller: "newsController",
    resolve: {
        initialData: function(newsControllerInitialData){
            return newsControllerInitialData();
        }
    }
})

And the controller is also easy.

app.controller("newsController", function ($scope, initialData) {
    $scope.message = initialData.message;
    $scope.greeting = initialData.greeting;
});

One of the keys to all of this working is $q.all, which is a beautiful way to compose promises and run requests in parallel.


Comments
gravatar Arun Mahendrakar Tuesday, May 20, 2014
Nice post Scott. I have posted a question on a related topic here (http://stackoverflow.com/questions/23724889/write-unit-test-for-resolve-function-in-angularjs). Can you please have a look at it and help me out? Thanks Arun
gravatar Scott Wednesday, May 21, 2014
@Arun: I think an end to end test is better for route resolution. I don't see a lot of benefit writing unit tests for the routes, but that is my personal opinion.
gravatar Callum Vass Wednesday, May 21, 2014
Hi Scott, This post is perfect timing. I'm trying to do something similar but I get an error. See this: http://stackoverflow.com/questions/23695912/angular-resolve-not-injecting-dependency. Any ideas, as I thought it looks similar to above? Thanks.
gravatar Arun Mahendrakar Wednesday, May 21, 2014
Thanks for the update Scott.
gravatar Rob Paddock Thursday, May 22, 2014
Hi Scott, First excellent post. I have been using this technique for a while and it works great. However I recently inherited a project that was not using resolve at all and the argument for it was it was better to get part of a page loaded rather than displaying nothing until all the data was resolved. Bear in mind this is a mobile app. Just interested to here what other people may think on this.
gravatar Scott Saturday, May 24, 2014
@Callum : Do you have a jsFiddle or Plunk to duplicate the problem? Would be strange if after changing to return the promise it didn't work.
gravatar Scott Saturday, May 24, 2014
@Rob: I think for the very first page should load fast, but this can usually be the shell view. After this I think it's up for debate, really, I can see advantages in both directions.
gravatar asaunders Tuesday, May 27, 2014
how do you address this situation? where you are structuring your controllers like this ? .controller('ImageUploadCtrl', ['$scope', '$dataStore', 'selectedTruck', function ($scope, $dataStore, selectedTruck) { // do something... }]); I am getting errors when trying to inject 'selectedTruck'?
gravatar scott Wednesday, May 28, 2014
@asaunders: It should work, what does the resolve look like?
gravatar Justin Monday, June 9, 2014
Also, I just ran into this problem. If your angular is minified, you have to include the resolve property in the "minified" syntax when declaring your controller. i.e ['$scope', 'resolveProperty', function()...]. Otherwise the property will come through as undefined.
gravatar Scott Monday, June 9, 2014
@Justin: Yes, you have to annotate all the dependencies if minifying.
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!