Sergio and the sigil

jQuery Custom Element and Global Events

Posted by Sergio on 2010-02-21

This last week I learned a new thing about jQuery custom events, particularly the ones of global nature. There's good documentation and examples about custom element events, but not much for the global ones.

Why do we need custom events?

Custom events make it easier to keep complex pages under control. They are a pillar for loosely-coupled UI scripts. Let's start with a simple example.

Suppose we have a fairly complex and dynamic page where many elements are Ajax-editable, using in-place editors or any other approach that posts updates to the server. Depending on how quickly the server responds to the request, there's a chance the user can start another simultaneous request before the first one finishes, maybe even seeing inconsistent results, by clicking a button too soon.

In our example — a fraction of what a real complex page would be — what we want to do is disable some of these buttons while the data is being changed, and re-enable them once we hear back from the server.

Click the field to edit it:<br>

<input type="text" readonly="readonly" id="email" name="email"
   value="joe@doe.com" style="background-color: #eee;"/> 

<input type="button" class="userOperation" id="sendButton" value="Send Message">
<input type="button" class="userOperation" id="summaryButton" value="Summary">

Custom Element Events

Let's tackle this problem first with the custom element events. Below is a summary of how these custom events are used.

$('#publisher').trigger('eventName');

$('#publisher1').bind('eventName', function() {
   //eventName happened. React here.
   $('#subscriber1').doStuff();
   $('#subscriber2').doOtherStuff();
   // more...
});

In this case we will make the elements being edited announce that they entered edit mode so that any other element can act on that announcement.

$('#email').
click(function(){
	$(this).removeAttr('readonly').css({backgroundColor: ''});
	$(this).trigger('editStart');
}).
blur(function(){
	$(this).attr('readonly', 'readonly').css({backgroundColor: '#eee'});
	$.post('/updateEmail', $('#email').serialize(), function() {
		$(this).trigger('editComplete');
	});
}).
bind('editStart', function(){
	// "this" is the #email element
	console.log('edit started, this =  ' + this.id);
	$('.userOperation').attr('disabled', 'disabled');
}).
bind('editComplete', function(){
	// "this" is the #email element
	console.log('edit complete, this =  ' + this.id);
	$('.userOperation').removeAttr('disabled');		
});

$('#sendButton').click(function(){
	//code to send a message
	alert('Message sent');
});

$('#summaryButton').click(function(){
	//code to generate summary
	alert('Summary created');
});

This approach works well in the beginning but gets really ugly as more elements need to publish their own similar events or when other new elements need to do somethings with these events too. We will need to bind handlers to all these element's events and the code inside these handlers will start getting longer and probably too far from the rest of the code that relates to it.

One step forward with page level events

Since the events we are producing here really reflect the document state more than any individual field's state, let's move that event to a more top level element, namely the body element:

$('#email').
click(function(){
	$(this).removeAttr('readonly').css({backgroundColor: ''});
	$('body').trigger('editStart');
}).
blur(function(){
	$(this).attr('readonly', 'readonly').css({backgroundColor: '#eee'});
	$.post('/updateEmail', $('#email').serialize(), function() {
		$('body').trigger('editComplete');
	});
});

$('body').
bind('editStart', function(){
	// "this" is the body element
	console.log('edit started, this =  ' + this.tagName);
	$('.userOperation').attr('disabled', 'disabled');
}).
bind('editComplete', function(){
	// "this" is the body element
	console.log('edit complete, this =  ' + this.tagName);
	$('.userOperation').removeAttr('disabled');		
});

$('#sendButton').click(function(){
	//code to send a message
	alert('Message sent');
});

$('#summaryButton').click(function(){
	//code to generate summary
	alert('Summary created');
});

Now we're getting somewhere. We reduced the number of event sources to just one, so guaranteed less duplication. But it still has some shortcomings.

The code is still bound to a different element than the one we want to operate on. What I mean by that is that the event handlers are in the context of the elements publishing the event and the code in the handlers is typically geared towards the elements that need to react to that event, that is, the this keyword is less useful than in most of your common event handlers.

The pattern of these page-level events is:

$('body').trigger('eventName');

$('body').bind('eventName', function() {
   //eventName happened. React here.
   $('#subscriber1').doStuff();
   $('#subscriber2').doOtherStuff();
   // more...
});

But wait, jQuery has real global events too

I had settled down with using the above style of global events until someone at work pointed out that there's another way of doing this, which unfortunately isn't as well discussed: the custom global events.

Here's our code using global custom events:

$('#email').click(function(){
	$(this).removeAttr('readonly').css({backgroundColor: ''});
	$.event.trigger('editStart');
}).blur(function(){
	$(this).attr('readonly', 'readonly').css({backgroundColor: '#eee'});
	$.post('/updateEmail', $('#email').serialize(), function() {
		$.event.trigger('editComplete');
	});
});

$('.userOperation').bind('editStart', function(){
	// "this" is a .userOperation button
	console.log('edit started, button: ' + this.id);
	$('.userOperation').attr('disabled', 'disabled');
}).bind('editComplete', function(){
	// "this" is a .userOperation button
	console.log('edit complete, button: ' + this.id);
	$('.userOperation').removeAttr('disabled');		
});

$('#sendButton').click(function(){
	//code to send a message
	alert('Message sent');
});

$('#summaryButton').click(function(){
	//code to generate summary
	alert('Summary created');
});

What is great about this type of event is that they are in the context of the subscribing elements, as if these elements were the publishers of the event, much like the majority of the event handling code we write.

They also allow us to move more code next to the other event handler for the subscribing elements, and even chain them all together. As an example, let's modify the event handlers of the #sendButton element to add some different behavior when the editStart event happens.

$('#sendButton').click(function(){
	//code to send a message
	alert('Message sent');
}).bind('editStart', function(){
	// "this" is the #sendButton button
	this.value = 'Send message (please refresh)';
	// change the click event handler.
	$(this).unbind('click').click(function(){
		alert('Sorry, refresh page before sending message');
	});
});

And here is the simplified representation of the global events code.

$.event.trigger('eventName');

$('#subscriber1').bind('eventName', function() {
   //eventName happened. React here.
   $(this).doStuff();
});

$('#subscriber2').bind('eventName', function() {
   //eventName happened. React here.
   $(this).doOtherStuff();
});
//more...

Conclusion

Event-based programming is the usual way we write UI code. By understanding the different types of events that jQuery provides we can allow our UI to grow without getting into a messy nightmare of event handling code scattered all over the place.