jvandemo.com

How to resolve application-wide resources centrally in AngularJS with ui-router

Introduction

user

Jurgen Van de Moere

Follow @jvandemo

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

How to resolve application-wide resources centrally in AngularJS with ui-router

Posted by Jurgen Van de Moere on .
Featured

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

How to resolve application-wide resources centrally in AngularJS with ui-router

Posted by Jurgen Van de Moere on .

featureimage

AngularUI Router is an amazing tool.

If you have read my article on resource resolution, you already know how to make sure that promises are resolved before controllers are instantiated. In fact, if you haven't read the article yet, I strongly recommend that your read it first before reading this article.

The challenge

Resolving promises before controllers are instantiated ensures that critical data is available in your application at the time you need it. In many cases information such as configuration data or translation dictionaries is required to be present at all times in your application.

You could hard code the information in your AngularJS code, but what if you need to load the information from your server?

And what if you need that information in every state?

The normal approach

Armed with the trick from my article on resource resolution, we can easily solve the problem like this:

// Use $stateProvider to configure your states.
$stateProvider

  .state('home', {
    url: '/home',
    resolve: {

      // Get AngularJS resource to query
      Config: 'Config',

      // Use the resource to fetch data from the server
      config: function(Config){
        return Config.get().$promise;
      },

      // Extract the configuration data
      // that we need for the home state
      data: function(config){
        return config.homeConfigData;
      }
    },
    views: {
      'content': {
        templateUrl: '/home.html',

        // The config data is guaranteed to be resolved
        // when the HomeCtrl is instantiated
        controller: 'HomeCtrl'
      }
    })


  .state('articles', {
    url: '/articles',
    resolve: {

      // Get AngularJS resource to query
      Config: 'Config',

      // Use the resource to fetch data from the server
      config: function(Config){
        return Config.get().$promise;
      },

      // Extract the configuration data
      // that we need for the articles state
      data: function(config){
        return config.articlesConfigData;
      }
    },
    views: {
      'content': {
        templateUrl: '/articles.html',

        // The config data is guaranteed to be resolved
        // when the HomeCtrl is instantiated
        controller: 'ArticlesCtrl'
      }
    });

This works like a charm, but the code to fetch the configuration becomes repetitive.

If your application has many states that require the same information, this would rapidly become hard to maintain.

Centralize resolution logic

Ui-router has this extremely handy feature where child states inherit resolves from their parent states. From the ui-router docs:

Child states will inherit resolved dependencies from parent state(s), which they can overwrite. You can then inject resolved dependencies into the controllers and resolve functions of child states.

So to avoid code duplication, let's introduce an abstract root state and call it main:

// Use $stateProvider to configure your states.
$stateProvider

  .state('main', {
    url: '/main',

    // Make this state abstract so it can never be
    // loaded directly
    abstract: true,

    // Centralize the config resolution
    resolve: {

      // Get AngularJS resource to query
      Config: 'Config',

      // Use the resource to fetch data from the server
      config: function(Config){
        return Config.get().$promise;
      }
    })

  // The home state now becomes a child state of main
  // so it can access the resolves of main
  .state('main.home', {
    url: '/home',
    resolve: {

      // Inject the config resolved in the main state
      // and extract the data we need
      data: function(config){
        return config.homeConfigData;
      }
    },
    views: {
      '[email protected]': {
        templateUrl: '/home.html',

        // The config data is guaranteed to be resolved
        // when the HomeCtrl is instantiated
        controller: 'HomeCtrl'
      }
    })

  // The articles state also becomes a child state of main
  // so it can access the resolves of main
  .state('main.articles', {
    url: '/articles',
    resolve: {

      // Inject the config resolved in the main state
      // and extract the data we need
      data: function(config){
        data = config.articlesConfigData;
      }
    },
    views: {
      '[email protected]': {
        templateUrl: '/articles.html',

        // The config data is guaranteed to be resolved
        // when the HomeCtrl is instantiated
        controller: 'ArticlesCtrl'
      }
    });

All shared code has now been centralized in the main parent state. The state is abstract, because we don't want visitors to be able to directly navigate to the state. We just want to use the state to perform resource resolution for its child states.

Update 2016-01-04: View a working example here.

Ensure resolution in child state

Notice that the config from the main state is injected as an argument in the resolve from the child state.

Since the config in the main state returns a promise, it is guaranteed to be resolved by the time the controller of the child state is instantiated.

It is important to note that this is only guaranteed when the config in the parent state returns a promise.

If the parent state would have looked like this:

// Use $stateProvider to configure your states.
$stateProvider

  .state('main', {
    url: '/main',

    // Make this state abstract so it can never be
    // loaded directly
    abstract: true,

    // Centralize the config resolution
    resolve: {

      // Get AngularJS resource to query
      Config: 'Config',

      // Use the resource to fetch data from the server
      // but don't return the promise
      config: function(Config){
        return Config.get();
      }
    });

then you can rest assured that config will eventually be resolved, but this time it is not guaranteed to be resolved by the time the child state controller is instantiated.

Resolve like a pro

With this powerful technique you can:

  • create hierarchies of promise based dependencies
  • centralize logic to resolve application-wide dependencies
  • specify which resolves are required to be resolved in each state individually

Taking control over dependency resolution allows you to take your application to a whole new level by letting you control exactly what data is available at the time you need it.

Centrally. Hierarchically. Per state. Using promises. Like a pro.

Sergey Goliney has created a nice plnkr here that demonstrates how the child states wait for the parent resolves to be resolved. Thanks Sergey!

user

Jurgen Van de Moere

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