English |  Español |  Français |  Italiano |  Português |  Русский |  Shqip

Developing an Ember.js Edge

4. Ember Applications & URLs

The web began as a way to share documents. URLs identified a canonical, universal address for a document and a link between documents. When Bob shares a document with Mary, he need only copy and paste a URL.

Each re-invention of the web seems to momentarily forget the grace of URLs before re-discovering them. Early PHP, for instance, tied URLs to the layout of source files on disk. API movements like Remote Procedure Call (RPC) attempted to eschew the resource concept almost entirely. Around 2006 the idea of using a domain specific language (DSL) for routing took hold in several server-side frameworks. A DSL separates human-readable URL fragments from application logic, making user-friendly URLs easy.

Ember embraces URLs as an important cornerstone of web applications. The Ember router presents a DSL that helps bridge user-readable URLs with an application-level state machine. As you enter and exit URLs, the router signals to routes that they should configure or cleanup.

To explore the Ember router, we will need to start an Ember application. In this chapter we won't dive deep into applications, but thankfully Ember follows convention over configuration. We can begin with very little code and assume lots of behavior at the start.

A Simple Ember Application

Ember applications begin extremely terse. The community declared war on boilerplate long ago, and given that all dependencies are in place, a single line of code will start your application.

var App = Ember.Application.create();

This application has no complex routes, but does handle the URL "/". In fact, two routes have been created. We can see this if we enable some logging:


var App = Ember.Application.create({
LOG_TRANSITIONS: true,
LOG_TRANSITIONS_INTERNAL: true,
LOG_ACTIVE_GENERATION: true,
LOG_VIEW_LOOKUPS: true
});

// In the console of your browser's development tools:
generated -> route:application > Object
generated -> route:index > Object
generated -> controller:application > Object
Could not find "application" template or view. Nothing will be rendered > Object
generated -> controller:index > Object
Could not find "index" template or view. Nothing will be rendered > Object
Transitioned into 'index'

The first two lines of output show that `route:application` was generated, and then `route:index`.

The application route is a special top-level route in Ember. It is the route that matches the root path of "/" in URLs. The index route is another special route, it specifies the default setup and cleanup if there is no child route. If either of these routes are actually defined, then the class generation is skipped.


var App = Ember.Application.create({
LOG_TRANSITIONS: true,
LOG_TRANSITIONS_INTERNAL: true,
LOG_ACTIVE_GENERATION: true,
LOG_VIEW_LOOKUPS: true
});
App.ApplicationRoute = Ember.Route.extend();

// In the console of your browser's development tools:
generated -> route:index > Object
generated -> controller:application > Object
Could not find "application" template or view. Nothing will be rendered > Object
generated -> controller:index > Object
Could not find "index" template or view. Nothing will be rendered > Object
Transitioned into 'index'

The application route is no longer generated, since it has been defined explicitly.

Generated classes abound in Ember. This simple application is not only generating routes, it is also generating controllers, views, and templates. Only what will be customized must be explicitly defined.

Simple Routes

Ember routing is designed to make nested structure easy. An example application structure might be:


Application
|-- About
|-- Tickets
|-- Ticket
|-- Ticket Receipt

When the about page is shown, the tickets page is not. A specific ticket is displayed alongside the list of tickets, so it must be nested inside the tickets.

The nesting applies to both the interface of an application and the routes. So expressed as an URL engine, the application might look like:


/ (Application)
|-- /about (About)
|-- /tickets/ (Tickets)
|-- /tickets/21 (Ticket)
|-- /tickets/21/receipt (Ticket Receipt)

When a user visits "/about" each layer of nesting has its route executed. First the `ApplicationRoute` is handled, then the `AboutRoute`.

The about route has no children routes of its own, so the route method is used. The tickets path, however, has a child route to show a single ticket and a child route to create a new ticket.

Starting an Ember application to handle these routes is easy:


var App = Ember.Application.create();
App.Router.map(function(){
this.route('about');
this.resource('tickets');
});

Not all of the URLs are implemented yet, but this is a simple beginning. In addition to `ApplicationRoute`, there are now 4 route classes to consider:

PathRoute nameRoute class
/ index IndexRoute
/about about AboutRoute
/tickets tickets TicketsRoute
/tickets tickets.index TicketsIndexRoute

