3.2 $.Controller – Templated Event Binding

by Justin Meyer

3.2 $.Controller – Templated Event Binding

Justin Meyer 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).

posted in Development on October 17, 2011 by Justin Meyer

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 use as template
and learning tools) typically will not leak.

Simple plugins bind only on elements within the element the plugin was
called on. jQuery cleans up these event handlers 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 right 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.
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 binding and unbinding. 3.1 brought
templated event binding. This lets you bind and delegate on elements
outside the controller’s parent 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
"{NAME} click", it looks in two places:

  1. this.options
    • First, it uses NAME to look up a value on the controller
      instance’s options.
  2. window – If a value is not found, it uses NAME to look up a value
    on the window object.\

The match can be one of two types:

  1. An object – If an object is found, it binds or delegates on that
    object instead of using the controller’s parent element. This could
    be another element, or any object that has events triggered on it (a
    Model for example).
  2. A string – If a string is found, it just replaces {NAME} with the
    value of that string.\

You can use a string template 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!

blog comments powered by Disqus

Getting Started with Cordova

Brian Moschelposted in Development on March 26, 2015 by Brian MoschelAt Bitovi, we’re big fans of building applications with web technologies and using build tools to target other platforms like iOS, Android, and desktop. This article will provide a quick guide to getting up and running quickly with Cordova.

Web Application Theory

Justin Meyerposted in Development on June 27, 2014 by Justin MeyerUnderstand the technology and architecture choices Bitovi make and why we make them. What Bitovi does and why.

Contact Us
(312) 620-0386 | contact@bitovi.com
 or cancel