Rails 4, Turbolinks, and Delegated Event Handling in jQuery

Javascript for Rails 4 with Turbolinks

Hey Devs, If you know of a better way to handle this particular issue, please share in the comments below!

If you've been paying attention to Rails edge or following the release of Rails 4, you are no doubt aware of one of the more flashy features: Turbolinks. From the Turbolinks readme: "Turbolinks makes following links in your web application faster. Instead of letting the browser recompile the JavaScript and CSS between each page change, it keeps the current page instance alive and replaces only the body and the title in the head."

Even better, you can use Turbolinks now as a gem in your Rails 3.2 project.

It sounds great: you just add the Gem to your Gemfile, add the require line to your javascript manifest, and you're done! No additional steps require. Even better, with Rails 4, this should be the default behavior.

The Problem

There are a few gotchas, however, and I'm going to address one of the more obvious ones: event handling in javascript. Most of the time, using jQuery, I've seen developers register direct event handlers like so:

$( selector ).click( function()… )

Or, alternatively, the more verbose and modern approach, using jQuery on():

$( selector ).on( 'click', function()… )

Or, in older cases, using bind():

$( selector ).bind( 'click', function()… )

The problem with this method is that your event handlers will need to be attached each time your DOM is modified, such as the case when Turbolinks updates your <body> element without loading the full page. Your document ready function will not fire with Turbolinks.

The Solution

The solution, with jQuery, is delegated event handling. Delegated event handling works very similarly to regular event handling, with one small change. You provide a top level element, and then provide a search selector for the individual elements to receive the event. When using jQuery on() with delegate events, the event is bound to the top level element, but when it runs, the children of the element are traversed looking for matches for the selector. A very simple way to do this would be to use the document object as the parent element. For example, if we wanted to handle the click events for elements with the class "click_link", we could register a delegated event handler like so:

$( document ).on( 'click', '.click_link', function()… )

Notice the second argument to on(), '.click_link'. This is the selector that is actually going to receive the event, but the DOM tree will be traversed looking for matching selectors, starting with the parent element, on the fly, when the event fires. This has the added benefit of making your code idempotent, and further, can be used in instances where you are modifying your DOM structure on the fly and have new elements created which should handle events. No more binding links in your addItem() function, for example.

Additionally, if you have a lot of elements receiving the event, this approach is faster than binding all of the individual event handlers.

Downsides?

The only downside (that I know of) to this approach is that event propagation may happen more slowly. This means if you stop event propagation (using event.preventDefault() or event.stopPropagation()), it may not occur in time to prevent all the events from firing that you want. This doesn't cause problems for links to '#' which are handled with event.preventDefault(), but it may cause issues in other cases where you have multiple event handlers on one element.

Please leave me any questions or comments. If you know of a better way to handle this particular issue, please share!