Skip to content

Instantly share code, notes, and snippets.

@justinbmeyer
Forked from jupiterjs/TemplatedEventBinding.md
Created October 17, 2011 18:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save justinbmeyer/1293430 to your computer and use it in GitHub Desktop.
Save justinbmeyer/1293430 to your computer and use it in GitHub Desktop.
Templated Event Binding

3.2's $.Controller - Templated Event Binding Goodness

JavaScriptMVC 3.2 brings a lot of great features and enhancements. So many features that changes to $.Controller didn't make the cut for our upcoming 3.2 article. This article reviews 3.2's $.Controller and talks about templated event binding (something we neglected to write up for 3.1).

Bind and Memory Leaks

Neglecting to unbind event handlers is the easiest way to create memory leaks. This is extremely common in an MVC architecture as you constantly listen for model changes:

Task.bind('created', function(ev, newTask){
  // add task to list
})

If your widgets are repeatedly added and removed from the page, you must remember to unbind these event handlers. People forget it all the time! This happens because simple jQuery plugins (which people learn from) typically will not leak.

Simple plugins bind only on elements within their 'element'. jQuery cleans these event handlers up automatically. For example, a tabs might look like:

$.fn.tabs = function(){
  // listen when an li is clicked and show tab content
  this.find('li').bind('click', function(){
    // show tab content
  })
}
$('#tabs').tabs()

If the #tabs element is removed like:

$('#tabs').remove()

jQuery will remove all event handlers on the element and in the element's children. But, if you are listening to anything outside the widget's element (say the <body> or a Model event), jQuery will not remove the event handler. Lets explore this with a leaking tooltip:

A Leaking Tooltip

To understand the problem of event handler leaking, consider a simple tooltip widget. The tooltip works by calling:

$('#tooltip').tooltip("Here is the text")

This will write "Here is the text" to the bottom righ of the #tooltip element. The code for this tooltip looks like:

$.fn.tooltip = function(html){
  var el = $('<p>').html(html),
      offset = this.offset();
  el.appendTo(document.body)

  el.offset({
    top: offset.top + this.height(),
    left: offset.left + this.width(),
    position:  'absolute'
  })

  $(document.body).click(function(){
    el.remove();
  })
}

See what's wrong? This code does not error, it leaks! If you can't spot the leak, don't feel bad. We've seen this mistake many, many times.

The problem is that although the element is removed, the body's click handler is not unbound. This function is still referenced by the DOM. And worse, this function has the paragraph element in its closure. The paragraph element and its child nodes will be kept in memory until the page refreshes.

Unbind with jQuery

jQuery helps you unbind event handlers in a number of ways:

Remove the bound element

If you remove an element from the DOM, all of its event handlers will be cleaned up. For example:

$('#foo').click(function(){
   // event handler code
});

// sometime later
$('#foo').remove()

However, this only works if you are using jQuery's DOM modifiers to remove the element like:

$('#foo').parent().html("");
$('#foo').parent().empty();
$('#foo').replaceWith("<p>Some Content</p>");

This is why you should rarely use direct DOM methods like:

$('#foo')[0].parentNode.removeChild( $('#foo')[0] );

