Handling DOM Events With EmberJS Views And Controllers

Hot on the heels of picking up Ember and starting to learn it, I think I’ve actually learned something useful! I certainly hope I have, at least. Otherwise, I’m going to be in trouble soon.

In my EmberCloneMail app, I have an <ul> tag with <li> tags that represent the list of emails you can click on and view – and that’s precisely the problem I ran in to. How do I handle a DOM “click” on each of the <li> elements, and know which of the Email objects is the one being clicked? Of course I know how to do this in Backbone already and I expected it to be as easy in Ember. It is just as easy – it’s just different and presents some options not available in Backbone.

The View And Template Setup

The basics of getting this set up are the same no matter how I decide to handle the specific click. There will be some details of this that change with the different options, but this is a good starting point to render the view with a template.

The Handlebars template for the list and items looks like this:

<script data-template-name="email-list-view" type="text/x-handlebars">
  {{#each EmberCloneMail.emailListController}}
      {{view EmberCloneMail.EmailPreview contentBinding="this"}}
  {{/each}}
</script>

<script data-template-name="email-preview" type="text/x-handlebars">
  {{#with content}}
    <span class="from">{{from}}</span>
    <span class="subject">{{subject}}</span>
    <span class="date">{{date}}</span>
  {{/with}}
</script>
view raw 2.html This Gist brought to you by GitHub.

Note that there’s two templates in this – one of the <ul> and the “collection” information, and one for individual <li> items that represent the emails I’ll be displaying. This follows the same idea that I’ve talked about with Backbone’s collection / child rendering, but it puts the loop directly in the template instead of in the JavaScript view object.

The Ember view then looks like this:

EmberCloneMail.EmailListView = Em.View.extend({
  tagName: "ul",
  classNames: ["email-list", "email-view"],
  templateName: "email-list-view"
});

EmberCloneMail.EmailPreview = Em.View.extend({
  tagName: "li"
  templateName: "email-preview"
});

view raw 1.js This Gist brought to you by GitHub.

There are also two views – one for the <ul> list and one for each of the <li> items.

You can either use another Handlebars template block to render the list in to the HTML directly, or use JavaScript to instantiate, render and display the view in the right spot. How you do that makes no difference in handling the DOM clicks.

Option 1: Let The View Handle It

This is pretty much “the Backbone way” and it can be done in Ember in much the same manner. In the view, you add a “click” method and it receives the standard jQuery “events” argument for jQuery DOM events.

EmberCloneMail.EmailPreview = Em.View.extend({
  tagName: "li"
  templateName: "email-preview",

  click: function(e){
    // handle the view click here
  }
});
view raw 3.js This Gist brought to you by GitHub.

From there, you can do things as you would expect – manipulating the view, manipulating the model and generally getting on about the job of working with the view and model. This is very much the “presenter” approach to view objects, creating the vertical path between Model -> Presenters -> View (HTML in this case) in an MVP style.

The problem with this approach is that it pretty listens to all of the clicks for any HTML element within the view template. If you have a very simple template and you want the click to behave this way, it works out well. But if you want to listen to a click on a specific element within the view, that’s where the next option comes in to play.

Option 2: {{action}} Helpers

Handlebars lets you register view helper functions that can pretty much do anything you want. One of the helpers that comes with Ember is the {{action}} function that let’s you specify a method on your view to handle on a DOM event.

<script data-template-name="email-preview" type="text/x-handlebars">
  {{#with content}}

    <span class="from" {{action "doStuff" on="click"}}>{{from}}</span>

    <span class="subject">{{subject}}</span>
    <span class="date">{{date}}</span>
  {{/with}}
</script>
view raw 4.html This Gist brought to you by GitHub.

In this case, we’re putting the {{action}} on the “from” portion of the view, so that when you click the email sender’s name it will be handled by the view. Like the first option, the methods that you specify with an {{action}} are methods on the view, directly:

EmberCloneMail.EmailPreview = Em.View.extend({
  tagName: "li"
  templateName: "email-preview",

  doStuff: function(){
    // do stuff here, based on the click of the HTML element
  }
});
view raw 5.js This Gist brought to you by GitHub.

You can also specify a different view to handle the event using {{action target=”someOtherView”}} in the template.

The benefit here, as stated above, is that you can handle any DOM event you want with any method in the view that you want. This is largely the functionality as Backbone’s declarative “events” hash on a view, but has the added advantage that you can target a different view. You’re also specifying this in the template directly – which may or may not be an advantage, depending on your perspective.

Option 3: Targeting A Controller Action

In looking through some of the example apps, I noticed that the several of the views included sub-view rendering of “Ember.Button” in order to create a button on the screen. These sub-views were then told to target a specific controller and action for the clicking of the button. I thought this sounded like a pretty cool option and I wanted to see what it would take to make my EmailPreview view work this way. Turns out, it was very easy.

I found the Ember.Button view in the source code with the help of Github’s awesome search feature (just hit “t” and then start typing. It will find file names in the current repo for you). From that code, I learned about the Ember.TargetActionSupport object. This is the object that let’s you specify the controller and action to target when you are setting up the view render in Handlebars templates.

To get your view to work with this, just extend the view with this object:

EmberCloneMail.EmailPreview = Em.View.extend(Ember.TargetActionSupport, {
  // tag, template, etc here
});
view raw 6.js This Gist brought to you by GitHub.

Then in your templates, when you call in to this view, you specify the target controller and action:

<script data-template-name="email-list-view" type="text/x-handlebars">
  {{#each EmberCloneMail.emailListController}}

      {{view EmberCloneMail.EmailPreview contentBinding="this"

        target="EmberCloneMail.emailController"
        action="showEmail"

      }}

  {{/each}}
</script>

<script data-template-name="email-preview" type="text/x-handlebars">
  {{#with content}}
    <span class="from">{{from}}</span>
    <span class="subject">{{subject}}</span>
    <span class="date">{{date}}</span>
  {{/with}}
</script>
view raw 7.html This Gist brought to you by GitHub.

To finish this out, you need to handle the DOM event that will trigger the controller / action the way you would use option #1 above. In my case, I added a “click” method. Within this method, you call out to the target action:

EmberCloneMail.EmailPreview = Em.View.extend(Ember.TargetActionSupport, {
  // tag, template, etc her

  click: function(e){

    // call the targeted controller / action
    this.triggerAction();

  }
});
view raw 8.js This Gist brought to you by GitHub.

The benefit in this case is that you can re-use a view and template across multiple behaviors in your app. When you specify which view you are rendering in the template, you tell it which controller and action to use. Therefore, when you are using the same view in multiple places, you can target different controllers and actions, based on what’s appropriate for the current use.

The drawback to this, though, is that you’re just about limited to a single controller and action target. This makes more sense when you consider that an HTML button typically only has the one click event. In the case of an HTML <li> like I’m using, it might not make quite as much sense to limit  your template and view to a single action. My use case called for this, though. I want to handle the click of the <li> and only the click of the <li>, so I was able to properly use the TargetActionSupport correctly.

Option ???: Something Else ???

At this point, I’ve identified these three options for handling a DOM event. There’s obviously a lot of flexibility available, here. You aren’t stuck with one way to handle a DOM event when a given scenario may call for something different.

Are there any other options (other than handling the click w/ plain jQuery)? What am I missing?

About Derick Bailey

Derick Bailey is an independent consultant, software developer, writer, blogger, speaker and technology leader in central Texas (north of Austin). He has been a professional software developer since the late 90's, and has been writing code since the late 80's. Find me on twitter: @derickbailey, @mutedsolutions, @backbonejsclass Find me on the web: MutedSolutions.com, BackboneTraining.net, WatchMeCode
This entry was posted in DOM, Ember, Javascript, JQuery, Model-View-Controller, Model-View-Presenter. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • http://twitter.com/dagda1 dagda1

    A better way of defining templates is in separate files and then using the templateName attribute to point to that file, e.g:

    templateName: ‘app/templates/url_search/show’ 

    You can load the templates into the Ember templates collection by subclassing a Tilt::Template subclass like this:

    https://gist.github.com/1780841 

  • http://mutedsolutions.com Derick Bailey

    Nice! I was going to look into separating the templates in to external files soon. Glad to know it’s that easy. :) 

    Am I correct in seeing that this gives you server side compilation of the template, which is going to improve the performance of the JS at runtime?

  • http://twitter.com/dagda1 dagda1

    Yes, it is loaded via Ajax when you create the Enber app. I think when the Ember application object is created or maybe when the view is first referenced.  They are loaded into the Ember.TEMPLATES collection.

    You also need the following lines in a rails initializer:

    require ‘sprockets/ember_handlebars’

    Rails.application.assets.register_engine ‘hjs’, EmberHandlebars

  • http://twitter.com/dagda1 dagda1

    Another interesting plugin I have found for Ember is the Ember.StateChart 
    https://github.com/emberjs-addons/sproutcore-statechart,  Whenever you switch states, you get some nice handlers for entering states (enterState) and exiting states (exitState).  

    Examples of switching states might be something simple like switching a tab control or another use case I found was to go from a non-streaming state to a streaming state via a websocket connection.

    It is an interesting add on to the usual MVC paradigm.  Still not sure why they called in StateChart instead of StateMachine.

  • http://mutedsolutions.com Derick Bailey

    If rumor and speculation serve me right, I think “StateChart” is a leftover name from the original SproutCore. 

    This definitely looks like something I need to dig into. I’ve been trying to find good ways to handle workflow / state changes.

    Thanks again! :)

  • http://twitter.com/dagda1 dagda1

    I think the main difference between backbone  and Ember is that backbone uses Dom events while Ember uses bindings.

    A binding in Ember is anything with the binding suffix.  For example, here is an example of one of my controllers:

    https://gist.github.com/1885960

    You can see that I am specifying a urlSearchBinding on line 11 which I can then reference in my template.

    Here is the show template at the templateName path:

    https://gist.github.com/1885979

    I am referencing the urlSearchBinding on line 2 and on line 3, I am speciying the targetBinding as the view but I could very easily point to a controller or any object.  For example in another template, I have the following Ember button:

    {{#view Ember.Button targetBinding=”leads” action=”showMore” isVisibleBinding=”leads.showMoreIsVisible”}}Show More{{/view}}.

    Bindings are the key difference between Ember and Backbone IMO.

    The Ember runloop is also something to be aware of and caught me out at the start.

  • Jeroen van Wissen

    And what about non-rails Ember apps? How do we put the handlebar templates in seperate files?

  • http://twitter.com/dagda1 dagda1

    I just blogged about the state chart here http://www.thesoftwaresimpleton.com/blog/2012/02/28/statemachine/