14

I want to display a crumble path with Ember. How can I iterate through the current path?

In my opinion there are two approaches:

The ember-way

EDIT: see my answer below

I keep this question up-to-date with the current status of displaying breadcrumbs. You can browse through the revisions of this question to see the history.

There are a couple of goals here:

  1. Listen on route change
  2. Finding current route
  3. displaying list of the current route
  4. display working links to the steps in the route

Controller

App.ApplicationController = Ember.Controller.extend({
    needs: ['breadcrumbs'],
    currentPathDidChange: function() {
        path = this.get('currentPath');
        console.log('path changed to: ', path);
        this.get('controllers.breadcrumbs').set('content',this.get('target.router.currentHandlerInfos'));
    }.observes('currentPath')
});
App.BreadcrumbsController = Em.ArrayController.extend({});

Router

App.ApplicationRoute = Ember.Route.extend({
    renderTemplate: function() {
        this.render();
        this.render('breadcrumbs', {
            outlet: 'breadcrumbs',
            into: 'application',
            controller: this.controllerFor('breadcrumbs')
        });
    }
});

Template

{{! application template }}
<div class="clearfix" id="content">
    {{outlet "breadcrumbs"}}
    {{outlet}}
</div>

{{! breadcrumbs template }}
<ul class="breadcrumb">
  {{#each link in content}}
    <li>
      <a {{bindAttr href="link.name"}}>{{link.name}}</a> <span class="divider">/</span>
    </li>
  {{/each}}
</ul>

The current problems to tackle are:

  • When I go to the URL: #/websites/8/pages/1 the output for the breadcrumbs is: (I removed all the script-tag placeholders
<ul class="breadcrumb">
  <li>
    <a href="application" data-bindattr-34="34">application</a> <span class="divider">/</span></li>
  <li>
    <a href="sites" data-bindattr-35="35">sites</a> <span class="divider">/</span>
  </li>
  <li>
    <a href="site" data-bindattr-36="36">site</a> <span class="divider">/</span>
  </li>
  <li>
    <a href="pages" data-bindattr-37="37">pages</a> <span class="divider">/</span>
  </li>
  <li>
    <a href="page" data-bindattr-38="38">page</a> <span class="divider">/</span>
  </li>
  <li>
    <a href="page.index" data-bindattr-39="39">page.index</a> <span class="divider">/</span>
  </li>
</ul>
  • The URL's should be a valid route
  • The menu is now hardcoded with {{#linkTo}} to the routes, I tried to make that dynamic, like here but a transitionTo doesn't trigger the currentPath-observer

The other way

Most is the same as above, but there are a couple of differences. The breadcrumbs are made by looping over location.hash instead of getting it from the Router.

The ApplicationController becomes:

ApplicationController = Ember.Controller.extend({
    needs: ['breadcrumbs'],
    hashChangeOccured: function(context) {
        var loc = context.split('/');
        var path = [];
        var prev;
        loc.forEach(function(it) {
            if (typeof prev === 'undefined') prev = it;
            else prev += ('/'+it)
            path.push(Em.Object.create({ href: prev, name: it }));
        });
        this.get('controllers.breadcrumbs').set('content',path)
    }
});
ready : function() {
    $(window).on('hashchange',function() {
        Ember.Instrumentation.instrument("hash.changeOccured", location.hash);
    });
    $(window).trigger('hashchange');
}

We need to subscribe the custom handler in the ApplicationRoute

App.ApplicationRoute = Ember.Route.extend({
    setupController: function(controller, model) {
        Ember.Instrumentation.subscribe("hash.changeOccured", {
            before: function(name, timestamp, payload) {
                controller.send('hashChangeOccured', payload);
            },
            after: function() {}
        });
    }
});

So far the alternative approach is working best for me, but it's not a good way of doing it because when you configure your Router to use the history instead of location.hash this method won't work anymore.

Willem de Wit
  • 8,604
  • 9
  • 57
  • 90

3 Answers3

3

Based on your current breadcrumb output I guess you have an error in your router.

The following command should return array with current breadcrumb:

App.get('Router.router.currentHandlerInfos');

Your router should be nested:

this.resource('page 1', function () {
    this.resource('page 2');
});

You can use #linkTo instead of a tag in your breadcrumb, you will get active class for free.

Wojciech Bednarski
  • 6,033
  • 9
  • 49
  • 73
2

I came up with a much simpler solution that I posted to the Ember discourse.

Iest
  • 1,391
  • 1
  • 9
  • 10
1

I found a (Ember-way) solution to display breadcrumbs. It is based on the router instead of my location.hash.

Infrastructure

First we need to make the infrastructure for the breadcrumbs before we add or remove items from the breadcrumbs array.

Menu

In my app.js I define a NavItem-object. This is a skeleton for all navigatable items. I use it to define my menu-items, but we are also going to use it for the breadcrumbs.

App.NavItem = Em.Object.extend({
    displayText: '',
    routeName: ''
});
// define toplevel menu-items
App.dashboardMenuItem = App.NavItem.create({
    displayText: 'Dashboard',
    routePath: 'dashboard',
    routeName: 'dashboard'
});
App.sitesMenuItem = App.NavItem.create({
    displayText: 'Websites',
    routePath: 'sites.index',
    routeName: 'sites'
});

Controllers

We need a BreadcrumbsController to keep the breadcrumbs in a central place

App.BreadcrumbsController = Em.ArrayController.extend({
    content: []
});

My ApplicationController depends on the BreadcrumbsController

App.ApplicationController = Ember.Controller.extend({
    needs: ['breadcrumbs']
});

The BreadcrumbsView is a subview of ApplicationView

Views

App.ApplicationView = Ember.View.extend({
    BreadcrumbsView: Ember.View.extend({
        templateName: 'breadcrumbs',

        init: function() {
            this._super();
            this.set('controller', this.get('parentView.controller.controllers.breadcrumbs'));
        },
        gotoRoute: function(e) {
            this.get('controller').transitionToRoute(e.routePath);
        },

        BreadcrumbItemView: Em.View.extend({
            templateName:'breadcrumb-item',
            tagName: 'li'
        })
    })
});

Templates

In my application-template I output the breadcrumbsview above the outlet

{{view view.BreadcrumbsView}}
{{outlet}}

I'm using Twitter Bootstrap so my markup for my breadcrumbs-template is

<ul class="breadcrumb">
  {{#each item in controller.content}}
      {{view view.BreadcrumbItemView itemBinding="item"}}
  {{/each}}
</ul>

The breadcrumb-item-template

<a href="#" {{action gotoRoute item on="click" target="view.parentView"}}>
  {{item.displayText}}
</a> <span class="divider">/</span>

Routing

We need to respond to the routing in our app to update the breadcrumbs.

When my SitesRoute (or any other toplevel route) is activated, we push the NavItem to the Breadcrumbs, but I also want to do that with the rest of my toplevel routes, so I first create a TopRoute

App.TopRoute = Em.Route.extend({
    activate: function() {
        this.controllerFor('menu').setActiveModule(this.get('routeName'));
        var menuItem = app.menuItems.findProperty('routeName',this.get('routeName'));
        this.controllerFor('breadcrumbs').get('content').pushObject(menuItem);
    },
    deactivate: function() {
        var menuItem = app.menuItems.findProperty('routeName',this.get('routeName'));
        this.controllerFor('breadcrumbs').get('content').removeObject(menuItem);
    }
});

All my toproutes extend from this route, so the breadcrumbs are automatically updatet

App.SitesRoute = App.TopRoute.extend();

For deeper levels it works almost the same, all you have to do is use the activate and deactivate hooks to push/remove objects from the Breadcrumbs

App.SiteRoute = Em.Route.extend({
    activate: function() {
        var site = this.modelFor('site');
        this.controllerFor('breadcrumbs').get('content').pushObject(app.NavItem.create({
            displayText: site.get('name'),
            routePath: 'site',
            routeName: this.get('routeName')
        }));
    },
    deactivate: function() {
        var site = this.modelFor('site');
        this.controllerFor('breadcrumbs').get('content').removeAt(1);   
    }
});
Willem de Wit
  • 8,604
  • 9
  • 57
  • 90