Ember.js Nested Routing with Multiple Outlets

By Dan Maglasang, Tech Manager of Web Applications at FiscalNote

Starting out with Ember.js is a lot like starting out with Rails — there is a plethora of ways to achieve one thing. This is a good thing and a bad thing. It’s good that the framework is flexible enough to support multiple and solid ways of implementing a feature, however, with a large team, a convention needs to be established for maintainability and overall sane-ness. So why not stick with the framework’s conventions?

With Ember.js (and more specifically, Ember CLI), we are given a solid, well thought-out set of conventions. In this article I’ll walk through Ember.js routing — specifically nested routing with multiple outlets on the same page.

See the finished app before we start: Demo

Technologies Used

  • Ember.js — v. 1.10.0-beta.2 (HTMLbars!!!)
  • Ember Data — v. 1.0.0-beta.14
  • Ember CLI — v. 0.1.5

Setup

To get our environment setup, we should initially have Node.js to install our various dependencies.

On a Mac with Homebrew installed, it’s as simple as

brew install node

Now install Ember-CLI

npm install -g ember-cli

Next, we’ll install Bower — Ember-CLI uses it as its package manager

npm install -g bower

Getting Started

Now that we have our environment set up, change your current directory to where you’ll be storing your files. I like to keep a folder called Dev in my home directory

cd ~/Dev

Now intialize a new Ember CLI app

ember new your-app-name
cd your-app-name

At this point you can now simply run

ember server

to start a new server located at http://localhost:4200/

Source up to this point: https://github.com/dorilla/ember-routing-tutorial/releases/tag/start-here

Some More Setup

Now we have to setup our app to receive data from some external API source. For the purposes of demonstration, we are going to use Fixtures as a way to fake data coming from an API (in a production app, you’ll have a full blown API service written in some framework like Rails or Node.js). You can read more about Fixtures here.

Create a folder named adapters in the /app directory. Then create a file named application.js with the following contents:

import DS from 'ember-data';

export default DS.FixtureAdapter.extend({
// Adds a fake latency timer to see realistic transitions between routes (time between AJAX requests)
latency: 500
});

This file tells Ember Data to use the FixtureAdapter to resolve interactions with the API.

The CSS we will be using in this app

// more in Git source
.master {
float: left;
width: 30%;
font-size: 20pt;
}
.master a {
display: block;
text-decoration: none;
}
.master a:hover {
text-decoration: underline;
}
.master a.active::before {
content: " » ";
}
.detail {
float: left;
width: 70%;
font-size: 14pt;
}

Meat and Potatoes

Our app will have one model: Users. Let’s now define this model in our app.

Create a file /app/models/user.js

import DS from "ember-data";
// define the User model
var User = DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
bio: DS.attr('string')
});
// create User fixtures
// this is what the FixtureAdapter uses as the API source
User.reopenClass({
FIXTURES: [
{id: 1, firstName: 'Steve', lastName: 'Jobs', bio: "Jobs' Bio here"},
{id: 2, firstName: 'Jony', lastName: 'Ive', bio: "Ive's Bio Here"}
]
});
export default User;

Our final page structure will look like this

-----------------------------------------------------
| Users | User Show Route |
| Index | |
| Route | |
| | |
| | |
| | |
| | |
-----------------------------------------------------

A typical Master-Detail page.

The Index Route will list out all the Users and the User Show Route will show a more detailed view of a single user.

We will first work on the Users Index Route

Users Index Route

In /app/router.js, add a new route

...
Router.map(function() {  
this.route('users', function() {});
});
...

You can read more about routing here, but the most important thing you must realize is that this creates a path /users. Ember CLI then expects a UsersTemplate to be created in order to resolve (a UsersController and a UsersRoute are optional — it is implicitly created).

So create the UsersTemplate /app/templates/users.hbs

<h1>Users</h1>  
<h5>View users in detail</h5>
<hr>  
{{outlet}}

Everything nested under the /users route will be rendered into the outlet.

Now we will create the UsersIndexRoute, which will display the list of users.

We will override the Route method model to make the AJAX call to grab users. Create a new directory /app/routes/users and a new file under it /app/routes/users/index.js

