Router: public API for current routes (dynamic breadcrumb)


#1

Public API for getting current routes from router

The use case: I want the ability to construct a dynamic breadcrumb based on the current route/state hierarchy.

The problem: There is currently no public API for accessing the chain of context on the router. The internal currentHandlerInfos can be used, but not without feeling like you need to take a shower afterwards. This use case is common enough that users shouldn’t have to dig into the internal api.

Proposal: I propose we have a public API for accessing this information, but as I am not an API design expert, I am simply proposing the concept so those smarter than me can consider the issue. I’m happy to think up a variety of use cases if necessary, but the value is pretty apparent.

Some things to consider:

  1. The user should have access to the linear hierarchy of routes & contexts. i.e. router.get(‘currentRoutes’)
  2. Should they be able to query for a specific route to get just that set of handler/context data for convenience?
  3. What’s the data shape of each item in the array? Do we need all the keys found in currentHandlerInfos, or can we simply have an array of the route (‘handler’) objects?

#2

What did you end up doing here? It seems the community has been tragically silent.


#3

The API is indeed lacking a clear way to do this. Maybe something just slightly cleaner would be to extend the didTransition method in your Router which receives a copy of the currentHandlerInfos list, and just maintain another property on your Router containing the list of routes. This is far from being ideal given that didTransition is private, but at least once a clean solution comes up you can turn that property into a computed property that will map to the right thing.


#4

I once wrote something like this:

template: function () {
    var template = [],
        controller = this.get('controller'),
        router = controller.container.lookup('router:main'),
        currentHandlerInfos = router.get('router.currentHandlerInfos');

    for (var i = 0; i < currentHandlerInfos.length; i++) {
        var name = Em.get(currentHandlerInfos[i], 'name');

        if (!(router.hasRoute(name) || router.hasRoute(name + '.index')) || name.endsWith('.index')) {
            continue;
        }

        var notLast = i < currentHandlerInfos.length - 1 && !Em.get(currentHandlerInfos[i + 1], 'name').endsWith('.index');

        template.push('<li' + (notLast ? '>' : ' class="active">'));
        if (notLast) {
            template.push('{{#linkTo "' + name + '"}}');
        }
        template.push(name);
        if (notLast) {
            template.push('{{/linkTo}}');
        }

        if (notLast) {
            template.push('<span class="divider">/</span>');
        }

        template.push('</li>');
    }

    return Em.Handlebars.compile(template.join("\n"));
}.property('controller.currentPath')

#5

Following @pjlammertyn’s lead, and taking bits and bobs from various answers, I recently used the following:

App.ApplicationController = Ember.Controller.extend({
  
  breadcrumbs: [],

  watchCurrentPath: function () {
    this.send('setCrumbs');
  }.observes('currentPath'),

  actions: {
    currentPathDidChange: function () {
      this.send('setCrumbs');
    },
    setCrumbs: function() {

      // BEWARE:
      // This is some super hacky, non-public API shit right here

      var crumbs = [];

      // Clear out the current crumbs
      this.get('breadcrumbs').clear();

      // Get all the route objects
      var routes = this.container.lookup('router:main')
        .get('router.currentHandlerInfos');

      // Get the route name, and model if it has one
      routes.forEach(function(route, i, arr) {

        // Ignore index routes etc.
        var name = route.name;
        if (name.indexOf('.index') !== -1 || name === 'application') {
          return;
        }

        var crumb = Ember.Object.create({
          route: route.handler.routeName,
          name: route.handler.routeName,
          model: null
        });

        // If it's dynamic, you need to push in the model so we can pull out an ID in the link-to
        if (route.isDynamic) {
          crumb.setProperties({
            model: route.handler.context,
            name: route.handler.context.get('name')
          });
        }

        // Now push it to the crumbs array
        crumbs.pushObject(crumb);
      });

      this.set('breadcrumbs', crumbs);

      // Set the last item in the breadcrumb to be active
      this.get('breadcrumbs.lastObject')
        .set('active', true);

    }
  }
});

Then in the template…


{{#each breadcrumbs}}
  <li {{bindAttr class="active"}}>
    {{#if model}}
      {{#link-to route model.id}}
        <i {{bindAttr class=':fa icon'}}></i> 
        {{name}}
      {{/link-to}}
    {{else}}
      {{#link-to route}}
        <i {{bindAttr class=':fa icon'}}></i> 
        {{name}}
      {{/link-to}}
    {{/if}}
  </li>
{{/each}}

It works pretty great, obviously with the caveat that it uses completely non-public APIs…