17 min read

In this article by Mitchel Kelonye, author of Mastering Ember.js, we will learn URL-based state management in Ember.js, which constitutes routing. Routing enables us to translate different states in our applications into URLs and vice-versa. It is a key concept in Ember.js that enables developers to easily separate application logic. It also enables users to link back to content in the application via the usual HTTP URLs.

(For more resources related to this topic, see here.)

We all know that in traditional web development, every request is linked by a URL that enables the server make a decision on the incoming request. Typical actions include sending back a resource file or JSON payload, redirecting the request to a different resource, or sending back an error response such as in the case of unauthorized access.

Ember.js strives to preserve these ideas in the browser environment by enabling association between these URLs and state of the application. The main component that manages these states is the application router. It is responsible for restoring an application to a state matching the given URL. It also enables the user to navigate between the application’s history as expected. The router is automatically created on application initialization and can be referenced as MyApplicationNamespace.Router. Before we proceed, we will be using the bundled sample to better understand this extremely convenient component. The sample is a simple implementation of the Contacts OS X application as shown in the following screenshot:

Mastering Ember.js

It enables users to add new contacts as well as edit and delete existing ones. For simplicity, we won’t support avatars but that could be an implementation exercise for the reader.

We already mentioned some of the states in which this application can transition into. These states have to be registered in the same way server-side frameworks have URL dispatchers that backend programmers use to map URL patters to views. The article sample already illustrates how these possible states are defined:

 // app.js
var App = Ember.Application.create();
App.Router.map(function() {
this.resource('contacts', function(){
this.route('new');
this.resource('contact', {path: '/:contact_id'}, function(){
this.route('edit');
});
});
this.route('about');
});

Notice that the already instantiated router was referenced as App.Router. Calling its map method gives the application an opportunity to register its possible states. In addition, two other methods are used to classify these states into routes and resources.

Mapping URLs to routes

When defining routes and resources, we are essentially mapping URLs to possible states in our application. As shown in the first code snippet, the router’s map function takes a function as its only argument. Inside this function, we may define a resource using the corresponding method, which takes the following signature:

this.resource(resourceName, options, function);

The first argument specifies the name of the resource and coincidentally, the path to match the request URL. The next argument is optional and holds configurations that we may need to specify as we shall see later. The last one is a function that is used to define the routes of that particular resource. For example, the first defined resource in the samples says, let the contacts resource handle any requests whose URL start with /contacts. It also specifies one route, new, that is used to handle creation of new contacts. Routes on the other hand accept the same arguments for the function argument.

You must be asking yourself, “So how are routes different from resources?” The two are essentially the same, other than the former offers a way to categorize states (routes) that perform actions on a specific entity. We can think of an Ember.js application as tree, composed of a trunk (the router), branches (resources), and leaves (routes). For example, the contact state (a resource) caters for a specific contact. This resource can be displayed in two modes: read and write; hence, the index and edit routes respectively, as shown:

this.resource('contact', {path: '/:contact_id'}, function(){
this.route('index'); // auto defined
this.route('edit');
});

Because Ember.js encourages convention, there are two components of routes and resources that are always autodefined:

  • A default application resource: This is the master resource into which all other resources are defined. We therefore did not need to define it in the router. It’s not mandatory to define resources on every state. For example, our about state is a route because it only needs to display static content to the user. It can however be thought to be a route of the already autodefined application resource.
  • A default index route on every resource: Again, every resource has a default index route. It’s autodefined because an application cannot settle on a resource state. The application therefore uses this route if no other route within this same resource was intended to be used.

Nesting resources

Resources can be nested depending on the architecture of the application. In our case, we need to load contacts in the sidebar before displaying any of them to the user. Therefore, we need to define the contact resource inside the contacts. On the other hand, in an application such as Twitter, it won’t make sense to define a tweet resource embedded inside a tweets resource because an extra overhead will be incurred when a user just wants to view a single tweet linked from an external application.

Understanding the state transition cycle

A request is handled in the same way water travels from the roots (the application), up the trunk, and is eventually lost off leaves. This request we are referring to is a change in the browser location that can be triggered in a number of ways.

Before we proceed into finer details about routes, let’s discuss what happened when the application was first loaded. On boot, a few things happened as outlined:

  • The application first transitioned into the application state, then the index state.
  • Next, the application index route redirected the request to the contacts resource.
  • Our application uses the browsers local storage to store the contacts and so for demoing purposes, the contacts resource populated this store with fixtures (located at fixtures.js).
  • The application then transitioned into the corresponding contacts resource index route, contacts.index.
  • Again, here we made a few decisions based on whether our store contained any data in it. Since we indeed have data, we redirected the application into the contact resource, passing the ID of the first contact along.
  • Just as in the two preceding resources, the application transitioned from this last resource into the corresponding index route, contact.index.

The following figure gives a good view of the preceding state change:

Mastering Ember.js

Configuring the router

The router can be customized in the following ways:

  • Logging state transitions
  • Specifying the root app URL
  • Changing browser location lookup method

During development, it may be necessary to track the states into which the application transitions into. Enabling these logs is as simple as:

var App = Ember.Application.create({
LOG_TRANSITIONS: true
});

As illustrated, we enable the LOG_TRANSITIONS flag when creating the application. If an application is not served at the root of the website domain, then it may be necessary to specify the path name used as in the following example:

App.Router.reopen({
rootURL: '/contacts/'
});

One other modification we may need to make revolves around the techniques Ember.js uses to subscribe to the browser’s location changes. This makes it possible for the router to do its job of transitioning the app into the matched URL state. Two of these methods are as follows:

  • Subscribing to the hashchange event
  • Using the history.pushState API

The default technique used is provided by the HashLocation class documented at http://emberjs.com/api/classes/Ember.HashLocation.html. This means that URL paths are usually prefixed with the hash symbol, for example, /#/contacts/1/edit. The other one is provided by the HistoryLocation class located at http://emberjs.com/api/classes/Ember.HistoryLocation.html. This does not distinguish URLs from the traditional ones and can be enabled as:

App.Router.reopen({
location: 'history'
});

We can also opt to let Ember.js pick which method is best suited for our app with the following code:

App.Router.reopen({
location: 'auto'
});

If we don’t need any of these techniques, we could opt to do so especially when performing tests:

App.Router.reopen({
location: none
});

Specifying a route’s path

We now know that when defining a route or resource, the resource name used also serves as the path the router uses to match request URLs. Sometimes, it may be necessary to specify a different path to use to match states. There are two common reasons that may lead us to do this, the first of which is good for delegating route handling to another route. Although, we have not yet covered route handlers, we already mentioned that our application transitions from the application index route into the contacts.index state. We may however specify that the contacts route handler should manage this path as:

this.resource('contacts', {path: '/'}, function(){
});

Therefore, to specify an alternative path for a route, simply pass the desired route in a hash as the second argument during resource definition. This also applies when defining routes.

The second reason would be when a resource contains dynamic segments. For example, our contact resource handles contacts who should obviously have different URLs linking back to them. Ember.js uses URL pattern matching techniques used by other open source projects such as Ruby on Rails, Sinatra, and Express.js. Therefore, our contact resource should be defined as:

this.resource('contact', {path: '/:contact_id'}, function(){
});

In the preceding snippet, /:contact_id is the dynamic segment that will be replaced by the actual contact’s ID. One thing to note is that nested resources prefix their paths with those of parent resources. Therefore, the contact resource’s full path would be /contacts/:contact_id. It’s also worth noting that the name of the dynamic segment is not mandated and so we could have named the dynamic segment as /:id.

Defining route and resource handlers

Now that we have defined all the possible states that our application can transition into, we need to define handlers to these states. From this point onwards, we will use the terms route and resource handlers interchangeably. A route handler performs the following major functions:

  • Providing data (model) to be used by the current state
  • Specifying the view and/or template to use to render the provided data to the user
  • Redirecting an application away into another state

Before we move into discussing these roles, we need to know that a route handler is defined from the Ember.Route class as:

App.RouteHandlerNameRoute = Ember.Route.extend();

This class is used to define handlers for both resources and routes and therefore, the naming should not be a concern. Just as routes and resources are associated with paths and handlers, they are also associated with controllers, views, and templates using the Ember.js naming conventions. For example, when the application initializes, it enters into the application state and therefore, the following objects are sought:

  • The application route
  • The application controller
  • The application view
  • The application template

In the spirit of do more with reduced boilerplate code, Ember.js autogenerates these objects unless explicitly defined in order to override the default implementations. As another example, if we examine our application, we notice that the contact.edit route has a corresponding App.ContactEditController controller and contact/edit template. We did not need to define its route handler or view. Having seen this example, when referring to routes, we normally separate the resource name from the route name by a period as in the following:

resourceName.routeName

In the case of templates, we may use a period or a forward slash:

resourceName/routeName

The other objects are usually camelized and suffixed by the class name:

ResourcenameRoutenameClassname

For example, the following table shows all the objects used. As mentioned earlier, some are autogenerated.

Route Name Controller Route Handler View Template
 applicationApplicationControllerApplicationRoute  ApplicationViewapplication      
 ApplicationViewapplication  IndexViewindex      
about AboutController  AboutRoute  AboutView about
 contactsContactsControllerContactsRoute  ContactsView  contacts    
 contacts.indexContactsIndexControllerContactsIndexRoute  ContactsIndexViewcontacts/index      
 ContactsIndexViewcontacts/index  ContactsNewRoute  ContactsNewViewcontacts/new    
 contact  ContactController  ContactRoute  ContactView contact
 contact.index  ContactIndexController  ContactIndexRoute  ContactIndexView contact/index
contact.edit  ContactEditController  ContactEditRoute  ContactEditView contact/index

One thing to note is that objects associated with the intermediary application state do not need to carry the suffix; hence, just index or about.

Specifying a route’s model

We mentioned that route handlers provide controllers, the data needed to be displayed by templates. These handlers have a model hook that can be used to provide this data in the following format:

AppNamespace.RouteHandlerName = Ember.Route.extend({
model: function(){
}
})

For instance, the route contacts handler in the sample loads any saved contacts from local storage as:

model: function(){
return App.Contact.find();
}

We have abstracted this logic into our App.Contact model. Notice how we reopen the class in order to define this static method. A static method can only be called by the class of that method and not its instances:

App.Contact.reopenClass({
find: function(id){
return (!!id)
? App.Contact.findOne(id)
: App.Contact.findAll();
},

})

If no arguments are passed to the method, it goes ahead and calls the findAll method, which uses the local storage helper to retrieve the contacts:

findAll: function(){
var contacts = store('contacts') || [];
return contacts.map(function(contact){
return App.Contact.create(contact);
});
}

Because we want to deal with contact objects, we iteratively convert the contents of the loaded contact list. If we examine the corresponding template, contacts, we notice that we were able to populate the sidebar as shown in the following code:

<ul class="nav nav-pills nav-stacked">
{{#each model}}
<li>
{{#link-to "contact.index" this}}{{name}}{{/link-to}}
</li>
{{/each}}
</ul>

Do not worry about the template syntax at this point if you’re new to Ember.js. The important thing to note is that the model was accessed via the model variable. Of course, before that, we check to see if the model has any content in:

{{#if model.length}}
...
{{else}}
<h1>Create contact</h1>
{{/if}}

As we shall see later, if the list was empty, the application would be forced to transition into the contacts.new state, in order for the user to add the first contact as shown in the following screenshot:

Mastering Ember.js

The contact handler is a different case. Remember we mentioned that its path has a dynamic segment that would be passed to the handler. This information is passed to the model hook in an options hash as:

App.ContactRoute = Ember.Route.extend({
model: function(params){
return App.Contact.find(params.contact_id);
},
...
});

Notice that we are able to access the contact’s ID via the contact_id attribute of the hash. This time, the find method calls the findOne static method of the contact’s class, which performs a search for the contact matching the provided ID, as shown in the following code:

findOne: function(id){
var contacts = store('contacts') || [];
var contact = contacts.find(function(contact){
return contact.id == id;
});
if (!contact) return;
return App.Contact.create(contact);
}

Serializing resources

We’ve mentioned that Ember.js supports content to be linked back externally. Internally, Ember.js simplifies creating these links in templates. In our sample application, when the user selects a contact, the application transitions into the contact.index state, passing his/her ID along. This is possible through the use of the link-to handlebars expression:

{{#link-to "contact.index" this}}{{name}}{{/link-to}}

The important thing to note is that this expression enables us to construct a link that points to the said resource by passing the resource name and the affected model. The destination resource or route handler is responsible for yielding this path constituting serialization. To serialize a resource, we need to override the matching serialize hook as in the contact handler case shown in the following code:

App.ContactRoute = Ember.Route.extend({
...
serialize: function(model, params){
var data = {}
data[params[0]] = Ember.get(model, 'id');
return data;
}
});

Serialization means that the hook is supposed to return the values of all the specified segments. It receives two arguments, the first of which is the affected resource and the second is an array of all the specified segments during the resource definition. In our case, we only had one and so we returned the required hash that resembled the following code:

{contact_id: 1}

If we, for example, defined a resource with multiple segments like the following code:

this.resource(
'book',
{path: '/name/:name/:publish_year'},
function(){
}
);

The serialization hook would need to return something close to:

{
name: 'jon+doe',
publish_year: '1990'
}

Asynchronous routing

In actual apps, we would often need to load the model data in an asynchronous fashion. There are various approaches that can be used to deliver this kind of data. The most robust way to load asynchronous data is through use of promises. Promises are objects whose unknown value can be set at a later point in time. It is very easy to create promises in Ember.js. For example, if our contacts were located in a remote resource, we could use jQuery to load them as:

App.ContactsRoute = Ember.Route.extend({
model: function(params){
return Ember.$.getJSON('/contacts');
}
});

jQuery’s HTTP utilities also return promises that Ember.js can consume. As a by the way, jQuery can also be referenced as Ember.$ in an Ember.js application. In the preceding snippet, once data is loaded, Ember.js would set it as the model of the resource. However, one thing is missing. We require that the loaded data be converted to the defined contact model as shown in the following little modification:

App.ContactsRoute = Ember.Route.extend({
model: function(params){
var promise = Ember
.Object
.createWithMixins(Ember.DeferredMixin);
Ember
.$
.getJSON('/contacts')
.then(reject, resolve);
function resolve(contacts){
contacts = contacts.map(function(contact){
return App.Contact.create(contact);
});
promise.resolve(contacts)
}
function reject(res){
var err = new Error(res.responseText);
promise.reject(err);
}
return promise;
}
});

We first create the promise, kick off the XHR request, and then return the promise while the request is still being processed. Ember.js will resume routing once this promise is rejected or resolved. The XHR call also creates a promise; so, we need to attach to it, the then method which essentially says, invoke the passed resolve or reject function on successful or failed load respectively. The resolve function converts the loaded data and resolves the promise passing the data along thereby resumes routing. If the promise was rejected, the transition fails with an error. We will see how to handle this error in a moment.

Note that there are two other flavors we can use to create promises in Ember.js as shown in the following examples:

var promise = Ember.Deferred.create();
Ember
.$
.getJSON('/contacts')
.then(success, fail);
function success(){
contacts = contacts.map(function(contact){
return App.Contact.create(contact);
});
promise.resolve(contacts)
}
function fail(res){
var err = new Error(res.responseText);
promise.reject(err);
}
return promise;

The second example is as follows:

return new Ember.RSVP.Promise(function(resolve, reject){
Ember
.$
.getJSON('/contacts')
.then(success, fail);
function success(){
contacts = contacts.map(function(contact){
return App.Contact.create(contact);
});
resolve(contacts)
}
function fail(res){
var err = new Error(res.responseText);
reject(err);
}
});

Summary

This article detailed how a browser’s location-based state management is accomplished in Ember.js apps. Also, we accomplished how to create a router, define resources and routes, define a route’s model, and perform a redirect.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here