Adding to Discourse using Ember.js Part 2: Controllers - Evil Trout's Blog


#1

Ember.Js + Client Side MVC

You might have heard Ember.JS described as a Client-Side MVC Framework, where MVC refers to “Model-View-Controller.” In the first part of our tutorial, we talked about how Ember.js uses convention over configuration to prevent developers from having to create boilerplate and get up and running quickly.

Previously, we created an adminReports resource to handle URLs in the form of /admin/reports/:type. Ember automatically looked for our class called Discourse.AdminReportsRoute. However, that’s not the only thing Ember looked for! It will also find Discourse.AdminReportsController and use that as our controller to handle that URL.

You might be thinking: “We never created an AdminReportsController though! What’s up with that?”

Ember did something very convenient for us. We didn’t define a controller, so it couldn’t find one when it wired things up. Instead, it created one for us and gave it the default behavior. And for our purposes the default behavior was enough, so things just worked and we had to write less code!

Controllers in Ember.JS

Controllers serve a few major purposes in an Ember.js application.

To respond to user interactions: When a user interacts with a template, say by clicking a button, you need to declare a method that will respond to that action. To expose data to your templates: Usually you will be exposing one model or a collection of models to a template. The controller is responsible for making a model’s data available for display in your template. To maintain state outside your models: Sometimes you need to work with data that doesn’t have anything to do with your models. A good example would if a user clicked on a column heading to change the sort order of a table. You could store the current sort order in the controller as a property.

If you’ve used a server side MVC framework such as Rails before, you should know that Ember.js controllers work a little differently. In server side MVC, controllers lose all state between requests. If you set an instance variable in one controller method and then call another in another request, the variable won’t be there!

In an Ember.js application such as Discourse, your controllers stay around. If you set a property in a controller, it will be there as long as the controller is still in use.

The AdminReportsController

How did Ember know what data to expose to our template if we never created a controller? If you recall, in our AdminReportRoute, we had a method called model:

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

When we entered our route, the model function was called and returned a single Discourse.Report object. Ember then inferred that our controller was dealing with a single model (rather than say, an array of models), and created an ObjectController for us.

An ObjectController is very simple. It exposes all the properties of your model as properties on the controller itself. In our template, we were able to say {{title}} and the controller knew to route that property to our model.

Adding controller functionality

The default behaviors of Ember can last you a while. But what if we wanted to add a bar chart display of the data in addition to our tabular display? We’ll have to define our own controller to handle that.

Let’s create the controller Ember needs and put it in admin/controllers/admin_reports_controller.js

Discourse.AdminReportsController = Ember.ObjectController.extend({
 viewMode: 'table',
 // true if we're viewing the table mode
 viewingTable: Ember.computed.equal('viewMode', 'table'),
 // true if we're viewing the bar chart mode
 viewingTable: Ember.computed.equal('viewMode', 'barChart'),
 // Changes the current view mode to 'table'
 actions: {
 viewAsTable: function() {
 this.set('viewMode', 'table');
 },
 // Changes the current view mode to 'barChart'
 viewAsBarChart: function() {
 this.set('viewMode', 'barChart');
 }
 }
});

Let’s do a quick run through. The first thing we do is declare a property called viewMode and set it to be ‘table’.

After that, we have two computed properties, viewingTable and viewingBarChart. As you can see, their implementation is very easy. They return booleans depending on the current view mode. The reason we do this is because handlebars is designed to be stupidly simple, so its #if statements can only respond to true or false values.

Finally, we have two methods, viewAsTable and viewAsBarChart that will be called when the user hits the buttons in the template.

A quick update to the model

In order to display a bar chart, we’ll need to add a percentage property to each row of data in our report. This is easily done in our find method.

Discourse.Report = Discourse.Model.extend({});
Discourse.Report.reopenClass({
 find: function(type) {
 var model = Discourse.Report.create({type: type});
 $.ajax("/admin/reports/" + type, {
 type: 'GET',
 success: function(json) {
 // Add a percent field to each tuple
 var maxY = 0;
 json.report.data.forEach(function (row) {
 if (row.y > maxY) maxY = row.y;
 })
 if (maxY > 0) {
 json.report.data.forEach(function (row) {
 row.percentage = Math.round((row.y / maxY) * 100);
 })
 }
 model.mergeAttributes(json.report);
 model.set('loaded', true);
 }
 });
 return(model);
 }
});

All we’re doing here is figuring out the maximum y property, and then updating each report object to have a percetage field which is their y divided by the maximum.

Finally, let’s set up our template:

{{#if loaded}}
 <h3>{{title}}</h3>
 <button class='btn'
 {{action viewAsTable}}
 {{bindAttr disabled="viewingTable"}}>View as Table</button>
 <button class='btn'
 {{action viewAsBarChart}}
 {{bindAttr disabled="viewingBarChart"}}>View as Bar Chart</button>
 <table class='table report'>
 <tr>
 <th>{{xaxis}}</th>
 <th>{{yaxis}}</th>
 </tr>
 {{#each data}}
 <tr>
 <td>{{x}}</td>
 <td>
 {{#if controller.viewingTable}}
 {{y}}
 {{/if}}
 {{#if controller.viewingBarChart}}
 <div class='bar-container'>
 <div class='bar' style="width: {{unbound percentage}}%">{{y}}</div>
 </div>
 {{/if}}
 </td>
 </tr>
 {{/each}}
 </table>
{{else}}
 {{i18n loading}}
{{/if}}

Near the top, you can see there are two <button> tags for switching the mode. The {{action}} helper wires things up so that when the user interacts with that button, the appropriate method on the controller will be called.

The next helper, {{bindAttr}} allows you to bind an HTML attribute to a property. In this case, we’ve bound the disabled property of the button tag to the opposite viewMode than the button represents. By doing this, the buttons will automatically enable and disable depending on the current state.

Try it out!

If you open up /admin/reports/active you’ll now see an interface that allows you to switch between views at the touch a button.

Think about all the code we didn’t have to write to make this work.

We have separated our concerns. We have a simple template that a front end designer can modify easily. The controllers and models are cleanly set apart. Our code loads data asyncronously and only displays it when it is ready. The user can switch the view style without having to contact the server for the data again.

In the next installment of this series, I plan to show how to create a custom view and the advantages it can give us!


Imported from: http://eviltrout.com/2013/03/17/adding-to-discourse-part-2.html

#2

s/viewingTable/viewingBarChart


#3

Very great post. But how if i want to re-fetch the data?

I am stumbling on refresh page. When i create a new record, how can i automatically refresh the list.