Demystifying custom events in jQuery
This article originally appeared in the May 2009 issue of JSMag. We’re all familiar with the basic events — click, mouseover, focus, blur, submit, etc. — that we can latch on to as a user interacts with the browser. Custom events open up a whole new world of event-driven programming. In this article, we’ll use jQuery’s custom events system to make a simple Twitter search application, but the general concepts should apply to any framework that supports custom events. I confess: it took me a long time to decide to learn about custom events. The built-in events seemed to suit my needs just fine, and it was difficult to understand why I’d want to start adding my own. Boy, was I missing out. It turns out that custom events offer a whole new way of thinking about event-driven JavaScript. Instead of focusing on the element that triggers an action, custom events put the spotlight on the element being acted upon. This brings a bevy of benefits, including:
- behaviors of the target element can easily be triggered by different elements using the same code;
- behaviors can be triggered across multiple, similar, target elements at once; and
- behaviors are more clearly associated with the target element in code, making code easier to read and maintain
$('.switch, .clapper').click(function() {
var $light = $(this).parent().find('.lightbulb');
if ($light.hasClass('on')) {
$light.removeClass('on').addClass('off');
} else {
$light.removeClass('off').addClass('on');
}
});$('.lightbulb').bind('changeState', function(e) {
var $light = $(this);
if ($light.hasClass('on')) {
$light.removeClass('on').addClass('off');
} else {
$light.removeClass('off').addClass('on');
}
});
$('.switch, .clapper').click(function() {
$(this).parent().find('.lightbulb').trigger('changeState');
});$('.lightbulb').
bind('changeState', function(e) {
var $light = $(this);
if ($light.hasClass('on')) {
$light.trigger('turnOff');
} else {
$light.trigger('turnOn');
}
}).
bind('turnOn', function(e) {
$(this).removeClass('off').addClass('on');
}).
bind('turnOff', function(e) {
$(this).removeClass('off').addClass('on');
});
$('.switch, .clapper').click(function() {
$(this).parent().find('.lightbulb').trigger('changeState');
});
$('#master_switch').click(function() {
if ($('.lightbulb.on').length) {
$('.lightbulb').trigger('turnOff');
} else {
$('.lightbulb').trigger('turnOn');
}
});jQuery Event Primer
Before we dive in, a couple of things we need to recap. In the world of custom events, there are two important jQuery methods: .bind() and .trigger(). I encourage you to read the jQuery docs for details, but basically:- The .bind() method takes an event type and an event handling function as arguments. Optionally, it can also receive data, which will be available to the event handling function in the data property of the event object. The event handling function always receives the event object as its first argument.
- The .trigger() method takes an event type as its argument. Optionally, it can also take an array of values. The first item in the array will be the second argument passed to the event handling function (after the event object).
Our Mission
To demonstrate the power of custom events, we’re going to create a simple tool for searching Twitter. The tool will offer several ways for a user to add search terms to the display: by entering a search term in a text box, by entering multiple search terms in the URL, and by querying Twitter for trending terms. The results for each term will be shown in a results container; these containers will be able to be expanded, collapsed, refreshed, and removed, either individually or all at once. When we’re done, it will look like this:The Setup
We’ll start with some basic HTML:Twitter Search
Search Results for
The Results Containers
The results containers are the heart of the application. We’ll create a setupResults() plugin that will prepare each results container once it’s added to the Twitter container. Among other things, it will bind the custom events for each container and add the action buttons at the top right of each container. Each results container will have the following custom events:- The refresh event will mark the container as being in the “refreshing” state, and fire the $.getJSON() request to fetch the data for the search term.
- The populate event will receive the returned JSON data and use it to populate the container.
- The remove event will remove the container from the page after the user verifies the request to do so. Verification can be bypassed by passing true as the second argument to the event handler. The remove event also removes the term associated with the results container from the global search_terms object.
- The collapse event will add a class of collapsed to the container, which will hide the results via CSS. It will also turn the container’s “Collapse” button into an “Expand” button.
- The expand event will remove the collapsed class from the container. It will also turn the container’s “Expand” button into a “Collapse” button.
// we'll use this every time we add a new results panel,
// so let's build it once and cache it in $actions
var $actions = $('$.fn.setupResults = function(settings) {
return $(this).each(function() {
var $results = $(this);
var $actions = settings.actions;
var term = settings.term;
// change the "Search results for" text
$results.find('span.search_term').text(term);
// bind custom events for results box
$results.
// the "refresh" event fetches
// the latest content for the term
bind('refresh', function(e) {
// indicate that the results are refreshing
var $this = $(this).addClass('refreshing');
$this.find('p.tweet').remove();
$results.append('Loading ...');
// get the twitter data using jsonp
$.getJSON(
'http://search.twitter.com/search.json?q=' + escape(term) + '&rpp=5&callback=?',
function(json) {
$this.trigger('populate', [ json ]);
}
);
}).
// the "populate" event takes results
// in json format
// and populates the results container
bind('populate', function(e, json) {
var results = json.results;
var $this = $(this);
$this.find('p.loading').remove();
$.each(results, function(i,result) {
var tweet = '' +
'' +
result.from_user +
': ' +
result.text +
' ' +
result.created_at +
'' +
'';
$this.append(tweet);
});
// indicate that the results
// are done refreshing
$this.removeClass('refreshing');
}).
// the remove event removes
// the results from the page
// after the user confirms the action
bind('remove', function(e, force) {
// allow forced removal without confirmation
if (
!force &&
!confirm('Remove panel for term ' + term + '?')
) {
return;
}
$(this).remove();
// indicate that we no longer
// have a panel for the term
search_terms[term] = 0;
}).
// the collapse event collapses the results so only the
// header of the results section is showing
bind('collapse', function(e) {
$(this).find('li.collapse').removeClass('collapse')
.addClass('expand').text('Expand');
$(this).addClass('collapsed');
}).
// the expand event
bind('expand', function(e) {
$(this).find('li.expand').removeClass('expand')
.addClass('collapse').text('Collapse');
$(this).removeClass('collapsed');
});
if ($actions && $actions.length) {
// add a clone of $actions to the results panel
var $a = $actions.clone().prependTo($results);
// use the class of each action to figure out
// which event it will trigger on the results panel
$a.find('li').click(function() {
// pass the li that was clicked to the function
// so it can be manipulated if needed
$results.trigger(
$(this).attr('class'), [ $(this) ]
);
});
}
});
};The Twitter Container
The Twitter container itself will have just two custom events:- The getResults event will receive a search term. It will check the global search_terms object to determine whether there’s already a results container for the term; if not, it will add a results container using the results template (div.template), set up the results container using the setupResults() plugin discussed above, and then trigger the refresh event on the results container in order to actually load the results. Finally, it will store the search term in the global search_terms object, so the application knows not to re-fetch the term.
- The getTrends event will query Twitter for the top 10 trending terms. It will iterate over them and trigger the widget’s getResults event for each of them, thereby adding a results container for each term. Here you can see how we go about passing data to a triggered event.
$('#twitter').
bind('getResults', function(e, term) {
// make sure we don't have a box for this term already
if (!search_terms[term]) {
var $this = $(this);
var $template = $this.find('div.template');
// make a copy of the template div
// and insert it as the first results box
$results = $template.clone().
removeClass('template').
insertBefore($this.find('div:first')).
setupResults({
'term' : term,
'actions' : $actions
});
// load the content using the "refresh"
// custom event that we bound to the results container
$results.trigger('refresh');
search_terms[term] = 1;
}
}).
bind('getTrends', function(e) {
var $this = $(this);
$.getJSON('http://search.twitter.com/trends.json?callback=?', function(json) {
var trends = json.trends;
$.each(trends, function(i, trend) {
$this.trigger('getResults', [ trend.name ]);
});
});
});$(document).ready(function() {
$('form').submit(function(e) {
e.preventDefault();
var term = $('#search_term').val();
$('#twitter').trigger('getResults', [ term ]);
});
$('#get_trends').click(function() {
$('#twitter').trigger('getTrends');
});
});$(document).ready(function() {
// pass search terms via URL hash
if (document.location.hash) {
var terms = document.location.hash.split(',').reverse();
$.each(terms, function(i,term) {
$('#twitter').trigger('getResults', [ term ]);
});
}
});$(document).ready(function() {
$('#refresh').click(function(e) {
$('#twitter div.results').trigger('refresh');
});
$('#expand').click(function(e) {
$('#twitter div.results').trigger('expand');
});
$('#collapse').click(function(e) {
$('#twitter div.results').trigger('collapse');
});
$('#remove').click(function(e) {
if (confirm('Remove all results?')) {
$('#twitter div.results').
trigger('remove', [ true ]);
}
});
});$('div.results:first, div.results:last').
trigger('refresh');Conclusion
Custom events offer a new way of thinking about your code: they put the emphasis on the target of a behavior, not on the element that triggers it. If you take the time at the outset to spell out the pieces of your application, as well as the behaviors those pieces need to exhibit, custom events can provide a powerful way for you to “talk” to those pieces, either one at a time or en masse. Once the behaviors of a piece have been described, it becomes trivial to trigger those behaviors from anywhere, allowing for rapid creation of and experimentation with interface options. Finally, custom events can enhance code readability and maintainability, by making clear the relationship between an element and its behaviors.Learn More
- The jQuery Event object
- jQuery’s bind() method
- jQuery’s trigger() method
- jQuery and JSON-P
- Need I say more?