Just Posted: Introducing Chirpie for iPhone

Custom events in jQuery open doors to complex behaviors

This post is written by Rebecca Murphey. Rebecca is a front-end architecture consultant based in Durham, N.C., specializing in jQuery development and standards-based HTML and CSS. You can read her blog or find her on Twitter @rmurphey.

I’ve been doing a lot of thinking lately about the best way to organize non-trivial jQuery features; in my article in April’s JSMag, I talk about using the object literal pattern for separating the component pieces of a feature. A second option that bears mentioning involves making use of jQuery’s custom events, and that’s what I want to talk about today.

Custom events let you structure your code such that behaviors are bound to the thing that’s being acted on, rather than the thing that triggers the action. For example, imagine a container that can be expanded or collapsed. The traditional approach would be to bind the expansion and contraction behavior to the element that triggers the behavior:

<div>
<div class="toggleElement">Open/Close</div>
<h1>Title</h1>
<div class="expandable">

Expandable content</div>
</div>
<script>
$('div.toggleElement').click(function() {
 $(this).parent().find('div.expandable').toggle();
});
</script>

An approach that uses custom events might separate the task into two parts: first, saying that the container is expandable; then, saying which element will trigger the behavior:

<div class="expandableContainer>
<div class="toggleElement">Open/Close</div>
<h1>Title</h1>
<div class="expandable">

Expandable content</div>
</div>
<script>
// say the container is expandable
$('div.expandableContainer').bind('toggleContent', function() {
 $(this).find('div.expandable').toggle();
});

// bind the expansion/contraction to the toggleElement
$('div.toggleElement').click(function() {
 $(this).parent().trigger('toggleContent');
});
</script>

As with lots of examples of new ways to do things, the custom event method is longer and perhaps a bit more confusing than the “traditional” way of handling this. However, as the number of behaviors that you can trigger grows, the more useful it becomes to have the definition of those behaviors associated with the element that the behaviors affect, rather than the element that triggers the behavior. With custom events, you can start to write code that depends less on the specific markup to which your events are bound, and more on the behaviors that certain elements are expected to exhibit. This can be especially useful when you start to define the behaviors of a widget, as you’ll see in a moment.

A quick recap of .bind() and .trigger()

Before we move forward, let’s back up for a second to make sure we have the basics covered.

jQuery offers a number of so-called convenience methods for binding behaviors, such as .click(), .mouseover(), .keyup(), etc. We’ll still use these, but it’s important to also understand the more verbose .bind() method, which needs to be used for custom events, and the generic .trigger() method, which can be used to trigger (duh) an event. We also want to understand how to take advantage of the .bind() method’s ability to store data for later use.
Here’s an example of how we could use .bind() and .trigger():

$('body').bind('foo', { 'bar' : 'bam' }, function(e) { alert(e.data.bar); });
$('body').trigger('foo'); // alerts 'bam'

For the record, you can also pass data to the .trigger() method, though this isn’t quite as pretty. Note that the first argument passed to the function handling the event will always be the event object.

$('body').bind('foo', function(e, param1, param2) { alert(param1 + ': ' + param2); });
$('body').trigger('foo', [ 'bar', 'bam' ]); // alerts 'bar: bam'

With that out of the way, let’s move on to a slightly more complex example of custom events.

An expandable, collapsible, editable, removable widget

The value of custom events becomes clearer when there are several behaviors that you need to trigger on the same element. For the sake of this post, we’re going to work on an imaginary widget that has several behaviors:

* It can be edited. Triggering the edit event loads a form for editing the widget, which is then AJAX-enabled to load the expanded widget with the new content.

* It can be deleted. Triggering the delete event removes the widget from the page, and sends a message to the server that the widget has been deleted.

* It can be expanded. Triggering the expand event the first time loads the full content of the widget from the server; triggering it again uses the already-fetched content.

* It can be collapsed. Triggering the collapse event hides the expanded content of the widget.

We’ll start with some basic HTML that represents a couple of widgets on the page:

<div class="widget" id="widget1">
<h1>Title</h1>
<div class='content'></div>
</div>
<div class="widget" id="widget2">
<h1>Title</h1>
<div class='content'></div>
</div>

Obviously these aren’t very exciting widgets, but note that they have a common class (”widget”) and unique IDs. We’ll use the class to target our event bindings; we’ll use their IDs to communicate to the back-end about which widget we’re working with when we are editing, deleting, or saving them. The back-end will be responsible for presenting us with the appropriate content or the appropriate form for editing the widget based on the ID.

Next, let’s set up the simplest custom event, delete.

var widgetScript = 'widgetManager.php';

