Model-based sidebars in Ember

Imagine an application that manages a book collection. A sidebar is shown with a list of authors on the left hand-side. When an author is selected, the main content lists the books by that author.

screenshot

How would you implement this using Ember? At first I tried using named outlets, but it quickly became convoluted: all routes with a sidebar needed to return multiple models, and link helpers were forced to pass id's instead of model instances.

Luckily I stumbled upon some great advice in Nested Routes = Nested UI by Chris Ball (emphasis my own):

When deciding to nest routes, think of the Router as describing your interface rather than a URL structure.

In Ember, a nested route is the most succinct way to implement a model-based sidebar.

Routes

Nested routes are used to display one template inside of another. When the route is rendered, it starts at the top-most route, working its way down; each nested route is rendered into the {{outlet}} of its parent route's template.

By creating a top-level sidebar route, all routes that require the sidebar can be nested routes. In our book collection manager, the routes look like this:

import Ember from 'ember';  
import config from './config/environment';

var Router = Ember.Router.extend({  
  location: config.locationType
});

Router.map(function() {  
  this.route('authors', {path: '/authors'}, function() {
      this.route('books', {path: '/:author_id/books'});
  });
});

export default Router;  

The authors route is responsible for loading the authors list which will be displayed in the sidebar. The nested books route then displays the books for the selected author.

Because we have two separate routes, the model hook for the sidebar only needs to load the authors:

import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        return this.store.findAll('author');
    }
});

The model hook for the books route can be left empty because the author's book list will be side-loaded via the model in the template.

Models

The application will have two models: author and book:

app/models/author.js

import DS from 'ember-data';

export default DS.Model.extend({  
    name: DS.attr('string'),
    books: DS.hasMany('book', {
        async: true,
    }),
});

app/models/book.js

import DS from 'ember-data';

export default DS.Model.extend({  
    title: DS.attr('string'),
    author: DS.belongsTo('author'),
});

To ensure that an author's books get side loaded when accessed from the template, the books relationship on the Author model has to be declared asynchronous. If you don't do this, Ember will raise an error. If you don't want to do this, the authors route would need to load all the books for each author in one go.

Templates

Positioning the authors list to the side of the books list can easily be achieved using CSS. In this case, I've used the bootstrap framework to make it even easier.

app/templates/authors.hbs

<div class="row">  
    <div class="col-sm-3 col-md-2">
        <div class="list-group">
            {{#each model as |author|}}
                {{#link-to "authors.books" author class="list-group-item"}}
                    {{author.name}}
                {{/link-to}}
            {{/each}}
        </div>
    </div>
    <div class="col-sm-9 col-md-10">
        {{outlet}}
    </div>
</div>  

app/templates/authors/books.hbs

<table class="table table-striped">  
    <thead>
        <tr>
            <th>Title</th>
        </tr>
    </thead>
    <tbody>
        {{#each model.books as |book|}}
            <tr>
                <td>{{book.title}}</td>
            </tr>
        {{/each}}
    </tbody>
</table>  

It's tempting to shy away from the nested route approach because a sidebar feels more like a minor part of an existing template; however, routes are really just a way of describing the user interface, and nested routes naturally correlate to a nested ui.

A complete working example of the code can be found on GitHub in the repository jonblack/bookworm under the tag model-based-sidebar.

Author image
Creator of Humble Coder and serial hobbyist