Subscribe now

Router, Routes, and Controller Basics [09.09.2016]

Yesterday we installed the Ember Inspector. It will help us better debug and understand our Ember applications. Today, we're going to learn a bit about the Ember router, routes, and controllers. We are going to do this by building a very basic contact app.

Let's get started by creating a new Ember project.

$ ember new contact-manager
$ cd contact-manager

This is a good opportunity to look at some of the generators that Ember CLI gives us. To see them listed type ember generate and pass in the --help flag.

ember generate --help

As you can see, there are a lot of options available to us. The first thing we should do is to create a route for our contacts for that we will use the route generator.

$ ember g route contacts

This command creates three files for us and modifies a fourth. Let's quickly look at what we have. First, the router.js file has been modified. We now have this line:

this.route('contacts');

This line maps the URL in the browser to our route that we just created. this.route() accepts three parameters, the last two are optional.

  • The first, a string, is the name of the route. By default this will also be the name of the URL path.
  • The second parameter is an options hash. One of the options is the path option. If you want your URL path to have a different name than your route you would specify that here.
  • The third parameter is a function containing any nested routes. If we wanted a nested route with a custom path, it would look like this:
this.route('contacts', { path: 'folks' }, function(){
  this.route('contact', { path: ':contact_id' })
});

The colon preceding contact_id in the path denotes a dynamic segment. An example URL for this route would look like: localhost:4200/folks/1. We will come back to that down the road.

Moving on from the router, let's look at our new route. The route, located in app/routes/contacts.js should look like this:

import Ember from 'ember';

export default Ember.Route.extend({
});

The template being rendered by that route lives in app/templates/contacts.hbs Presently, it contains only an empty outlet:

{{outlet}}

The last file that the generator creates for us is tests/unit/routes/contacts-test.js. We will look at testing our Ember application later, but the generated file contains a very basic test that asserts the existence of the route that we just generated. If we start our server and visit http://localhost:4200/contacts right now, it would work and we shouldn't get any errors. In fact, we wont get anything at all! Let's add some markup so we can see that everything is rendering. In app/templates/contacts.hbs let's add a header above the {{outlet}} :

<h2>Contacts</h2>
{{outlet}}

Now, let's start our server, and our header is displaying...but it isn't very interesting. To see how routes and controllers work together, let's add a model to the route. Normally, we would return something from Ember Data -- either an object or an array of objects -- in the route's model hook. Let's save that for later. For now, we'll just add a string to the route so we can see how it works:

export default Ember.Route.extend({
  model(){
    return 'I am from the Route!';
  }
});

The route's model hook sets the return value of the model function as a property on the controller and makes it available in our templates. Now, in our template, we can call the model:

<h2>Contacts</h2>
{{model}}
{{outlet}}

Now, if we look at our site, we get our expected result. Let's replace that string with a more realistic value for a contacts route. We'll create two objects and return them as an array.

export default Ember.Route.extend({
  model(){
    let contact1 = { id: 1, firstName: 'Josh', lastName: 'Adams', email: 'josh@dailydrip.com'};
    let contact2 = { id: 2, firstName: 'Adam', lastName: 'Dill', email: 'adam@dailydrip.com'};

    return [contact1, contact2];
  }
});

Now that we have an array, we will need to iterate over the objects to display their properties. For this we can use Handlebars' #each helper. Change the template to:

<ul>
  {{#each model as |contact|}}
    <li>{{contact.firstName}} {{contact.lastName}}</li>
  {{/each}}
</ul>

Now if we look back at our site, we will see this list rendering.

Let's make one more change, this time on our controller so we can see properties on the controller impacting our display. To this point, since we didn't have anything outside of defaults going on in our controller, we allowed Ember to create a contacts controller instance for us. To be clear, the default behavior here is just attaching the return value of the model hook in the route to the controller as the model property. Now, we want to do some more stuff, so we'll need to generate a contacts controller.

$ ember g controller contacts

Now that we have an instance of the controller we can open it up and make some changes. Let's change our controller to look like this:

export default Ember.Controller.extend({
  queryParams: ['sortBy'],
  sortBy: null,

  sortedContacts: Ember.computed('sortBy', 'model', function() {
      let sortBy   = this.get('sortBy');
      let contacts = this.get('model');

      if (!!sortBy) {
        return contacts.sortBy(sortBy);
      } else {
        return contacts;
      }
  })
});

We have a few new things going on here:

  • First, we have a queryParams property on the controller.
  • Inside that array, we are passing in sortBy which creates a binding for us between the sortBy property on the controller and a sortBy queryParam in the URL.
  • Below that, we see a computed property. A computed property is a function whose return value is cached until the cache is invalidated by a change in any of the keys.

In our case, sortedContacts will update any time the sortBy or model change - but only if we call it. So let's head to our template and change model to sortedContacts.

<ul>
  {{#each sortedContacts as |contact|}}
    <li>{{contact.firstName}} {{contact.lastName}}</li>
  {{/each}}
</ul>

Now, we can revisit our site and add some queryParams to the URL. If we add ?sortBy=firstName we will see our list sorted by first names. Additionally, if we look at our controller in Ember inspector, we can see that our sortBy property is set to firstName. If we change our params to ?sortBy=lastName we can see that our list is now sorted by last names and we can see in the controller that the sortBy property is equal to 'lastName'.

http://locahost:4200/contacts?sortBy=lastName

http://locahost:4200/contacts?sortBy=firstName

Summary

That is it for today. We covered a lot, from the basics of the router and routes to controllers and templates. Although, controllers will not play a large part in the future of Ember, the principles that we discussed today will transfer very well to components moving forward.

Notes

Ember is moving away from controllers and moving towards allowing developers to hook components directly to routes. Presently, controllers in Ember exist as in intermediary for the model which it gets from the associated route, and to handle actions and query params. Soon, they will no longer be necessary. If you want to read more about that transition you can look over this RFC. In the future, you will specify query param defaults on the Route rather than the controller, but for now, the controller is still where queryParams belong.