import Ember from 'ember';
export default Ember.Route.extend({
  model: function() {
// use Ember Data to make an AJAX call to grab users
return this.store.find('user');
},
});

Now that we have the users, we can now display them in the template /app/templates/users/index.hbs

{{#each model}}
<div>{{firstName}} {{lastName}}</div>
{{/each}}

Open http://localhost:4200/users in your browser and you should see something like this:

Source up to this point: https://github.com/dorilla/ember-routing-tutorial/releases/tag/users-index-route

In preparation of multiple outlets, we will make a modification to how the UsersIndexRoute renders.

Modify /app/templates/users.hbs

<h1>Users</h1>  
<h5>View users in detail</h5>
<hr>
<div class="master">  
<div class="inner">{{outlet 'master'}}</div>
</div>
<div class="detail">  
<div class="inner">{{outlet 'detail'}}</div>
</div>

As you can see we are adding the master-detail layout here. We are now going to name the existing outlet master

Now modify /app/routes/users/index.js to render the template into the new named route

...
  renderTemplate: function() {
this.render({
outlet: 'master',
});
},
...

Not much to look at yet — just a master list of users. Now we will add the UsersShowRoute and add links to the list that direct to the respective users.

Users Show Route

First we will update /app/router.js

...
Router.map(function() {  
this.route('users', function() {
this.route('index', { path: ''}, function() {
this.route('show', { path: ':id' });
});
});
});
...

This nests /users/:id under the index Route, ensuring that the master list will always show as we move between show routes.

Next up is creating the UsersShowRoute /app/routes/users/index/show.js

import Ember from 'ember';
export default Ember.Route.extend({
  renderTemplate: function() {
this.render({
outlet: 'detail',
});
},
  model: function(params) {
// using `fetch` instead of the usual `find` method
// to always make a call to the API
// regardless of the current store
return this.store.fetch('user', params.id);
},
});

Then we create the template /app/templates/users/index/show.hbs

<strong>{{firstName}} {{lastName}}</strong>  
<hr>
{{bio}}

Now create the links to the show pages in the master list. Replace contents of the file /app/templates/users/index.hbs

{{#each model}}
{{#link-to 'users.index.show' this.id}}{{firstName}} {{lastName}}{{/link-to}}
{{/each}}

At this point we can seamlessly move between nested show routes. This is how it should look:

Source up to this point: https://github.com/dorilla/ember-routing-tutorial/releases/tag/users-show-route

Reload the page on a show route and the master list will be loaded along side the detail view. Increase the FixtureAdapter latency to see the transitions clearly.

Conclusion

And that’s it! We have completed the tutorial. As you can see, if you follow Ember’s conventions, it takes very minimal effort and lines of code to implement a Master-Detail page.

Bonus

Loading substates

Learn more about Loading Substates here.

We will utilize Loading Substates to better visualize the transitions between routes. Right now, since we have set a latency to our API requests, we can see a slight delay in the rendering. It would be better as a user to see a loading substate in place of any other content. Ember gives us a pretty solid convention for this. We simply need to define the routes for both the index route and the show route to tell Ember to use our named outlets.

First create the loading template /app/templates/loading.hbs

Loading...

Create /app/routes/users/loading.js

import Ember from 'ember';
export default Ember.Route.extend({
  renderTemplate: function() {
this.render('loading', { // the loading template
outlet: 'master', // place into the 'master' outlet
});
}
});

Create /app/routes/users/index/loading.js

import Ember from 'ember';
export default Ember.Route.extend({
  renderTemplate: function() {
this.render('loading', { // the loading template
outlet: 'detail', // place into the 'detail' outlet
});
}
});

Source up to this point: https://github.com/dorilla/ember-routing-tutorial/releases/tag/loading-substates

Final product: Demo


Notes and Acknowledgements

This is very similar to how Ember101 implements the master-detail pagehere. However, I found that this didn’t quite get it to where I wanted it to be. I wanted two outlets (a master and a detail) that contain separate loading substates. In this way, we do not have to wait for the index (master) list to resolve before rendering anything on the page.