Javascript
Ember Routing: The When and Why of Nesting
Understanding the proper use of the Router in an Ember.js app is a fundamental core concept of the framework. The docs tell us that the router translates a URL into a series of nested templates and almost all available tutorials show how to nest routes as part of an explanation of how the Router works. This is great when you want a UI where a list of items are present at the same time a single item is shown. This leaves many beginners struggling, however, as they try and replace the entire contents of a page with another route's template. Today we'll explore two ways to tackle this problem.
Getting started
First, let's start off with a perfunctory application template.
<script type="text/x-handlebars">
<h1>My Ember App</h1>
</script>
var App = Ember.Application.create();
JS Bin example: http://jsbin.com/alewof/2/edit
Our application is instantiated with one line to Ember.Application.create(); When Ember encounters a Handlebars template without a data-template-name attribute, it will use it by default as the template for the index route and consider it's data-template-name as 'application' if an 'application' template is otherwise not found. Thus Ember will create an app and render this template with little code. It can be helpful to consider this template the application layout.
Adding a Products Route, Model and Template
<script type="text/x-handlebars">
<h1>My Ember App</h1>
{{ outlet }}
</script>
<script type="text/x-handlebars" data-template-name="products">
<h2>Products</h2>
{{#each controller}}
{{ title }}
{{/each}}
</script>
var App = Ember.Application.create();
App.Router.map(function() {
this.resource('products');
});
App.Store = DS.Store.extend({
revision: 13,
adapter: 'DS.FixtureAdapter'
});
App.Product = DS.Model.extend({
title: DS.attr('string')
});
App.Product.FIXTURES = [
{ id: 1, title: 'Rube Goldberg Breakfast-o-Matic' }
];
App.ProductsRoute = Ember.Route.extend({
model: function() {
return App.Product.find();
}
});
This is quite a bit of code at once but if you're acquainted with Ember basics it should look familiar. We've defined an {{ outlet }} in our application template where new templates will render. We've added a 'products' template that iterates the products available in the controller and lists their titles. We define 'products' as a route within App.Router and in our ProductsRoute we set the model property on the route to return all products. The rest of this code is our Product model, fixtures, and establishing our App.Store with ember-data. Additionally, we can automatically redirect to the 'products' route from the root by defining an IndexRoute.
App.IndexRoute = Ember.Route.extend({
redirect: function() {
this.transitionTo('products');
}
});
Now when we load our app the products route is activated and we should see the products template rendered.
JS Bin example: http://jsbin.com/alewof/3/edit
Nesting Routes
As most tutorials available show, we can nest our routes as follows:
App.Router.map(function() {
this.resource('products', function() {
this.resource('product', { 'path' : '/:product_id' });
});
});
This will look very familiar to Rails developers, and in my opinion this is part of the rub. It seems very natural to nest this resource because the URLs implied match the notion of REST that most Rails developers recognize.
From here we can add links to individual products in our products template:
<script type="text/x-handlebars" data-template-name="products">
<h2>Products</h2>
{{#each controller}}
{{#linkTo 'product' this}}{{ title }}{{/linkTo}}
{{/each}}
</script>
And correspondingly we can add a minimal product template like so:
<script type="text/x-handlebars" data-template-name="product">
<h2>Product: {{ title }}</h2>
</script>
For the sake of demonstration, if we want our templates to nest just as our routes do we can add an {{ outlet }} to the bottom of our products template. Then when we click the link in the products template the product template will render inside of it.
<script type="text/x-handlebars" data-template-name="products">
<h2>Products</h2>
{{#each controller}}
{{#linkTo 'product' this}}{{ title }}{{/linkTo}}
{{/each}}
{{ outlet }}
</script>
JS Bin example: http://jsbin.com/alewof/4/edit
We don't want to do this today, though. Let's look at some other Ember methods available to render templates.
Replacing page content with renderTemplate
We can define a renderTemplate method inside of an Ember Route object to handle the specifics of which template the route will render and where:
App.ProductRoute = Ember.Route.extend({
renderTemplate: function() {
this.render('product', { into: 'application' });
}
});
JS Bin example: http://jsbin.com/alewof/5/edit
Our new function is calling out to render the product template inside of 'application'. Remember that our Handlebars template without the data-type-name attribute is by default treated as 'application'. Now our product template is replacing the products template. Note that this happens even if you still have an added {{ outlet }} to the bottom of the products template.
This seems like an innocuous and easy change to get the behavior we want, but we very subtly departed from the 'Ember Way' without realizing it. First, however, we've introduced a bug.
Fixing the empty products template bug
If you are on the products page and click the product link all works as expected. However, if you press the back button on your browser, (you can replicate this in the JS Bin example by pressing backspace or delete) you end up with an empty area where you expect the products template to render. Before the explanation, let's look at the code to fix this:
App.ProductsIndexRoute = App.ProductsRoute = Ember.Route.extend({
renderTemplate: function() {
this.render('products', { into: 'application' });
},
model: function() {
return App.Product.find();
}
});
JS Bin example: http://jsbin.com/alewof/6/edit
This should hopefully trigger some alarms that you've strayed from the path. Now in our ProductsRoute we need to add almost the same code to ensure the template is rendered where we want. Additionally and even worse is that we need to define a ProductsIndexRoute which we previously didn't even care about. Why is this?
The reason for this behavior illuminates a fundamental difference in the way a Rails developer approaching Ember must look at nested routes. In Ember routes are very much tied to how templates will render on the page and not just constrained to URL construction and interpretation.
In Ember when you designate that a route is nested, you are essentially confirming that the child route and the parent route will render at the same time, on the same page. This is a default behavior of nested routes. Remember that we're talking about routes and not necessarily URLs.
In the case above we have defined our routes in a nested fashion, and then strong-armed Ember away from its defaults by forcing the product template to render over its parent. When we click the back button on our browser, Ember expects that we are simply returning to a route which it believes has already rendered. Thus as far as your Ember app is concerned, the content you expect to see should already be there.
To make matters even more complex, when you supply a function as an argument to a resource in your route (nesting them), an index route is created in memory and its template will be rendered after the products route. Therefore when we go back from the product route Ember considered the products index the former route, and then the products route. For this reason if they are all to render into the application template they must all implement the same renderTemplate code.
Whether this seems complex or not, rendering over the parent with renderTemplate is going against the grain. Let's look at something a little more idiomatic.
The Ember Way
To correct this error, we need to go back to where it began, in the Router.
App.Router.map(function() {
this.resource('products');
this.resource('product', { 'path' : 'products/:product_id' });
});
This may seem weird at first because we expect product to be nested under products from a URL routing standpoint, but here they are declared flatly beside each other. We can maintain the URL structure we expect by passing the appropriate value in for our 'path'.
We can also now remove the renderTemplate method from our ProductsRoute:
App.ProductsRoute = Ember.Route.extend({
model: function() {
return App.Product.find();
}
});
We've also removed the declaration of the ProductsIndexRoute from this code as well. Another object that's ripe for the chopping block is the ProductRoute. Once you remove the renderTemplate code from the ProductRoute you aren't left with anything expect for declaring the ProductRoute's existence which Ember will take care of for you by default.
This is significantly more straightforward, and feels more in line with Ember idioms.
JS Bin example: http://jsbin.com/alewof/7/edit
Routing revisited
Ember's Router largely represents the state of your application in a way that is both more firmly established and enforced than any other Javascript MVC framework that I have used. This is a good thing, but it requires a little adjustment in your thinking if you're coming from other frameworks.
Plan your routes according to your UI. If a route replaces another, it should should be represented at the same level in your router. If a route is to render on the same page as another route within the {{ outlet }} in its template, then you should nest that route in your Router.
Ember is doing a lot for you. If something feels tricky, take a step back and make sure you're respecting the defaults.