If you do this, your event handler will sit around in memory forever! Also, sometimes you do not want (or can't) to remove the bound element. So jQuery has other options:

Unbind directly

jQuery, of course, lets you unbind an event handler with unbind. When our tooltip is removed, we can unbind the body's click handler like:

$(document.body).click(function(){
  el.remove();
  $(document.body).unbind('click', arguments.callee)
})

Note: be very careful to pass in the same function to unbind as you passed to bind (arguments.callee happens to be this function); otherwise, jQuery will not unbind your event handler and you will continue to have a leak.

If you only handle the event once, one(event, handler) will unbind call for you. We can use that to listen to body clicks and avoid leaking like:

$(document.body).one(function(){
  el.remove();
})

Finally, jQuery provides namespaced event handlers. It let you unbind all event handlers on an element that match a particular namespace. We could use namespaces like:

$(document.body).bind('click.tooltip',function(){
  el.remove();
  $(document.body).unbind('click.tooltip')
})

Problems

So far, this might seem ok, but there are lot of potential problems. But most importantly, there's a lot of waste! For every bind, there needs to be an unbind. You are double-coding. In our experience, few think about memory leaks and cleanup until it's too late.

Controller

Controller has always been useful for unbinding and unbinding. 3.1 brought templated event binding. This lets you bind and delegate on elements outside the controller's element. We can rewrite tooltip like:

$.Controller('Tooltip',{
  init : function(element, message){
    this.element.html(message)
  },
  "{document.body} click" : function(){
    this.element.remove();
  }
})

$('#info').tooltip("Search Google!");

Notice the "{document.body} click". This does exactly what you think it does and when the controller is destroyed, it will automatically unbind the document.body click handler. No double coding!

We can change Tooltip to use different elements to hide itself too. The following accepts an optional 'hideElement' option while keeping the document.body as a default:

$.Controller('Tooltip',{
  defaults : { hideElement : document.body }
},
{
  init : function(element, opts){
    this.element.html(opts.message)
  },
  "{hideElement} click" : function(){
    this.element.remove();
  }
})

// use clicks on document.body to hide
$('#info').tooltip({ message: "Search Google!"});

// use clicks on #contentArea to hide
$('#wikiInfo').tooltip({ 
   hideElement: $('#contentArea'),
   message: 'Search Wikipedia'
});

How it works

When controller finds {NAME} in a prototype method like "{hideElement} click", it uses NAME to look up a value on this.options. If a value is not found, it uses NAME to look up a value on the window.

If an object is found, it binds or delegates on that object instead of using the controller's element. If a string is found, it just replaces {NAME} with the value of that string. You could use this to configure the type of event that hides the element:

$.Controller('Tooltip',{
  defaults : {  hideElement : document.body }
},
{
  init : function(element, opts){
    this.element.html(opts.message)
  },
  "{hideElement} {hideEvent}" : function(){
    this.element.remove();
  }
})

// hide on hoverenter
$('#info').tooltip({ 
  hideEvent: 'hoverenter',
  message : 'stop moving to make me go away'
})

Updating options

New in 3.2 is the ability to update options and templated event handlers with controller's update(options) method. If a controller is already bound to an element, calling its jQuery helper with options calls update(options). So, we can update the hideEvent and hideElement like:

// first time, tooltip calls init
$('#info').tooltip({ 
  message : 'stop moving to make me go away',
  hideEvent: 'click'
})

// second time, tooltip calls update
$('#info').tooltip({ 
  hideEvent: 'hoverenter',
  hideElement : $('#closer')
})

If we want to update the message, we can overwrite update to do so:

$.Controller('Tooltip',{
  defaults : { hideElement : document.body }
},
{
  init : function(element, opts){
    this.element.html(opts.message)
  },
  "{hideElement} {hideEvent}" : function(){
    this.element.remove();
  },
  update : function(opts){
    this._super(opts)
    this.element.html(opts.message)
  }
})

Templated and MVC

MVC apps are constantly listening to changes in the $.Model layer to reflect changes in the UI. Templated event handlers make it stupidly easy to write abstract widgets that work with any model. An abstract list might look like:

$.Controller('List',{
  init : function(){
    this.element.html(this.options.template, this.options.list)
  },
  update : function(options){
    this._super(options)
    this.element.html(this.options.template, this.options.list)
  },
  "{list} add" : function(list, ev, added){
    this.element.append(this.options.template, added)
  },
  "{list} remove" : function(list, ev, removed){
    removed.elements(this.element).remove()
  }
  "{list} updated" : function(list, ev, item){
    item.elements.replaceWith(this.options.template, [item] );
  }
})

You can create list widgets that respond to changes in a $.Model.List like:

Task.findAll({}, function(tasks){
  $('#tasks').list({list: tasks, template: 'tasks.ejs'})
})

People.findAll({}, function(people){
  $('#people').list({list: people, template: 'people.ejs'})
})

And update the list with a new list like:

Task.findAll({personId: 1}, function(tasks){
  $('#tasks').list({list: tasks})
})

The PlayerMX and Todo apps are very good examples of using templated event handlers.

Conclusion

Templated event handlers have made a big difference in how we write our apps. We've abandoned OpenAjax's pub-sub for direct Model events. We rarely use callbacks on model.destroy() or model.save() like:

model.destroy(function(){
 // remove element!
})

and instead listen for changes like:

"{model} destroy" : function(){
  // remove element!
}

This makes the remove element! code run no matter how the model instance gets destroyed.

Finally, templated event handlers lead to some of the bigger 3.2 changes: $.Observe and $.route.

Enjoy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment