jvandemo.com

How to force AngularJS resource resolution with ui-router

Introduction

user

Jurgen Van de Moere

Follow @jvandemo

ui-router, angular, ng-resource, 7 minutes

How to force AngularJS resource resolution with ui-router

Posted by Jurgen Van de Moere on .
Featured

ui-router, angular, ng-resource, 7 minutes

How to force AngularJS resource resolution with ui-router

Posted by Jurgen Van de Moere on .

featureimage

I'm a big fan of AngularUI Router and have used it extensively in several web applications we released with fabulous results. In this article I'll cover how we use ui-router and ngResource to resolve dependencies that are injected in a template controller.

If you are new to ui-router, I highly recommend you check it out the official wiki at https://github.com/angular-ui/ui-router/wiki as this article is by no means an introduction to ui-router.

A sample scenario

Let's assume we have a state customers:

angular.module('demo')  
    .config(['$stateProvider', function ($stateProvider) {
        $stateProvider
          .state("customers", {
            url : "/customers",
            templateUrl: 'customers.html',
            controller : 'customersCtrl'
          });
}]);

that has a template customers.html:

<ul>  
    <li ng-repeat="customer in customers">
        {{customer.name}}
    </li>
</ul>  

with a controller customersCtrl:

angular.module('demo')  
    .controller('customersCtrl', ['$scope', 'customers', function($scope, customers){

        // Log customers when controller executes
        console.log(customers);

        // Assign customers to scope
        $scope.customers = customers;
    }]);

and a resource customersResource:

angular.module('demo')  
    .factory('customersResource', ['$resource', function($resource) {
       return $resource('/customers/:customerId', {customerId: [email protected]'})
    }]);

Resolving the customers

Notice that the controller is defined to receive customers data using dependency injection. Instead of using an AngularJS service, we configure ui-router to take care of injecting the data by adding a resolve property to the state like this:

angular.module('demo')  
    .config(['$stateProvider', function ($stateProvider) {
        $stateProvider
          .state("customers", {
            url : "/customers",
            templateUrl: 'customers.html',
            resolve: {

                // A string value resolves to a service
                customersResource: 'customersResource',

                // A function value resolves to the return
                // value of the function
                customers: function(customersResource){
                    return customersResource.query();
                }
            },
            controller : 'customersCtrl'
          });
}]);

Note that we added a resolve property that represents a JavaScript object with 2 properties:

  • customersResource: since this has a string value, ui-router considers it to be an existing service in AngularJS, so it uses AngularJS dependency injection to resolve the customersResource resource factory.

  • customers: a function that is executed by ui-router and the return value is passed to the controller as the value of customers. In this case it will query a REST api using customersResource and assign the result to customers.

The caveat

Although at first sight the template will display the customers correctly, the console output of the controller will show an empty array [] instead of an array with all customers.

This is because AngularJS resource uses promises, which is a great technique that allows you to fetch the actual data in the background while the view is being rendered. Since we use the query() method of ngResource, AngularJS knows it will receive an array so it immediately assigns an empty array to customers, allowing the code to continue while the data is being fetched in the background.

As soon as the actual data has been resolved, AngularJS automatically updates customers with the real data, triggering the two-way binding to update the view resulting in a list of customers being displayed on the screen.

That is the exact reason why the console.log in the controller shows an empty array at the time the controller is executed. AngularJS has already assigned a empty array to customers but it is still fetching the data in the background.

Forcing resolution before the controller executes

Although this is expected behavior and perfectly acceptable in many cases, there may be situations where you need to be sure that the data is already resolved before the controller is executed.

Luckily, ui-router offers a great feature to accomplish this. From the ui-router docs:

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.

So to accomplish this, we need to update the line:

return customersResource.query();  

in the state definition to:

return customersResource.query().$promise;  

resulting in:

angular.module('demo')  
    .config(['$stateProvider', function ($stateProvider) {
        $stateProvider
          .state("customers", {
            url : "/customers",
            templateUrl: 'customers.html',
            resolve: {
                customersResource: 'customersResource',
                customers: function(customersResource){

                    // This line is updated to return the promise
                    return customersResource.query().$promise;
                }
            },
            controller : 'customersCtrl'
          });
}]);

By returning the $promise property of query(), ui-router will make sure that the data is completely resolved before the controller is executed.

When running the code again, you will notice that the full array of customers is now logged to the console at runtime from within the controller.

This also applies for getting a single resource with the customersResource.get() method. A child state customers.edit could look like this:

angular.module('demo')  
    .config(['$stateProvider', function ($stateProvider) {
        $stateProvider
          .state("customers.edit", {
            url : "/customers/:customerId",
            templateUrl: 'edit-customer.html',
            resolve: {
                customersResource: 'customersResource',
                customer: function(customersResource, $stateParams){

                    // Extract customer ID from $stateParams
                    var customerId = $stateParams.customerId;

                    // Return a promise to make sure the customer is completely
                    // resolved before the controller is instantiated
                    return customersResource.get({customerId: customerId}).$promise;
                }
            },
            controller : 'customersEditCtrl'
          });
}]);

This will ensure that the single customer is resolved before the customersEditCtrl controller is instantiated.

Why use resolve instead of AngularJS dependency injection?

You may be wondering why you should use ui-router resolve instead of using standard AngularJS dependency injection directly in the controller.

The main benefit of using the ui-router resolve property is the fact that the resolved information is also made available by ui-router to all child states of the current state. This is a fantastic feature that allows you to resolve the data only once and use it in different controllers in different child states.

And by supporting promises, ui-router also conveniently provides you with a mechanism to enforce resolution before the controller actually executes, which is awesome in case you need the actual data at runtime.

Update (2014-06-17)

If you find yourself resolving the same promise(s) for multiple states, you may also benefit from reading this related article on resolving application-wide resources centrally.

Update (2015-07-24)

The code samples in the article assume that you have already loaded the ngResource module in your application:

angular.module('demo', [  
  'ui.router',
  'ngResource'
]);

Not loading the module will cause the example code to fail silently, which may cause unwanted confusion. Special thanks to Dan Sullivan for bringing this up!

user

Jurgen Van de Moere

Front-end architect at The Force. Gymnast. Dad. Family man. Creator of Angular Express.