Advanced Event Handling with jQuery.

Mark Dalgleish #MelbJS 2011.10.12

A little about me.

Front End Developer

Flint Interactive

Created with Fathom.js

Starting at the beginning: Basic Binding

  1. Select all elements matching selector
  2. Apply event handler to each element
$('img').bind('click', function(event){
    console.log('Hello world!');
});

Or, using the shorthand:

$('img').click(function(event){
  console.log('Hello world!');
});

Real world event binding, let's add some structure

We'll put our code in an object:

Gallery.prototype = {
    init: function(elem) {
        //Save reference to 'this' for callbacks
        var self = this;

        this.$gallery = $(elem);

        this.$gallery.find('img').click(function(){
            self.zoom();
        });
    },
    zoom: function() {
        //Code...
    }
};

Now let's introduce some more concepts...

Namespaced events === avoid stepping on anyone's toes

We may want to unbind just our events later, without accidentally removing event handlers from other plugins.

var self = this;
this.$gallery.find('img').bind('click.gallery', {
    self.zoom();
});

Now we can target our click event when unbinding:

this.$gallery.find('img').unbind('click.gallery');

Or, unbind all of your events:

this.$gallery.find('img').unbind('.gallery');

Proxying events: Why we need $.proxy

Often you'll create an anonymous function just to call a single method.

var self = this;
this.$gallery.find('img').bind('click.gallery', function(){
    self.zoom();
});

This is JavaScript! Can't we just pass the function name?

Well, this won't work since jQuery changes the context:

this.$gallery.find('img').bind('click.gallery', this.zoom);
zoom: function() {
    this.$gallery; //Undefined :(((
}

$.proxy to the rescue!

Returns a new function that always runs in the supplied context:

$.proxy(function, context)

Using our example:

this.$gallery.find('img').bind('click.gallery', $.proxy(this.zoom, this));
zoom: function(event) {
    this.$gallery; //object! :)))

    //Still need the DOM element that was clicked?
    $(event.target);
}

$.fn.live - Handle events further up the tree

Attaches an event handler to the document root.

$('img').live('click', $.proxy(this.zoom, this));

But...

...don't use it!

$.fn.live is wasteful:

$('img').live('click', $.proxy(this.zoom, this));

Use 'delegate' instead!

Effectively replaces 'live'

//This old line of 'live' code:
$('img').live('click', $.proxy(this.zoom, this));

//Is equivalent to:
$(document).delegate('img', 'click', $.proxy(this.zoom, this));

We can attach the event handler to any element:

this.$gallery.delegate('img', 'click', $.proxy(this.zoom, this));

Attaches one event handler to this.$gallery instead of one for each 'img'.

Create your own event types

Custom events allow you to bind and trigger non-standard events.

Custom events act like regular events, including bubbling up the DOM.

$('p').bind('turnRed', function(){
  $(this).css('color', 'red');
});

$('p:first').trigger('turnRed');

Coupled with namespacing, makes a great way for plugins to expose functionality.

$('#gallery').trigger('next.gallery');

Decouple your code with custom event types

Instead of manually calling the methods of one module from another, you can manually trigger and listen to your own event types.

//Module A:
zoom: function(event) {
    $(event.target).trigger('zoom.gallery');
}
//Module B:
$(document).delegate('img', 'zoom.gallery', function(){
    $(this); //Someone zoomed this image in a gallery
});

This avoids creating a dependency where Module A will fail if Module B isn't present.

Or, if the element doesn't matter...

Create a super simple publish/subscribe (pubsub) system:

//Module A:
zoom: function() {
    $(document).trigger('zoom.gallery');
}
//Module B:
$(document).bind('zoom.gallery', function(){
    $(this); //Someone zoomed this image in a gallery
});

By always triggering and binding our custom events against the document, we've essentially created a centralised messaging system.

Keyboard navigation!

Arrowing through content is totally awesome:

var self = this;
$(document).keydown(function(event){
    switch (event.which) {
        case 37: //Left arrow
            self.prev();
            break;
        case 39: //Right arrow
            self.next();
            break;
    }
});

Clever keyboard navigation can add a lot to a site.

Any significantly interactive site should have keyboard navigation.

Coming in jQuery 1.7: New 'on' and 'off' functions

Instead of forcing you to use different methods for event binding vs. event delegation, you'll be able to use the unified 'on' and 'off' functions.

Instead of:

$elem.bind('click', $.proxy(this.func, this));

You can write this:

$elem.on('click', $.proxy(this.func, this));

The 'off' function works the same way, except for unbinding.

