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.