Using Ember.js Part 3: Custom Views
Apr 1 2013This article belongs to a series of tutorials about Ember.js and Discourse:
You might want to also check out:
Who needs views?
In an Ember application, the “View” part of your MVC will often just be plain old handlebars templates. You might have noticed that in the first two parts of this tutorial series, I created Controller classes but I didn’t have to create equivalent View classes.
So when is a View necessary? The Ember View Guide explains it nicely:
Views in Ember.js are typically only created for the following reasons:
- When you need sophisticated handling of user events
- When you want to create a re-usable component
Creating a reusable component is one of the most interesting things you can do in an Ember app. Let me show you an example of one I added to Discourse recently.
The “hotness” control
In Discourse, if a moderator clicks ‘edit’ on a category, they are presented with a modal that looks like this:
The “Hotness” attribute is there to allow the moderators of a site to choose the topics they want featured on the “Hot” list. The idea is that categories that have a higher hotness will appear more frequently than those with a lower hotness.
As you can see, our scale goes up to eleven:
Embedding a View
You can embed a view in any handlebars template by using the {{view}} markup. Here’s how we can include a custom control called Discourse.HotnessView in our edit_category template:
{{view Discourse.HotnessView hotnessBinding="hotness"}}
The hotnessBinding attribute tells Ember that we want to create a binding between our control and the category’s hotness property. If the category model’s hotness changes, the control will be updated to that value automatically. Conversely, if the control updates the property, the category model will be updated to the new value as well.
Implementing our View
To implement our view, we’ll create a file called hotness_view.js and put it under discourse/app/views in our javascripts folder.
Let’s start with something basic:
Discourse.HotnessView = Discourse.View.extend({
classNames: ['hotness-control'],
templateName: 'hotness'
});
If we create a view like this, Ember will render the ‘hotness’ handlebars template where we added our {{view Discourse.HotnessView}} markup.
If you open your web browser’s inspector you’ll notice that all views are rendered into HTML as <div> tags. (You’ll also see a bunch of views you didn’t create. All handlebars templates end up in views in Ember’s internals.) You can also change the type of tag Ember creates by adding a tagName property, but in this case a div will work just fine.
Ember allows you to add custom class names to the div container by using to the classNames property. In our case, we wanted our hotness view to have a class name of .hotness-control for styling purposes.
We can now implement a template for our view. Here’s how our HTML should look:
<button value="1">1</button>
<button value="2">2</button>
<button value="3">3</button>
<button value="4">4</button>
<button value="5" class="selected">5</button>
<button value="6">6</button>
<button value="7">7</button>
<button value="8">8</button>
<button value="9">9</button>
<button value="10">10</button>
<button value="11">11</button>
The selected class will render current hotness button in red.
Now we find ourselves in a bit of a quandary: Handlebars has a really useful {{each}} helper for iterating through a list of items, but in this case our items are simply a number range.
We could model this by creating an array of objects in our view to represent numbers 1 through 11. We could then use {{each}} to iterate through them all, but it seems quite wasteful to allocate a bunch of objects when all we’re doing is listing basic numbers.
Handlebars is great for rendering most things you throw at it, but this is one of the few cases where it feels like you’re fighting against the abstraction it provides. Fortunately, there’s a way we can avoid using handlebars altogether for our view.
Custom Rendering
If you create a method called render in your view, Ember will use its result instead of a template for rendering. It will be called with a buffer that accepts a push call with a string value. Here’s an example:
Discourse.HelloView = Discourse.View.extend({
render: function (buffer) {
buffer.push('hello');
buffer.push(' eviltrout');
}
});
If you embedded it using {{view Discourse.HelloView}} you’d see “hello eviltrout” instead of a handlebars template.
Let’s use this render method to create our buttons, adding the selected class where necessary:
Discourse.HotnessView = Discourse.View.extend({
classNames: ['hotness-control'],
render: function(buffer) {
for (var i=1; i<12; i++) {
buffer.push("<button value='" + i + "'");
if (this.get('hotness') === i) {
buffer.push(" class='selected'");
}
buffer.push(">" + i + "</button>");
}
}
});
And now if we reload our page, we’ll see our hotness control! The correct button will be highlighted thanks to a comparison with the hotness property of the view, which is bound to the hotness property of our model.
Potential Drawbacks
Rendering using the buffer like this can be quite useful, but it does some tradeoffs.
The first is that your code will be uglier. Appending strings is no where near as easy to read or write as a simple handlebars template. If you want to divide up responsibilities on your team between front end developers and designers, you’ll probably find that they are much more comfortable changing a handlebars template than the above.
The second is you have to remember to escape user supplied values yourself to avoid XSS issues. You can do this by including the handlebars escape function and calling it (thanks @krisselden):
var escape = Handlebars.Utils.escapeExpression;
buffer.push(escape(user_value));
The third is that your bound properties wont automatically update. Consider this: what would happen if you changed the hotness property within your category model? Since it’s bound to the HotnessView the hotness view will receive the updated value, but it won’t know to render again. We’ve lost some of the magic that makes Ember a pleasure to work with.
There’s a way around this. We can add the following method to Discourse.HotnessView:
hotnessChanged: function() {
this.rerender();
}.observes('hotness')
The hotnessChanged method is now set up as an observer on the hotness property of the view. Whenever the hotness property changes, the observer will be called. In this case, all we want to happen is for the view to render again. this.rerender() tells Ember to do just that.
Now our control will properly update if the hotness changes.
An Aside: Rendering Performance
There is another benefit to rendering controls using the buffer rather than handlebars: it can be considerably faster. Ember is never quite sure what properties in your template will change and how frequently that will happen. By default, it assumes anything can and will change, and spends a considerable amount of rendering time setting up the structures it needs to observe them.
This is less pronounced on simple templates with few properties, but on large templates with thousands of properties you might start to notice.
There are cases where, if you know that your properties don’t need to update, you can take advantage of the performance of buffer based rendering. It will make your code uglier and less flexible, so I suggest you think twice before converting your handlebars templates to buffer.push calls.
Responding to Button Presses
The last thing our view has to do is respond to clicks so we can update the hotness property when the user clicks on a new number. The other powerful aspect of Ember views is they expose user events that you can deal with in a clean and elegant way. For example, we can define a click method like so:
click: function(e) {
console.log("The view was clicked! Here's the jQuery event:" + e);
}
It will be called whenever the user clicks on the view. If you’ve ever written jQuery code to handle events you should feel right at home.
Note: we didn’t have to create the jQuery binding to the click event. Ember knew we wanted it when we created the click method. This also means we don’t have to unbind the click event. You can let Ember do that automatically when the View is no longer used, which is great for avoiding programmer errors that leak memory.
As you can see, the click method is called with a reference to the jQuery event of what was clicked on. We can use this to figure out what button the user clicked like so:
click: function(e) {
var $target = $(e.target);
// Return if something in the View was clicked on that wasn't a button
if (!$target.is('button')) return;
// Set our hotness value to the value of the button
this.set('hotness', parseInt($target.val(), 10));
// Don't bubble up any more events
return false;
}
And we’re done!
Now we have a nice looking reusable control in about 38 sloc.
In the future, we may allow users to set their own hotness preferences for categories to their own tastes. We could embed the same control on their user preferences using one line of handlebars. That’s pretty sweet!
If you’re the type who’s been following upcoming HTML standards, you might be familiar with web components. The great thing about the above implementation is that it provides an abstraction that can easily be refactored into proper web components when more browsers support them and the timing is right.
Happy coding!
Are you interested in learning more about Ember.js?
I'll be teaching at Embergarten in Toronto on May 18 + 19. It's going to be a great way to learn and socialize with other awesome developers. You should consider coming!