Note the difference between the single class generated for the about route, and the two generated for the tickets resource. As with the omni-present `ApplicationController`, which created `IndexRoute`, the tickets route has a generated index route.

Index routes provide the default route where another may be nested. If no specific child is in the URL, such as when visiting "/tickets", then the `tickets.index` route is used. The application route itself is working this way, defaulting visitors to the index route and allowing the about or tickets paths to override it. 

Dynamic Paths

Routes can contain fragments used to find models. We call these dynamic paths. Here, let's create a route to find a specific ticket:


var App = Ember.Application.create();
App.Router.map(function(){
this.resource('ticket', {path: 'tickets/:ticket_id'});
});

This resource has options passed with an explicit path argument. Implicitly, the path of a route matches its name. The explicit path contains a dynamic segment, the `:ticket_id` part.

So, visiting "/tickets/5" will load the `ticket` route with a params hash of `{ticket_id: 5}`.

Dynamic segments can be named anything, but if the segment ends in `_id` Ember will follow a convention to load that route's model.

  1. Strip `_id` from the end of the segment.
  2. UpperCamelCase the remaining string using Ember.String.classify()
  3. Try to resolve that string as a model class.
  4. Call find() on the class with the params value as an argument.

For example, given a params hash of { ticket_id: 5 } the model for that route will be fetched with App.Ticket.find(5). Later in this chapter we will dive deeper into models and routes.

Nesting Routes in Resources

In the example application described earlier, the following URL was described:

/tickets/21/receipt

To handle this URL a route (receipt) must be nested inside a resource (tickets). The routes to implement this look like:


var App = Ember.Application.create();
App.Router.map(function(){
this.route('about');
this.resource('ticket', {path: 'tickets/:ticket_id'}, function(){
this.route('receipt');
});
});

To revisit the list of generated routes and paths, they would now be:

PathRoute nameRoute class
/ index IndexRoute
/about about AboutRoute
/tickets/21 ticket TicketRoute
/tickets/21 ticket.index TicketIndexRoute
/tickets/21/receipt ticket.receipt TicketReceiptRoute

Nesting a route impacts the URL, first and foremost, but by convention it also decides where a route's template will render (inside its parent). It also will inform how messages from templates (actions) bubble up through different handlers. In Ember, routes dictate a large portion of an application's architecture. This is what we mean when we say Ember applications are driven by URLs.

Resource vs. Route

Ember's own guide has a good tip for choosing between resources and routes.

You should use this.resource for URLs that represent a noun, and this.route for URLs that represent adjectives or verbs modifying those nouns.

Using the resource method implies that a path can have other content nested inside it. Using the route method creates a blunt endpoint, more appropriate for actions modifying a resource (such as "edit"). Often, you will need to pass an argument to the resource method with a dynamic segment.

Simple Templates

Ember's router not only generates route classes, it also generates controllers and templates. Regardless of whether these are generated or explicitly defined, the router wires them together when entering a route. Templates, specifically, are wired to use a feature called outlets. Outlets are a way for templates to nest inside each other.

A simple example of outlets will illustrate the idea quickly.


var App = Ember.Application.create();
App.Router.map(function(){
this.route('new_york');
this.route('san_francisco');
});
Ember.TEMPLATES['application'] = Ember.Handlebars.compile(
'You must love this song: <blockquote>{{outlet}}</blockquote>'
);
Ember.TEMPLATES['new_york'] = Ember.Handlebars.compile(
'New York, New York, a visitor\'s place,<br />' +
'Where no one lives on account of the pace'
);
Ember.TEMPLATES['san_francisco'] = Ember.Handlebars.compile(
'My love waits there in San Francisco<br />' +
'Above the blue and windy sea'
);

Visiting "/new_york" shows the lyrics from Our Town, visiting "/san_francisco" swaps them out for Tony Bennet lyrics. The {{outlet}} helper provides a hook for the rendering of ApplicationRoute's two child routes. Routes nested under a resource work the same way- The resource template provides an {{outlet}} where its children routes can render their templates.

The next chapter will discuss Handlebars and outlets in detail. 

Customizing Routes