$('div.widget').
 bind('delete', function(e, $li) {
 var $this = $(this);
 // send a message to the server to delete the widget
 $.post(
 // use the widgetScript as the URL for the post
 widgetScript, 

 // tell the widget script that we want to delete a
 // widget and give it the id of the widget to delete
 { 'action' : 'delete', 'widget_id' : $this.attr('id') }, 

 // when the script returns, remove the widget from the page
 function() { $this.remove(); }
 );
 });

Note that when the code refers to

$(this)

it’s referring to the widget to which the event was bound. Now, if we wanted to delete the first widget, we could do:

$('#widget1').trigger('delete');

which is handy but not all that exciting. Let’s go ahead and set up the rest of the custom events …

$('div.widget').

 bind('delete', function(e) {
 var $this = $(this);
 $.post(
 widgetScript,
 { 'action' : 'delete', 'widget_id' : $this.attr('id') },
 function() { $(this).remove(); }
 );
 }).

 bind('edit', function(e) {
 var $this = $(this);
 var $controls = $this.find('ul.controls').clone(true);
 $this.load(
 widgetScript,
 { 'action' : 'edit', widget_id : $this.attr('id') },

 // ajax-enable the form that is returned
 function(html) {
 // populate with new content
 $this.html(html);
 $this.find('form').submit(function(e) {
 e.preventDefault();
 var $form = $(this);
 var url = $form.attr('action');

 // do an AJAX post of the form using
 // the form's action attribute as the URL
 $.post(
 url, 

 // serialize the form data and tack on the action
 // and widget id
 $form.serialize() +
 '&action=save&widget_id=' +
 $this.attr('id'),
 function(html) {
 // populate the widget with the returned
 // html and restore the controls
 $this.html(html).prepend($controls);
 }
 );
 });
 }
 );
 }).

 bind('expand', function(e) {
 var $this = $(this);

 // get the full content from the server
 if (!$this.hasClass('contentLoaded')) {
 $(this).find('div.content').load(
 widgetScript,
 { 'action' : 'expand', 'widget_id' : $(this).attr('id') },
 function() { $this.addClass('contentLoaded'); }
 );
 }

 // mark the widget as expanded, which will
 // show the full content via css
 $this.addClass('expanded');
 }).

 bind('collapse', function(e) {
 // mark the widget as collapsed, which will
 // hide the full content via css
 $(this).removeClass('expanded');
 });

At this point, we have four custom events that we can trigger on any widget (or set of widgets) from anywhere in our code. The ability to run a custom event against a set of elements in a single line is a powerful benefit of using custom events. For example, if we wanted to expand all widgets, we could do:

$('div.widget').trigger('expand');

which would fetch the content for each widget, populate each widget’s content div, and expand each widget to show its content. This ability to trigger custom events on a set of widgets — whether it’s all of them, the first one, the odd ones, a set chosen by the user, or whatever set makes you happy — is one of the most powerful advantages of structuring your features this way.

Hooking it up

The final step in this example is adding controls to the widgets to actually initiate the events. We’ll do this by building a controls element and adding it to each of the widgets. The controls element will look to see which widget it belongs to, and then use the class attribute of individual control elements to trigger the proper event on the widget.

 var $controls = $('
<ul class="controls"/>');

$('
	<li class="edit">Edit</li>
').appendTo($controls);
$('
	<li class="delete">Delete</li>
').appendTo($controls);
$('
	<li class="expand">Expand</li>
').appendTo($controls);

// event delegation on the controls element
$('div.widget ul.controls').click(function(e) {
 // get the actual element that was clicked
 var $t = $(e.target);

 // if the element that was clicked is an
	<li> and
 // is not disabled ...
 if ($t.is('li:not(.disabled)')) {
 // determine which widget the controls belong to
 var $widget = $(this).parents('div.widget');

 // use the class on the li to determine what action to trigger;
 // pass the triggered event a data array containing the
 // element that initiated the event
 $widget.trigger($t.attr('class'), [$t]);
 };
});

$('div.widget').prepend($controls.clone(true));

Note that we’re passing some data when we trigger the event, in the form of an array that contains the element that initiated the event (the <li> that was clicked). With some minor adjustments to the widget event handlers, we can use this data to change the text of the <li> when the widget is expanded or collapsed.

// ...
bind('expand', function(e, $sourceElement) { // ADDED $sourceElement as argument
 var $this = $(this);
 if (!$this.hasClass('contentLoaded')) {
 $(this).find('div.content').load(
 widgetScript,
 { 'widget_id' : $(this).attr('id'), 'action' : 'expand' },
 function() { $this.addClass('contentLoaded'); }
 );
 }
 $this.addClass('expanded');

 // NEW CODE
 if ($sourceElement) {
 $sourceElement.removeClass().addClass('collapse').text('Collapse');
 }
}).

bind('collapse', function(e, $sourceElement) { // ADDED $sourceElement as argument
 $(this).removeClass('expanded');

 // NEW CODE
 if ($sourceElement) {
 $sourceElement.removeClass().addClass('expand').text('Expand');
 }
});

The neat thing here is that this text change is managed by the widget’s custom event handlers; the code for triggering the event (above) stays nice and clean and abstracted, and the “collapse” and “expand” custom event handlers are primed for reuse by other elements that might initiate them (and need their text changed, too).

Conclusion

When everything’s said and done, we have created well-organized code that consolidates the behavior widgets in a few bind() statements, rather than scattering those behaviors across the elements that initiate them. We’ve also gained flexibility, in that it’s easy for any element to initiate a behavior, or for a behavior to be triggered across multiple widgets at once. Finally, we’ve added a simple controls element, which captures a click event and translates it into an event to be triggered on the widget.

See the full code in action (with a dummy back-end).

Custom events are a new way of thinking about jQuery for people accustomed to the traditional model, which puts the emphasis on the element that initiates the event. Custom events, on the other hand, put the emphasis on the object that is being acted upon. Though they can be confusing at first glance, they are a powerful way to organize your code while emphasizing abstraction and reuse.

Rebecca Murphey is a front-end architecture consultant based in Durham, N.C., specializing in jQuery development and standards-based HTML and CSS. You can read her blog or find her on Twitter @rmurphey.

15 Responses to “Custom events in jQuery open doors to complex behaviors”

  1. Elijah Manor says:

    You’ve been tweeted (a good thing) – Tweetback from @elijahmanor

  2. Chris says:

    I love it. I’m not too experienced with bind, but this example will save me a lot of code. You’re in my bookmarks!

  3. Keith says:

    The DEMO for this Custom events in jQuery open doors to complex behaviors | Programming Blog is broken in IE7.

    Clicking on Widget1 Edit – Delete – Expand acts upon Widget2

    Unable to affect Widget1.

    Great concepts though! Keep up the good work!

  4. Keith: Thanks for pointing this out, my bad. If you take the line:

    $(’div.widget’).prepend($controls.clone(true));

    (line 33 in the “Hooking it up” section) and change it to:

    $(’div.widget’).prepend($controls.clone());

    Then the IE issue goes away. The “true” part was from an earlier iteration of this, so it’s no longer appropriate. Apologies, and I’ll upload a new sample!

  5. Ralf says:

    Very smart!

  6. Jay says:

    Just wanted to say I really enjoyed this article.

  7. Samuel Wan says:

    Thanks, Rebecca, this is a very helpful article that showed me a different way of looking at bind/trigger. I come from other object-oriented UI frameworks that support data binding and property change notification… your article helped me see possibilities of using bind/trigger in similar ways.

  8. Bruce says:

    Ummm, but this is not a custom event? Its just some custom behaviours. The event remains constant as “click”. Nothing will fire without the click event. Title of the page is a little misleading

  9. Jake says:

    Hi Rebecca
    Thanks for your article.

    Technical question: I notice that you pass the data array to trigger between [] like this [$t]. I find that if I just use the name of the array ie $t, only the first element of the array arrives in the handler, and your [] syntax does the trick of getting the whole array passed properly. But I don’t understand why! I’ve never seen this syntax before. Can you shed any light? Many thanks!

    $widget.trigger($t.attr(’class’), [$t]);

    Jake

Trackbacks/Pingbacks

  1. [...] a guest post on his blog, reynoldsftw.com, and I happily obliged. Check out my post there about custom events in jQuery, and how they can change your approach to event binding by putting the emphasis on the element [...]

  2. [...] In: web resources 2 Apr 2009 Hello there! If you are new here, you might want to subscribe to the RSS feed for updates on this topic.Powered by WP Greet BoxRebecca Murphey provides a look at using custom events in jQuery. While she doesn’t really get to the why until the end of the article, the benefits are certainly something worth… Read more [...]

  3. [...] Rebecca Murphey discusses custom events in her tutorial, Custom events in jQuery open doors to complex behaviors [...]

  4. [...] Custom events in jQuery open doors to complex behaviors | Steve Reynolds Blog [...]

  5. [...] Rebecca Murphey discusses custom events in her tutorial, Custom events in jQuery open doors to complex behaviors [...]

  6. [...] Rebecca Murphey discusses custom events in her tutorial, Custom events in jQuery open doors to complex behaviors [...]

Leave a Reply