nathanhammond/ember-route-alias

View on GitHub
addon/initializers/route-alias.js

Summary

Maintainability
C
7 hrs
Test Coverage
import Ember from 'ember';

/*
Reopen `Ember.Route` to attach a new property to the controller to make it
available in all route templates. We only need to do this once as it is a global
change that is not instance specific.
*/
Ember.Route.reopen({
  setupController(controller) {
    this._super.apply(this, arguments);
    controller.set('contextRoute', this.routeName);
  }
});

// Sets up our magic alias function.
function createAlias() {
  Ember.RouterDSL.prototype.alias = function(aliasRoute, aliasPath, aliasTarget) {
    Ember.assert('You must create a route prior to creating an alias.', this.handlers || this.intercepting);
    Ember.assert('The alias target must exist before attempting to alias it.', this.handlers[aliasTarget]);

    // Grab a reference to the arguments passed in for the original route.
    let [options, callback] = this.handlers[aliasTarget];
    options.path = aliasPath;

    this.intercepting.push({ aliasRoute, aliasTarget });

    this.route(aliasRoute, options, callback);
  };
}

// Patches the RouterDSL route function to work with aliases.
function patchRoute(lookup) {
  // Save off the original method in scope of the prototype modifications.
  let originalRouteMethod = Ember.RouterDSL.prototype.route;

  // We need to do a few things before and after the original route function.
  Ember.RouterDSL.prototype.route = function(name, options, callback) {
    Ember.assert('You may not include a "." in your route name.', !~name.indexOf('.'));

    // Method signature identification from the original method.
    if (arguments.length === 2 && typeof options === 'function') {
      callback = options;
      options = {};
    }

    if (arguments.length === 1) {
      options = {};
    }

    // Save off a reference to the original arguments in a reachable scope.
    // This is so later calls to `alias` have something to find.
    if (!this.handlers) { this.handlers = {}; }
    this.handlers[name] = [ options, callback ];

    // For storing the root of the aliased route.
    if (!this.intercepting) { this.intercepting = []; }

    // So, we're "recursing" through a structure, but we can sneak in by wrapping the invoked function.
    if (this.intercepting.length) {

      // Make the callback modify the DSL generated for nested routes.
      // Necessary so they can register themselves.
      // Propogate the original interception information forward.
      var currentIntercepting = this.intercepting[this.intercepting.length - 1];
      let interceptingCallback = function() {
        this.intercepting = [currentIntercepting];
        callback.call(this);
      };

      // Figure out how many routes we created.
      let originalLength = [].concat.apply([], this.matches).length;
      originalRouteMethod.call(this, name, options, callback ? interceptingCallback : undefined);
      let newMatches = [].concat.apply([], this.matches);
      let newLength = newMatches.length;

      // Add each of them to the lookup.
      for (let i = originalLength; i < newLength; i += 3) {
        let intermediate = newMatches[i + 1].split('.');
        let qualifiedAliasRoute = intermediate.join('/');
        let qualifiedTargetRoute = qualifiedAliasRoute.replace(currentIntercepting.aliasRoute, currentIntercepting.aliasTarget);

        if (qualifiedAliasRoute !== qualifiedTargetRoute) {
          lookup[qualifiedAliasRoute] = qualifiedTargetRoute;
        } else {
          // For index routes we need to try again with the base intercepting object.
          let isIndex = intermediate.pop().indexOf('index') === 0;
          qualifiedTargetRoute = qualifiedAliasRoute.replace(this.intercepting[0].aliasRoute, this.intercepting[0].aliasTarget);
          if (isIndex && qualifiedAliasRoute !== qualifiedTargetRoute) {
            lookup[qualifiedAliasRoute] = qualifiedTargetRoute;
          }
        }
      }

    } else {
      originalRouteMethod.call(this, name, options, callback);
    }
  };
}

// Routing only happens once.
const lookup = {};

// Only patch things once for testing.
let hasRun = false;
export function initialize() {
  if (!hasRun) {
    hasRun = true;
    createAlias();
    patchRoute(lookup);
  }

  // Feed the lookup into the application.
  let application = arguments[1] || arguments[0];
  application._routeAliasLookup = lookup;
}

export default {
  name: 'route-alias',
  initialize: initialize
};