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.

@enyo
Copy link

enyo commented Dec 9, 2012

@mars I think that the new router is only available in the new-router branch. But I'm also very interested in the future of the current router implementation...

@jsmestad
Copy link

This is an HUGE improvement over the current router while giving a bit more flexibility with router/controller code organization and modularity. Not to mention its easier for new developers to mind-map the similarities in the DSL to server-side frameworks conventions like Rails. :shipit:

@arasbm
Copy link

arasbm commented Dec 15, 2012

This guide is really awesome. Thank you!

@arasbm
Copy link

arasbm commented Dec 16, 2012

like @mars and others I am also wondering now, will the connectOutlets method be deprecated soon?

@krotkiewicz
Copy link

@enyo I was thinking that controller is responsible for fetching data from server, why it couldn't setup itself ? Like some init method or setup.

I am python developer and in our frameworks (like Pylons, Pyramid, Django) the router only points to view (or controller in ember vocabulary) that should be use for given URL.

@machty
Copy link

machty commented Dec 19, 2012

fyi, nested routing looks like this:

  Router.map(function(match) {
    match("/").to("root", function(match) {
      match("/").to("home");
      match("/specials/:menu_item_id").to("special");
    });
  });

That said, while I think the new api will be easier for most people to grok, it's still missing out on a crucial feature that allows URL changes to put the app in an invalid state. Have a look here for more info:

emberjs/ember.js#1606

@Nopik
Copy link

Nopik commented Dec 28, 2012

Is this match() called each time url need to be matched? I.e. maybe it is possible to have code like this:

Router.map(function(match) {
  match("/").to("root", function(match) {
    match("/").to("home");
    if( check_if_current_state_allows_routing_to_specials() == true ) {
      match("/specials/:menu_item_id").to("special");
    }
  });
});

That would ease the pain somewhat.

@zeppelin
Copy link

When I do:

Router.map(function(match) {
  match("/").to("home");
  match("/posts").to("posts", function(match) {
    match("/special").to("special");
  });
});

The /posts route is not recognized anymore, but /posts/special is working as it should.

Am I missing something?

@logicalhan
Copy link

@zeppelin, I'm guessing you need to define your index route within the nested posts routes like so:

Router.map(function(match) {
  match("/").to("home");
  match("/posts").to("posts", function(match) {
    match("/").to("postsIndex");
    match("/special").to("special");
  });
});

@SinisterMinister
Copy link

@tomdale Looks like the way you have setupControllers documented is a bit off. It seems you can't access the controller through this.get('controller'). You have to do setupControllers(controller, context) to be able to access it.

I updated the doc here to notate this.

@darthdeus
Copy link

I'm not sure if some setup in my app is wrong, but I am no longer able to get an instance of the router by doing App.get("router"), or instances of controllers by App.get("router.applicationController") etc.

Is this something that also changed?

@zeppelin
Copy link

zeppelin commented Jan 1, 2013

@logical42 Thanks, that makes sense.

@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