Skip to content

Instantly share code, notes, and snippets.

@tomdale
Last active November 26, 2019 21:19
Show Gist options
  • Save tomdale/3981133 to your computer and use it in GitHub Desktop.
Save tomdale/3981133 to your computer and use it in GitHub Desktop.
Ember.js Router API v2

WARNING

This gist is outdated! For the most up-to-date information, please see http://emberjs.com/guides/routing/!

It All Starts With Templates

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.

The Homepage

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');
  }
});

Bringing Templates to Life (With Controllers)

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.

Organizing Your Data With Models

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.

Adding Menu Items

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.

@logicalhan
Copy link

@darthdeus I am also running into this problem, it's actually a bit brutal. Since we don't have access to the instance of our application's Router, we also lose our ability to manually use transitionTo, which sucks. @tomdale @wycats if you guys could explain what alternative approach you guys are currently using, I would be eternally grateful.

@zeppelin no problem, I'm glad my limited knowledge was able to be useful.

@logicalhan
Copy link

@darthdeus I think I've figured it out a piece of your problem. You can access the router like so:

App.container.lookup('router:main').router

and you can access the controller like this:

 App.container.lookup('controller:home')

see issue 1646 for more detail.

Copy link

ghost commented Jan 3, 2013

Some questions for @tomdale:

  1. Will Ember.Route get all that event awesomeness (click, dropenter... event callbacks) of Ember.View? (probably I'll have to use an Ember.View named after the route - for example HomeView and HomeRoute, right?)
  2. When will a new version (including this new router) be released?
    Thanks!

@wooandoo
Copy link

wooandoo commented Jan 4, 2013

To avoid the "JSON Parse error: Property name must be a string literal" with the example (test on Safari), the JSON has to be fixed with:

{
  "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" }
  ]
}

The new features seem to be great!

@kavika13
Copy link

kavika13 commented Jan 6, 2013

This write-up and the current docs are painful for someone new to Ember (me). This could be fixed by updating everything to the new build (especially the starter kit), or by creating a jsfiddle for this writeup. Also see: emberjs/ember.js#1685

I think krotkiewicz is right. Why doesn't the controller set itself up? Single Responsibility Principle. I don't think the data fetching code belongs in a router if the data is supposed to live in the controller, and the router already has the responsibility of mapping a URL to a controller. It might be okay if it just pokes the controller and lets it know to fetch data, though that also seems to me like something that should happen by convention rather than having to manually specify it (especially in the router).

Never used SproutCore or Ember previously though so I guess take my peanut gallery commentary for what it is worth :)

@kavika13
Copy link

kavika13 commented Jan 6, 2013

Here are some working jsfiddles:

Note that I also fixed what I perceived to be bugs in the write-up (how to access controller properties), so the code doesn't 100% match the article.

I can't easily find an up-to-date copy of ember-data, and I don't want to build it myself, let alone find somewhere to host it, otherwise I'd make those fiddles as well. If anyone wants to take those over be my guest!

@zooshme
Copy link

zooshme commented Jan 8, 2013

How will multiple outlets work?

@thhermansen
Copy link

So, I have a router like:

App.Router.map (match) ->
  match('/categories').to 'categories', (match) ->
    match('/:category_id').to 'categories/show'
    match('/new').to 'categories/new'

I put the template related to categories/new in templates/categories/new.hbs. It worked out as expected and was rendered out in my application template's outlet. Next thing I wanted was to define the related controller and view classes. What are the naming convention for this - if any? :) I did not get it to work by putting the view class in a App.Categories-namespace like App.Categories.NewView.

Copy link

ghost commented Jan 8, 2013

@myresearchstyle: check out the source code docs. Here is the interesting part:

App.PostRoute = App.Route.extend({
  renderTemplate: function() {
    this.render('myPost', { // the template to render
      into: 'index', // the template to render into
      outlet: 'detail', // the name of the outlet in that template
      controller: 'blogPost' // the controller to use for the template
    });
  }
});

Simply change the outlet parameter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment