Skip to content

Instantly share code, notes, and snippets.

@juarezpaf
Forked from tomdale/gist:3981133
Created August 15, 2013 13:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save juarezpaf/6240940 to your computer and use it in GitHub Desktop.
Save juarezpaf/6240940 to your computer and use it in GitHub Desktop.

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.

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