CouchCoder

Search


Latest Posts


Recent Comments


July 2017
M T W T F S S
« Apr    
 12
3456789
10111213141516
17181920212223
24252627282930
31  
CouchCoder

Automate ui-router using $templateCache, a decorator, and some conventions

Automate ui-router states registration by decorating Angular's $templateCache service, and following some conventions

MerottMerott

Introduction

I recently started work on a cross-platform, Ionic powered version of my pet project (it’s MApp), and had to make a decision as to which version of Angular to use. Except, I didn’t have much of a choice here because Angular 2 is still Alpha at the time of writing this post, and Ionic 2 won’t be out and stable anytime soon. Some of the greatest features of Ionic also depend on ui-router, so even Angular’s new router was out of the picture. However, I really liked the new router’s conventional approach to registering controllers and templates, especially because of the amount of boilerplate code that it helps us avoid. I set out to do something similar, and maybe more, using our good ol’ ui-router.

I’ve always structured my component controllers and template folders in such way to mirror that of the URLs they are pointed at, as a result of which I should be able to programmatically generate my state definition objects. Now, you may not be mirroring the physical folder structure in your own projects, but it’s very likely that you’ll be able to find some sort of connection between the two, or create a connection with some slight changes in order to be able to auto-generate your state definitions.

The main challenge is to read that folder structure and parse those paths so that we can generate our states. We can either do that as part of our build process and generate a single file/module where all the states and route definitions go, or we can leverage Angular’s $templateCache and define our states at runtime. I’m going to show you how this can be done using the $templateCache.

How to automate ui-router

First of all, using Angular’s $templateCache and a build process such as gulp-angular-templatecache or grunt-angular-templates we can pre-populate Angular’s template cache with our templates in order to reduce network calls and improve performance. If you’re not doing that already, you should be.

1. Decorate $templateCache

We’re going to decorate the $templateCache service and intercept the put method so that we know about every template that goes into the $templateCache and can act on them.

angular.module('mapp.services.autoRouter', ['ui.router'])
    .config(config);

// @ngInject
function config($provide, $stateProvider) {
  $provide.decorator('$templateCache', decorateTemplateCache);

  function decorateTemplateCache($delegate) {
    let put = $delegate.put;

    $delegate.put = function(url) {
      setupRoute(url);
      return put.apply($delegate, Array.prototype.slice.call(arguments));
    };

    return $delegate;
  }
}

2. Set up routes

Inside of the setupRoute function, we’ll check if the template URL passing through belongs to a component that will have a route. If so, we’ll parse the URL and register a state using $stateProvider.

// ui-router uses dot notation for representing state hierarchies
const STATE_DELIMITER = '.';

// My components that need states are under the ./components folder
const STATES_DIR_ROOT = './components';

// like with the new Angular router, my controllers have a Controller suffix
const DEFAULT_CONTROLLER_SUFFIX = 'Controller';

// a dictionary object to keep track of our configured states
let configuredStates = {};

function setupRoute(templateUrl) {
  if(templateUrl.indexOf(STATES_DIR_ROOT) === 0) {
    // find the template path relative to the root
    let afterRoot = templateUrl.substr(STATES_DIR_ROOT.length);

    // find the directory path relative to the root
    let componentDir = afterRoot.match(/^\/(.*)\/[^\/]*$/)[1];

    // the name of the component will be the name of the folder
    let component = componentDir.substr(componentDir.lastIndexOf('/') + 1);

    // the name of the ui-router state will be the directory path parts
    // delimited with dots (e.g. master.detail)
    let stateName = componentDir.replace(/\//g, STATE_DELIMITER);

    // make sure this state hasn't been registered already
    if(!configuredStates[stateName]) {
      configuredStates[stateName] = true;

      let stateConfig = {};
      stateConfig.url = `/${component}`;
      stateConfig.templateUrl = templateUrl;
      stateConfig.controller = componentToCtrl(component);
      stateConfig.controllerAs = component;

      // Finally, register the state
      $stateProvider.state(stateName, stateConfig);
    }
  }
}

// taken from the new angular router's source
function componentToCtrl(name) {
  return name[0].toUpperCase() + name.substr(1) + DEFAULT_CONTROLLER_SUFFIX;
}

Your implementation of setupRoute may be different depending on your own conventions and preferences; you have full control over your state configurations.

This was my first ever blog post, and I hope that you found it useful. Let me know in the comments :-).


Full example

angular.module('mapp.services.autoRouter', ['ui.router'])
    .config(config);

// ui-router uses dot notation for representing state hierarchies
const STATE_DELIMITER = '.';

// My components that need states are under the ./components folder
const STATES_DIR_ROOT = './components';

// like with the new Angular router, my controllers have a Controller suffix
const DEFAULT_CONTROLLER_SUFFIX = 'Controller';

// @ngInject
function config($provide, $stateProvider) {
  // a dictionary object to keep track of our configured states
  let configuredStates = {};

  $provide.decorator('$templateCache', decorateTemplateCache);

  function decorateTemplateCache($delegate) {
    let put = $delegate.put;

    $delegate.put = function(url) {
      setupRoute(url);
      return put.apply($delegate, Array.prototype.slice.call(arguments));
    };

    return $delegate;
  }

  function setupRoute(templateUrl) {
    if(templateUrl.indexOf(STATES_DIR_ROOT) === 0) {
      // find the template path relative to the root
      let afterRoot = templateUrl.substr(STATES_DIR_ROOT.length);

      // find the directory path relative to the root
      let componentDir = afterRoot.match(/^\/(.*)\/[^\/]*$/)[1];

      // the name of the component will be the name of the folder
      let component = componentDir.substr(componentDir.lastIndexOf('/') + 1);

      // the name of the ui-router state will be the directory path parts
      // delimited with dots (e.g. master.detail)
      let stateName = componentDir.replace(/\//g, STATE_DELIMITER);

      // make sure this state hasn't been registered already
      if(!configuredStates[stateName]) {
        configuredStates[stateName] = true;

        let stateConfig = {};
        stateConfig.url = `/${component}`;
        stateConfig.templateUrl = templateUrl;
        stateConfig.controller = componentToCtrl(component);
        stateConfig.controllerAs = component;

        // Finally, register the state
        $stateProvider.state(stateName, stateConfig);
      }
    }
  }

  // taken from the new angular router's source
  function componentToCtrl(name) {
    return name[0].toUpperCase() + name.substr(1) + DEFAULT_CONTROLLER_SUFFIX;
  }
}

I'm a web developer who enjoys coding on the couch as much as I enjoy developing applications as a profession. I owe much of my success as a developer to the very people who blog and share, and this is my attempt to give something back to the community. I'm an advocate of writing clean, concise, well-structured code, and I also love to experiment with new technologies, trying to get my hands dirty with anything and everything.

Comments 2
  • Michael Jennings
    Posted on

    Michael Jennings Michael Jennings

    Reply Author

    I can’t believe it! I’m the first commenter! Well let me say thank you, on behalf of all the lurkers who have undoubtedly benefited from this like I have :)


    • Merott
      Posted on

      Merott Merott

      Reply Author

      Hey Michael, thank you for letting me know that you found this post useful. It’s motivation for me to write more :)