Skip to content

Using A1AtScript With UI Router

Tim Kindberg edited this page Jul 7, 2015 · 2 revisions

Although A1AtScript has no built in injector for UI-Router, you can register your own! Below you'll see two approaches to doing @State decorators.

States - Routing to Components

This approach is great is you want your states to be declared side-by-side with your components. Here's how you could use it:

@Component({
  selector: 'products-list',
  controllerAs: 'vm',
  properties: {
    state: 'state'
  }
})
@View({
  inline: `
    <h1>{{ vm.id }}</h1>
    <div ng-repeat="product in vm.products">{{ product.name }}}</div>
  `
})
@State('MyState', {
  url: '/products/:id',
  resolve: {
    products: ['$http', ($http) => $http.get('/api/products/all')]
  }
  /* leave these out
  controller: auto-generated
  template: auto-generated
  */
})
export default class HomePageComposer {
  constructor() {

    // All params are available from `this.state.params`
    this.id = this.state.params.id;

    // All fully resolved dependencies are in `this.state.resolve`
    this.products = this.state.resolve.products;
  }
}

Note: a1atscript v0.4.1 needed Here is the implementation:

import {getInjector, ToAnnotation, registerInjector} from 'a1atscript';

// Use the new hooks feature from v0.4.1 to register a hook that is run
// right after every component's `angular.directive` call is made.
getInjector('component').componentHooks.after.push(configureState);

// Each hook function gets the module and the ddo. That should be enough data to
// create almost any custom decorator that you'd want to work with Components.
// Some ddo exploration will be needed to find what you are after ;)
function configureState(module, ddo) {
  let component = ddo._controller;
  let annotations = component.annotations;
  let stateInstance = annotations.find((ann) => ann instanceof State.originalClass);

  if (stateInstance) {
    // console.log('Configure State:', ddo, ddo._controller, ddo._controller.annotations);

    stateInstance.generateControllerAndTemplate(ddo._annotation.selector);

    module.config(['$stateProvider', function($stateProvider) {
      $stateProvider.state(stateInstance.name, stateInstance.config);
    }]);
  }
}

// Create the State annotation
@ToAnnotation
export class State {
  constructor(name, config) {
    this.name = name;
    this.config = config;
  }

  generateControllerAndTemplate(selector) {
    if (this.config.resolve) {
      let injectedResolves = Object.keys(this.config.resolve).map(name => name);
      let params = {}

      function controller($scope, $stateParams, ...resolves) {
        $scope.state = {};
        $scope.state.params = $stateParams;
        $scope.state.resolve = resolves.reduce((obj, val, i) => {
          obj[injectedResolves[i]] = val;
          return obj;
        }, {});
      }

      this.config.controller = ['$scope', '$stateParams', ...injectedResolves, controller];
    }

    let componentHtml = `<${selector} bind-state="state"></${selector}>`;
    this.config.template = this.config.template || componentHtml;
  }
}

export class StateInjector {
  get annotationClass() {
    return State;
  }

  instantiate() { }
}

// Register it.
registerInjector('state', StateInjector);

States - States as Classes

This method keeps your states separate from your components and opts for classes instead of state configs. One of the things that has always annoyed me about ui-router is you write your states into a config block. Wouldn't it be nice if you could do something like this:

@State('root.main.inner')
class RootMainInnerState {
  constructor() {
    this.template = 'awesome/awesome.html'
    this.controller = 'AwesomeController'
  }

  @Resolve('Backend')
  model: function(Backend) {
  }

  @Resolve('AuthService')
  user: function(AuthService) {
  }

}

Well the good news is you could potentially do that. Just define an Annotation and an Injector

import {ToAnnotation, registerInjector} from 'bower_components/dist/a1atscript'

@ToAnnotation
export class State {
   constructor(stateName) {
     this.stateName = stateName;
   }
}

@ToAnnotation
export class Resolve {
  constructor(...inject) {
    this.inject = inject;
  }
}

// An Injector must define an annotationClass getter and an instantiate method
export class StateInjector {
  get annotationClass() {
    return State;
  }

  annotateResolves(state) {
    state.resolve = {}
    for (var prop in state) {
      if (typeof state[prop] == "function") {
        var resolveItem = state[prop];
        resolveItem.annotations.forEach((annotation) => {
          if (annotation instanceof Resolve) {
            resolveItem['$inject'] = annotation.inject;
            state.resolve[prop] = resolveItem;
          }
        });
      }
    }
  }

  instantiate(module, dependencyList) {
    var injector = this;
    module.config(function($stateProvider) {
      dependencyList.forEach((dependencyObject) => {
        var metadata = dependencyObject.metadata;
        var StateClass = dependencyObject.dependency;
        var state = new StateClass();
        injector.annotateResolves(state);
        $stateProvider.state(
          metadata.stateName,
          state
        );
      });
    })
  }
}

registerInjector('state', StateInjector);

That code works -- I've used it in my own projects for making ui-router easy to use. The best part is then you can create base states with common resolves and the extend them for your individual states.