Adding to Discourse using Ember.js Part 1: Routing and Templates

Feb 27 2013

This article belongs to a series of tutorials about Ember.js and Discourse:


I’ve heard from a lot of people in the process of learning EmberJS that there’s quite a jump from the Getting Started guide to actually contributing to a project like Discourse. I thought it might be useful to step through the creation of a new feature in detail and write out how it was done so others can get an idea of how things fit together. Let’s get started!

Feature Idea: A report of Visits by Day

It would be nice if there was a place in the admin section of Discourse that would show how many visits a forum receives in a day. We are already tracking this in the user_visits table in the database, but there’s no place in the user interface to view it.

Getting started: The URL Structure

When I start a new feature, I find it’s best to begin with its URL and expand outwards from there.

My first inclination would be to access the report at /admin/visits. That would certainly work, but what if we wanted to add more reports in the future? It’s' likely there could be some common code shared between them, so it makes more sense to separate them under a common resource such as /admin/reports/visits.

I now find myself thinking - are there commonalities between all reports that we can advantage of? They all have a title and some tuples of data to display at the very least. We could ask the server for a report of type visits, and it could return an object that the front end would know how to display.

If our URL was /admin/reports/:type (where :type is what identifies the report), for example, that gives us a lot of flexibility and potential for code re-use. Let’s go with that.

The Server Side

Since I’d like to focus this tutorial on the Ember side of things, I’m not going to go into detail about how the Rails portion of this feature is implemented. Let’s establish that if we make an AJAX request to the server at /admin/reports/visits.json we’ll get back a JSON response with the report data to be rendered in the following format:

{
  "report": {
    "type": "visits",
    "title": "Users Visits by Day",
    "xaxis": "Day",
    "yaxis": "Users",
    "data": [
      {"x": "2013-02-01", "y": 7},
      {"x": "2013-02-02", "y": 13},
      {"x": "2013-02-03", "y": 15},
      ...
    ]
  }
}

(If curious to see how the server side component was implemented, you may view the source on github.)

Adding our new Route

The first thing we have to do is tell our Ember app about the new URL. In Ember, this is known as routing.

The Discourse admin section client code is located in its own directory. The first file we’ll need to edit is routes/admin_routes.js. Let’s add a resource with a dynamic segment to match the URL we came up with. We’ll place the following line inside the admin resource declaration.

this.resource('adminReports', { path: '/reports/:type' });

What does the above do? Our EmberJS application now knows that whenever a user navigates to /admin/reports/visits, it needs to invoke the part of the application we’ve called adminReports to handle the request. There is a little magic going on under the hood so this is worth explaining in detail.

One thing EmberJS borrows heavily from Rails is convention over configuration. Instead of asking you to wire up every component manually, the Ember router asks you for a very small piece of information, in this case our adminReports string, and it will use that to look up the classes it needs to respond to the user.

There is a convention in Ember where a resource defined as adminReports will tell Ember to look for a class called Discourse.AdminReportsRoute. If you’ve created this class, Ember will use it when the user visits our URL.

Your Route classes should be short and simple. Ours will start by just rendering a handlebars template that we’ll fill in later with some data.

Let’s create a file in admin/routes called admin_reports_route.js and place the following content into it:

Discourse.AdminReportsRoute = Discourse.Route.extend({
  renderTemplate: function() {
    this.render('admin/templates/reports', {into: 'admin/templates/admin'});
  }
});

This tells Ember to render the template called admin/templates/report into our main admin template. Why into? Every view in our admin section has a simple menu up top that we want to display. We want our template to be rendered inside the template with the menu so the user doesn’t get lost. We’ll start by definining a placeholder template in the file: admin/templates/reports.js.handlebars:

<h1>This will display our report!</h1>

Now when we navigate to /admin/reports/active, we see this:

Basic Ember Template

Retrieving Data from the Server

Our feature would be a lot more exciting if it contacted the server to get some data. To do this, we’ll define a Report model. In Discourse, models are simple classes. Let’s create a file, admin/models/report.js and create a basic model:

Discourse.Report = Discourse.Model.extend({});

Discourse uses plain old jQuery AJAX calls to communicate with the server. Let’s add an AJAX call to find a report by type. We can use Ember’s reopenClass to add a find class method:

Discourse.Report.reopenClass({
  find: function(type) {
    return jQuery.ajax("/admin/reports/" + type, { type: 'GET' }.then(function(result) {
      return Discourse.Report.create(json.report);
    }));
  }
});

This find method makes an AJAX call to /admin/reports/:type on the server side and places the resulting data into an instance of Discourse.Report. Note that since AJAX is asynchronous, the find call will return a promise immediately and the report object will be created only when it finishes in the future.

Let’s now tell our Route to load the model when a user navigates to it. This can be done by implementing the model method. Here’s our new admin_reports_route.js:

Discourse.AdminReportsRoute = Discourse.Route.extend({
  model: function(params) {
    return Discourse.Report.find(params.type);
  },

  renderTemplate: function() {
    this.render('admin/templates/reports', {into: 'admin/templates/admin'});
  }
});

When the model method is defined, Ember will call it when the route is entered to retrieve the model and automatically make it available in our template.

Rendering the template

Let’s add a simple HTML table to display the results in admin/templates/reports.js.handlebars:

<h3>{{title}}</h3>

<table class='table'>
  <tr>
    <th>{{xaxis}}</th>
    <th>{{yaxis}}</th>
  </tr>

  {{#each data}}
    <tr>
      <td>{{x}}</td>
      <td>{{y}}</td>
    </tr>
  {{/each}}
</table>

Ember uses Handlebars to render its templates. The really cool thing about this is if the properties you’ve put into your template change, Ember is smart enough to update them in the web browser too. You don’t have to write any extra code to make sure the HTML re-renders, it just works!

If you change the title property of your report, Ember is smart enough to only update that part of the HTML. This is known as binding, and it’s at the core of what makes Ember a really useful tool for developing web applications. It allows you to cleanly separate your data from its presentation, and offers you a universal way to access it.

What’s next?

In the next part of this tutorial, I’m going to show how to set up a controller to respond to actions that the user performs in your template. In particular, I’ll show how you can present the same data that your application already has in a different way (a bar chart) without having to contact the server.

All of the above code is now checked in to our github repository, so feel free to browse it.