#Introduction
This gist walks through some best-practices for intermediate routing architecture, augmenting the information already covered in the guides. It will cover:
- Nested routes and outlet placement
- Resuing templates for multiple routes
- Normalising routes
By the end of the gist, you should be able to make informed decisions about how to structure your routes and their templates. This guide is not meant to provide all the answers, but to help you understand the tools available to you for architecting your app.
Routing often feels magical to newcomers to Ember because the link between each route and what is rendered is not the most obvious. Ths most important thing to note, is that Index Routes come for free the moment you specify child routes at that level.
From the guides:
For example, if you write a simple router like this:
Router.map(function() {
this.route('favorites');
});
It is the equivalent of:
Router.map(function() {
this.route('index', { path: '/' });
this.route('favorites');
});
At this point, you will have access to 2 routes: index
and favorites
. Now, if you pass a function defining any child routes, you'll automagically also get an index
child route, resulting in 3 accessible routes:
Router.map(function() {
this.route('index', { path: '/' });
this.route('favorites', function () {
//this.route('index', { path: '/' }); This comes for free!
this.route('new', { path: '/new' });
});
});
This means that when you go to /favorites
, the route you're actually hitting is favorites.index
. If you do not specify any child routes in favorites
, then the favorites
route will be a leaf route and there will not be, by default, a favorite.index
route.
Now that we know what route we're going to, how do we know what actually gets rendered in the page?
The guides explain that "Each template will be rendered into the {{outlet}}
of its parent route's template". I've found it easier to understand it as the template for each child route is rendered into the {{outlet}}
helper of the template for that route;
Given the above route map, when you go to /
in your browser, Ember's router will route you to the index
route, which renders the templates/application.hbs
template.
When you go to /favorites
in your browser, Ember's router will route you to the favorites.index
route, which renders:
templates/application.hbs
, which, if it has an{{outlet}}
, will render into that{{outlet}}
:templates/favorites.hbs
, which, if it has an{{outlet}}
, will render into that{{outlet}}
:templates/favorites/index.hbs
, and so forth.
If you don't have templates/favorites/index.hbs
, Ember will render the default template into the {{outlet}}
in templates/favorites.hbs
. And guess what, the default template is {{outlet}}
.
At the same time, if templates/favorites.hbs
does not contain an {{outlet}}
, templates/favorites/index.hbs
(or any templates for child routes for that matter) will simply not be rendered.
With this in mind, it should now be easier to rationalise which parts of your app are shared across child routes and which are child-specific. For example, if you have multiple child routes such as favorites.index
, favorite.edit
and favorite.new
, and there are Components that don't change across those children (navigation and filters come to mind), you'll want to put them in the favorite.hbs
template so that as you navigate across the children routes, the shared Component does not need to re-render each time.
You'll notice that I'm very careful to say each that there are templates for a route as oppose to each route having a template. This is because it is not always the case that there's a 1-to-1 mapping between templates/routeName.hbs
and routes/routeName.js
. While the Ember Resolver will start look for a template that corresponds to the route's name, there are cases in which you might not want the template in that path.
Let's extend our route map to showcase such an instance:
Router.map(function() {
this.route('index', { path: '/' });
this.route('favorites', function () {
this.route('new', { path: '/new' });
this.route('edit', { path: '/:favorite_id' });
});
});
Now, we have a means of editing a favorite, and creating a new one. In many apps, the UIs for doing both of these actions are similar (or even the same), and we don't want to create duplicate templates when one is sufficient.
Here, we can define templates/favorites/edit.hbs
that comprises our forms for editing an existing model, and re-use that template for the favorites.new
route using the renderTemplate hook:
// routes/favorites/edit.js
import Ember from 'ember';
export default Ember.Route.extend({
model(params){
return this.store.findRecord('favorite', pararms.favorite_id);
}
});
// routes/favorites/new.js
import Ember from 'ember';
export default Ember.Route.extend({
model(){
return this.store.createRecord('favorite');
},
renderTemplate(controller, model) {
this.render('favorites.edit', {
model
});
}
});
Doing this, however, is an indication that the templates/favorites/edit.hbs
should probably become a Component that you can reuse. You can then simply drop this Component into your templates/favorites/edit.hbs
and templates/favorites/new.hbs
files. The main benefit of extracting this out into a Component is that you are declaring that it is reusable (and thus reused).
Calling this.render
with a template that is named differently from the route also establishes an implicit dependency: if one day the favorites.edit
route is deprecated, it's not immediately obvious that the favorite.new
route requires the favorite/edit.hbs
to be present for rendering. Using a Component here then removes this implicit dependency as a potential source of failure.
The guides briefly cover the use case in which you may have multiple sources of data that you'll want to render your templates with. By simply returning an RSVP.hash
, {{model}}
in your template thus becomes a simple hash of the properties that you pass into the RSVP.hash.
It is important that you are aware that this comes with its own tradeoffs. Specifically recall that when using the {{link-to}}
helper, the model hook does not run if you supply an object an as argument for the desired route.
If the edit route is now
// routes/favorites/edit.js
import Ember from 'ember';
export default Ember.Route.extend({
model(params){
return Ember.RSVP.hash({
favorite: this.store.findRecord('favorite', pararms.favorite_id),
pokemon: this.store.findAll('pokemon')
});
}
});
Then, clicking on a link generated with {{link-to 'favorites.edit' 123
}} will bring you to the favorites.edit
route, which will render templates/favorites/edit
with a {{model}}
value of
{
favorite: {
id: 123,
attribute1: 'value1',
...
},
pokemon: [DS.Model...]
}
That is, {[link-to 'favorites.edit' 123}}
will cause the model()
hook to run with favorite_id
having the value of 123
.
However, if you're passing a model object into the helper, clicking the link {{link-to 'favorites.edit myModel}}
will still bring you to the favorites.edit
route, but the route will bypass the model()
hook and simply render templates/favorites/edit
with a {{model}}
value of myModel
. If myModel
does not have the same shape as the template expects (i.e. it doesn't have the favorite
and pokemon
fields with the right data types / model types), your template will not render as expected.
My advice is for any route that expects a param to pivot on, its model()
hook should return exactly what the route purpots to return. favorite/123
should always return in its model()
hook, then, a single favorite
model and nothing else. This way, it is obvious what is being passed into the template via the {{model}}
helper.
For data that you might want to fetch outside of the model proper, consider either loading it one level above in the parent route, if appropriate, or use the beforeModel()
and afterModel()
hooks. Since the store is a singleton Service, you can retrieve those values later in your controller or Components for rendering. If you prefer the performance gain of firing off multiple async calls for data at once, you can still use RSVP.hash
as long as you resolve the promise to the sole model that your template expects:
// routes/favorites/edit.js
import Ember from 'ember';
export default Ember.Route.extend({
model(params){
return Ember.RSVP.hash({
favorite: this.store.findRecord('favorite', pararms.favorite_id),
pokemon: this.store.findAll('pokemon')
}).then(hash => {
if (hash.favorite.state === 'reject') {
throw new Error(`Could not find a favorite with the id ${params.favorite_id}`);
}
return hash.favorite.value;
});
}
});
That said, all of this is my opinion, and you can choose to follow or ignore it as long as you know what you are doing. If you prefer to construct your own object hash to pass into a {{link-to}}
helper so that your route's model()
hook can return an RSVP.hash
, know that you'll have to do this everywhere you want to bypass the model hook.