jQuery's 'on' instead of 'live'

Instead of:

$('img').live('click', $.proxy(this.func, this));

You can write this:

$(document).on('click', 'img', $.proxy(this.func, this));

jQuery's 'on' instead of 'delegate'

Instead of:

$gallery.delegate('.next', 'click', $.proxy(this.next, this));

You can write this:

$gallery.on('click', '.next', $.proxy(this.func, this));

So what about 'bind', 'live' and 'delegate'?

They'll still be available for the foreseeable future.

If you're writing code specifically for a new project using jQuery 1.7, 'on' and 'off' have really great APIs.

If you're writing a plugin, it's a good idea to stick with 'bind' and 'delegate' for now.

Putting it together... (verbose version)

Gallery.prototype = {
  init: function(){
    this.$gallery = $('#gallery');

    this.$gallery
      .delegate('img',   'click.gallery', $.proxy(this.zoom, this))
      .delegate('.next', 'click.gallery', $.proxy(this.next, this))
      .delegate('.prev', 'click.gallery', $.proxy(this.prev, this));
    
    $(document).bind('keydown.gallery', function(event){
        if      (event.which === 37) { self.prev(); }
        else if (event.which === 39) { self.next(); }
      });
  },

  zoom: function(){ ... },
  next: function(){ ... },
  prev: function(){ ... }
};

Streamlining our event handling

That was a lot of code for something conceptually simple. Plus, we had to repeat our event handling logic for mouse and keyboard events separately.


Anyone familiar with Backbone.js knows how it handles events:

var DocumentView = Backbone.View.extend({

  events: {
    'dblclick'                : 'open',
    'click .icon.doc'         : 'select',
    'contextmenu .icon.doc'   : 'showMenu',
    'click .show_notes'       : 'toggleNotes',
    'click .title .lock'      : 'editAccessLevel',
    'mouseover .title .date'  : 'showTooltip'
  }

But what if we want this syntax in a regular, non-Backbone page? And what if we want to take this idea even further?

Introducing...

Eventralize — Event handling for OO JavaScript

jQuery plugin written in CoffeeScript for handling events in object oriented JavaScript using a Backbone-inspired events hash.


$('#element').eventralize({
  'event'          : 'functionName',
  'event selector' : 'functionName',
  'event selector, event selector' : 'functionName'
}, context);

The 'context' parameter is where your methods are stored and the context in which they will run. In most cases, the context will be this.

The 'selector' parameter can be any jQuery selector, or;

You can also use document, window and body as selectors if you need to capture events higher up the DOM.

Eventralize — Keyboard events? No problem.

Simple keyboard handling syntax:

$container.eventralize({
  'click .next, keydown(right) document' : 'next'
}, this);

Handles key combinations elegantly:

$container.eventralize({
  'keydown(alt+ctrl+right) document' : 'no_way'
}, this);

No more nested ifs manually checking key codes!

No more repeating your event binding logic between clicks and key presses!

Eventralize — Easy namespacing!

The method name is used as the namespace by default!


If you'd like to selectively override this, you can namespace any of your events the old fashioned way:

$container.eventralize({
  'click.gallery .next, keydown.gallery(right) document' : 'next'
}, this);

Or, if you decide to pass in a namespace as the third parameter, Eventralize will handle the rest. Namespaces delcared inline will take precedence.

$container.eventralize({
  'click .next, keydown(right) document' : 'next'
}, this, 'gallery'); // <-- There's our namespace!

Eventralize — Automatically passes the event object

The specified function will provide you with the 'event' object just like a regular jQuery callback.

next: function(event) {
    event.target; //The element that triggered the event
    event.preventDefault();
}

Using Eventralize with our example

Rather than declaring our events inline, we'll store them in Gallery:

Gallery.prototype = {
	
  events: {
    'click' : 'focus',
    'click img' : 'zoom',
    'click .next, keydown(right) document' : 'next',
    'click .prev, keydown(left) document'  : 'prev'
  },
  
  init: function() {
    this.$gallery = $('#gallery');

    this.$gallery.eventralize(this.events, this);
  },

  //Methods...
};

Bonus! Cover your tracks with uneventralize

Eventralize lets you add and remove entire collections of event handlers without breaking a sweat.

It's now possible to remove all of your event handlers with one line:

  destroy: function() {
    this.$gallery.uneventralize(this.events);
  }

Storing our event hash pays off! :)

What have we learned today?

Be the first to get your hands on Eventralize.

Slides available online:

Follow me on Twitter! @markdalgleish