WARNING
This gist is outdated! For the most up-to-date information, please see http://emberjs.com/guides/routing/!
An Ember application starts with its main template. Put your header, footer, and any other decorative content in application.handlebars
.
<header>
<img src="masthead">
</header>
<footer>
<p>© Joe's Lamprey Shack - All Rights Reserved</p>
</footer>
If you already know CSS, awesome! Ember templates are normal HTML, so your styling skills will come in handy.
You may have noticed something missing in the above template. The template has a header and footer, but no placeholder for the content. Let's fix that:
<header>
<img src="masthead">
</header>
<div class="content">
{{outlet}}
</div>
<footer>
<p>© Joe's Lamprey Shack - All Rights Reserved</p>
</footer>
As your user navigates around the page, you will render templates into this outlet.
Let's start by defining what happens when the user navigates to /
, the application's home page.
App.Router.map(function(match) {
match("/").to("home");
});
App.HomeRoute = Ember.Route.extend({
});
Your application's router defines your application's flows and how to represent those flows in the URL.
The HomeRoute
is a handler that describes what the application should do when the user navigates to the /
route.
You may remember that I added an outlet
to ourapplication
template in the previous section but left you in suspense about how it gets populated.
When you enter a route, the application renders and inserts a template into the outlet. Which template? By default, Ember picks a template based on the name of the route: App.HomeRoute
renders the home
template (App.ShowPostRoute
renders the show_post
template).
<h3>Hours</h3>
<ul>
<li>Monday through Friday: 9am to 5pm</li>
<li>Saturday: Noon to Midnight</li>
<li>Sunday: Noon to 6pm</li>
</ul>
When a user enters the application at /
, they will see the header, the Lamprey Shack's hours from home.handlebars
and the footer.
A route handler has a number of methods you can use to customize the behavior of navigating to a particular path. One common customization is overriding the template to render.
App.HomeRoute = Ember.Route.extend({
renderTemplates: function() {
this.render('homepage');
}
});
So far, I have only used static templates, without any dynamic data. In real life, you will want dynamic data, probably coming from a server, to control your templates.
Ember templates get their dynamic data from Controllers
. A HomeController
provides the data for my home
template.
App.HomeController = Ember.Controller.extend({
// The HomeRoute will populate the hours
hours: null
});
And the updated home.handlebars
:
<h3>Hours</h3>
<ul>
{{#each entry in hours}}
<li>{{entry}}</li>
{{/each}}
</ul>
Where does hours
come from in the template? Its controller! So far, the controller's hours
is null
, so the template will render an empty list.
I'll fix that by implementing a setupControllers
in HomeRoute
:
App.HomeRoute = Ember.Route.extend({
setupControllers: function() {
this.set('controller.hours', [
"Monday through Friday: 9am to 5pm",
"Saturday: Noon to Midnight",
"Sunday: Noon to 6pm"
])
}
});
"How is this any better?" I can hear you saying. "The information is still hardcoded in the application". Ember templates update automatically, so I can change setupControllers
to do its work using Ajax and everything will continue to work.
Take a look.
App.HomeRoute = Ember.Route.extend({
setupControllers: function() {
var self = this;
jQuery.getJSON('/hours').then(function(json) {
self.set('controller.hours', json.hours);
});
}
});
Even though I set the controller's hours asynchronously, the app will continue to work. At first, the home
template will see the empty hours
property, and render an empty list. But Ember templates have a trick up their sleeve: whenever a property referenced in a template changes, the template will automagically update. When the Ajax request returns and the callback sets controller.hours
, the template will update itself with the updated Array.
Astute readers might be thinking that having content pop in seemingly at random is not exactly the best user experience, and they'd be right. Let's add in a spinner while the content loads.
<h3>Hours</h3>
{{#if isLoaded}}
<ul>
{{#each entry in hours}}
<li>{{entry}}</li>
{{/each}}
</ul>
{{else}}
<p class="loading">Loading Hours</p>
{{/if}}
Now, the template will show a loading spinner (with the right CSS) until the controller's isLoaded
property becomes true. I'll update the controller's isLoaded
in the Ajax callback.
App.HomeRoute = Ember.Route.extend({
setupControllers: function() {
var self = this, controller = this.get('controller');
jQuery.getJSON('/hours').then(function(json) {
controller.set('hours', json.hours);
controller.set('isLoaded', true);
});
}
});
And that's it. If you were wondering whether the two calls to set
will trigger two DOM updates, don't fret. Ember defers DOM updates until after JavaScript code has finished running, so you never have to worry about DOM updates smack in the middle of your code.
For simple applications, you can get away with directly using Ajax to populate your controllers and templates. If you're dealing with remote data or need persistence, you'll want to build models to encapsulate the behavior.
I'll refactor the above code to use an Ember model.
App.Hour = DS.Model.extend({
days: DS.attr('string'),
hours: DS.attr('string')
});
Don't be put off by the DS
namespace. Ember's model functionality lives in the DS
namespace (DS stands for "Data Store").
Here, I defined a single model named Hour
with two attributes: days
and hours
. I will use the built-in RESTAdapter
to communicate with the server, which expects a JSON response for a request to "/hours":
{
"hours": [
{ "id": 1, days: "Monday through Friday", hours: "9am to 5pm" },
{ "id": 2, days: "Saturday", hours: "Noon to Midnight" },
{ "id": 3, days: "Sunday", hours: "Noon to 6pm" }
]
}
I will need to make a few tweaks to App.HomeRoute
and home.handlebars
. Let's tackle the route first.
App.HomeRoute = Ember.Route.extend({
setupControllers: function() {
this.set('controller.hours', App.Hour.find());
}
});
The call to App.Hour.find
returns a record array and kicks off a request for the hours to the server. Ember handles the rest of the work I did before. It notifies listeners when the data comes in, and updates an isLoaded
property on the record array once the data arrives.
Next, I'll update the template.
<h3>Hours</h3>
{{#if hours.isLoaded}}
<ul>
{{#each entry in hours}}
<li>{{entry.days}}: {{entry.hours}}</li>
{{/each}}
</ul>
{{else}}
<p class="loading">Loading Hours</p>
{{/if}}
Each entry is now a full-fledged model, so the template pulls the data from the entry's attributes. Also, Ember now provides the isLoaded
property on the record array, so I updated the conditional to use hours.isLoaded
instead of looking for it on the controller.
For our last trick, let's allow the user to navigate from the front page to a special page for each of a list of current specials.
First, I'll create a new model for our menu items.
App.MenuItem = DS.Model.extend({
title: DS.attr('string'),
price: DS.attr('string'),
description: DS.attr('string')
});
I'll need a template for the specials. I'll call it special.handlebars
.
<div class="special">
<h3>{{title}}</h3>
<p class="price">${{price}}</p>
<div class="description">
{{description}}
</div>
</div>
To allow me to look up the properties directly on the controller, I will make the SpecialController
an ObjectController
. An ObjectController
proxies its property lookup to its content
property.
App.SpecialController = Ember.ObjectController.extend({
// SpecialRoute will set the content
});
The router will need a new route for the specials. Because I want it to render the special
template, I'll name it App.SpecialRoute
.
App.Router.map(function(match) {
match("/").to("home");
match("/specials/:menu_item_id").to("special");
});
App.SpecialRoute = Ember.Route.extend({
setupControllers: function(menuItem) {
this.set('controller.content', menuItem);
}
});
There's something different about this route. I don't know its full path ahead of time; the path will differ based on which menu item the user wants to see.
To accomplish this, Ember supports dynamic segments in routes. Dynamic segments start with a :
and represent a model object.
You're probably wondering where the menuItem
parameter to setupControllers
comes from. When you use a dynamic segment ending in _id
, Ember will find the model for you and pass it into setupControllers
. In this case, it will automatically call MenuItem.find(params.menu_item_id)
.
Later, you'll see that without any more code, Ember will also update the URL as your user clicks around. This symmetry—going from URL to model and model to URL—is what makes the Ember router feel so magical.
Two more things.
First, I need to update the HomeRoute
to load specials information from the server.
App.HomeRoute = Ember.Route.extend({
route: "/",
setupControllers: function() {
this.set('controller.hours', App.Hour.find());
this.set('controller.specials', App.MenuItem.find());
}
});
Now, when the user enters the app at /
, the app will load a list of the Lamprey Shack's hours and the current specials.
Finally, I need to update the home
template to show a list of specials and make them link to the special itself.
<h3>Hours</h3>
{{#if hours.isLoaded}}
<ul>
{{#each entry in hours}}
<li>{{entry.days}}: {{entry.hours}}</li>
{{/each}}
</ul>
{{else}}
<p class="loading">Loading Hours</p>
{{/if}}
{{#if specials.isLoaded}}
{{#each menuItem in specials}}
<div class="special">
{{#linkTo 'special' menuItem}}
<img {{bindAttr src="menuItem.image"}}>
{{/linkTo}}
</div>
{{/each}}
{{else}}
<p class="loading">Loading Specials</p>
{{/if}}
Most of this should be very familiar by now. I insert a spinner while the specials are loading. I loop over the list of specials provided by the controller.
There are just two new things here. The {{bindAttr}}
helper allows you to bind an element's attribute to a property path.
More importantly, the {{linkTo}}
helper allows you to create a dynamic link to another named route, passing in a context. In this case, we create a link to SpecialRoute
, passing in the current menuItem
as its context.
Recall the SpecialRoute
.
App.SpecialRoute = Ember.Route.extend({
setupControllers: function(menuItem) {
this.set('controller.content', menuItem);
}
});
When a user clicks on the {{link}}
helper, the router will pass the supplied menuItem
to the setupControllers
method, update the URL, and render the special
template.
Amazingly, if the user saves off the current URL (/specials/12
) and reloads the page, everything will just work, as you'd expect it to.
You can see how a route, controller and template all work together. With just a few lines of code, I have routing, links, asynchronous loading and template rendering all taken care of.
This is just the beginning! In this guide, I walked you through the basics of Ember application architecture. I couldn't cover every feature involved in Ember architecture, but the best thing about Ember is how well these features scale to more routes and more templates.
As your app grows, you may want to override some of Ember's defaults. We built Ember with this in mind, and I would encourage you to dig into the "Read More" links throughout this guide to find out more about how you can get more specific about how to handle model serialization, deserialization, rendering and other aspects of the architecture.
When I do:
The
/posts
route is not recognized anymore, but/posts/special
is working as it should.Am I missing something?