To customize the setup or teardown of a route, one of several hooks can be overridden. These hooks control what model is loaded for a route, how the controller is configured, and which templates render where.

The Ember.Route class should be extended for any explicitly defined route. There are several methods that can be overridden as hooks for various points in a route's entrance or exit.

Route lifecycle hooks can be asynchronous, or synchronous. These hooks are asynchronous:

  • beforeModel is called before the model hook, and is intended for rejecting entrance to a route or redirecting away to another route.
  • model is expected to return an object (or a promise resolving to an object) that will be set as the model presented to the template.
  • afterModel is called after the model hook, and can reject proceeding into a route based on the content of the model or fetch additional data.

When any model hook returns a promise, the route is placed in a loading state. After the promise resolves, the route will continue through the other lifecycle hooks. The synchronous hooks on a route handle setup and teardown of the application state:

  • activate is called when a route is first entered.
  • setupController this sets the model on the controller. This default behavior is removed when you override the method.
  • renderTemplate renders a template with a name matching the route into the parent template's outlet. This default behavior is removed when you override the method.
  • serialize formats an object or collection passed as the model into parameters for building the route's URL segment.
  • deactivate is called when exiting a route.

Additionally there are two actions called on a route for lifecycle events:

  • willTransition is called when a route is about to be exited, and has the opportunity to cancel the transition.
  • didTransition is called after a route has been entered.

This is an intimidating list of method names, but recall that Ember strives for good conventions. Often the generated routes with their default behavior is sufficient.

Asynchronous Route Hooks

The synchronous lifecycle hooks of a route must return values immediately. For example, if my ticket route needs to ensure a details panel is open each time the user visits, I can override the `setupController` hook:


var App = Ember.Application.create();
App.Router.map(function(){
this.route('ticket', { path: '/ticket/:ticket_id' });
});
App.TicketRoute = Ember.Route.extend({
setupController: function(controller, model){
// Ensure the default setupController behavior with _super:
this._super.apply(this, arguments);
// Extend the default behavior by setting the detail panel to closed:
controller.set('isDetailPanelOpen', false);
}
});

The model hooks are trickier. Often, fetching a model means making an AJAX request.

To manage the asynchronous behavior of model interactions, the router is built on promises. Promises provide a simple and consistent API for a very common JavaScript challenge.

In JavaScript you can handle asynchronous actions three ways: callbacks, where a function is passed to later called for success or failure (Node.js often uses callbacks); events, where an object emits messages that are listened for by observers, and promises. The promise pattern is that an asynchronous method will return a thennable. Thennables are objects that represent an eventual state of fulfillment (success) or unfulfillment (failure). Good further reading sources on promises include the jQuery deferred documentation, the RSVP.js documentation (RSVP implements promises in Ember), and the Promises/A+ spec.

For example, the jQuery function `getJSON` returns a promise:


$.getJSON('/ticket/1.json').then(function(ticket){
console.log("I've got a golden ticket!", ticket);
}, function(error){
console.error("Oh my!", error);
});

Because Ember's routes use promises to manage asynchronous behavior, any method returning a promise can drop in with little ceremony. Here is a route that fetches its model with `getJSON`:


var App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
model: function(){
return new Ember.RSVP.Promise(function(resolve, reject){
$.getJSON('/tickets.json').then(function(data){
Ember.run(null, resolve, data);
}, function(error){
nbsp; Ember.run(null, reject, error);
});
}
});

This model hook returns a promise. The route will wait for that promise to resolve or reject before moving to setup the controllers or render that route's template. In the interim, the route is placed in a temporary loading state.

It may take a little practice to get familiar with this pattern of returning promises, but it makes working with asynchronous data extremely easy and — in keeping with every pattern we’ve seen in Ember so far — declarative.

Wrapping Up

Here, we’ve covered the essentials of understanding both the router and individual routes in Ember. In the next chapter, we will create basic applications with templates and navigation. How outlets work in detail, and the options for overriding `renderTemplate` will become much more clear.

The router is the conductor of your Ember applications. It coordinates all of the other components, giving them access to each other. To a large extent, it takes on the communication between different controllers, important in complex applications.

There has been error in communication with Booktype server. Not sure right now where is the problem.

You should refresh this page.