Using Ember.js Part 3: Custom Views


#1
This article belongs to a series of tutorials about Ember.js and Discourse: Important Update: The preferred way to do this is now using Ember Components. The concepts in this tutorial still work, but the Hotness control created below would be better created as a proper component now that Ember has support for it. (They didn't exist when this was written.)

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 hotness=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!


Imported from: http://eviltrout.com/2013/04/01/adding-to-discourse-part-3.html

#2

This is good, but doesn't embedding HTML inside of javascript result in code that's harder to read, and manage? Like this view - https://github.com/discourse/d... - A whole bunch of HTML generated by string manipulation


#4

Good read. I want to see the Authentication n Authorization post as well coz there is none in Google search for embers n authentication.


#5

FWIW -

I recently attended the Ember training in Boston. Yehuda and Tom had mentioned that the more preferred way to use views in the future is to use a more helper style

instead of this {{view Discourse.HotnessView hotnessBinding="hotness"}} in your template

you would do something like this {{hotness obj=someobj }} where the obj=someobj feels similar to how rails does the locals hash and you dont need to use the Binding suffix. Then you create a helper with a view hooked in. You can use a predefined view or a general view.

Ember.Handlebars.helper('hotness', Discourse.HotnessView );

or

Ember.Handlebars.helper('hotness', Ember.View.extend({
view logic here...
}));

Thanks for all the insight into Ember. This along with the Discourse source code has been a great help.


#6

This is definitely true now and wasn't at the time of my writing. The {{view}} method will work for some time (if not indefinitely) so my article is not totally obsolete. But it would be cleaner to make a helper for it.


#9

@EvilTrout:disqus really nice post! I have a doubt about how to interact the view actions with controllers. So for example imagine if when the user choose the Hotness number it will be send to server updating this value in database. How can I send the action inside of *click: function(e) {* to the controller? Thanks in advance and keep the amazing work with Discourse.


#10

Thanks for reply. I just implemented this, but I have another doubt and if you can help I will be very grateful.
I have *Post* and *Comments*, and users can vote in *Post*, like the default "hands up" and "hands down", but the return of the *Post* object I have the property votes: {up: 3, down: 1} so I need to show in the view *2 votes* and I want to dynamically change it when users vote up or down, but I don't know how to bind correctly based in values inside of an object. Could you help me?


#11

sounds like you need a computed property for this