Introduction
I'd like to introduce a simple yet powerful concept that might just revolutionize the way you handle events in JavaScript - Event Forwarding. Simply put, Event Forwarding says this: "when event A happens, fire event B instead".
What does that achieve?
Well say you have a <button>
element that you want to trigger a JavaScript function. In the bad old days, you'd add an "onclick
" attribute and put your code in there. Nowadays, you'd probably use JQuery to select the element and attach a click handler. But you're still reliant on finding the correct element with the JQuery selector, and you still need to know that the "click
" event is what needs to happen to trigger the action. That's fine until you change the id of the <button>
, or move it, or change it from a button to an anchor, or some other element that needs to trigger the action on some event other than "click
".
In MVC terms, you've introduced a dependency between your view and your controller that should not be there. Enter Event Forwarding.
Background
Event Forwarding is not my idea, I borrowed the concept from the wonderful ZK Framework and used it in a personal project of mine, Bootstrap.jsp.
Bootstrap.jsp is a JSP tag library that helps JSP developers build Bootstrap interfaces without having to get bogged down in the details of the HTML structure and class names that Bootstrap demands. I'm writing this article as if you were, but you don't need to be using Bootstrap.jsp, Bootstrap, JSP or even Java to make use Event Forwarding in your project.
A Demonstration
Let's look at the example given above and explore how Event Forwarding can help. In Bootstrap.jsp, you would create your <button>
element something like this:
<%@ taglib uri="http://bootstrapjsp.org/" prefix="b" %>
<b:button id="action" context="info" label="Action"/>
Which when rendered will look like this:
<button id="action" class="btn btn-info">Action</button>
In your JavaScript, you can attach an event handler like so:
$('#action').on('click', function(e) { ... });
Your script needs to know a) the id of the button to attach the handler to, and b) that it is a click that should trigger the handler. You can remove this dependency by using Event Forwarding to add a layer of abstraction between the element, the event, and the handler.
In Bootstrap.jsp, you would use the "forward
" attribute:
<%@ taglib uri="http://bootstrapjsp.org/" prefix="b" %>
<b:button forward="click=doAction" context="info" label="Action"/>
The "forward
" attribute attaches a "data-forward
" attribute to your element:
<button data-forward="click=doAction" class="btn btn-info">Action</button>
The "data-forward
" attribute is saying "when the 'click' event happens, trigger a 'doAction
' event. So now, when the button is clicked, the "doAction
" event is triggered, so your script does not need to know that it is a click event that triggers the action. The forwarded event will bubble up the DOM, so your script does not need to know the id of the button to attach a handler to the event:
$(document).on('doAction', function(e) { ... });
See how you've isolated your view and controller? Now if you later decide to trigger the action from a different element, or another element, by way of a different event, the "doAction
" event will still be triggered and your handler function will still be called, with no changes to your JavaScript.
Implementation
Of course, Event Forwarding doesn't just happen. The browser knows nothing of the "data-forward
" attribute or how to handle it. To do that, you need an Event Forwarding implementation. Here is my implementation from Bootstrap.jsp:
$(document).bind('ready DOMNodeInserted', function(e) {
var forwarded = $(e.target).find('[data-forward]');
$.each(forwarded, function() {
var element = $(this), forwards = element.attr('data-forward');
$.each(forwards.split(','), function() {
var forward = this.split('=');
element.on(forward[0], function(event) {
element.trigger({type:forward[1], originalEvent:event});
});
});
});
});
The handler that attaches the forwarding event handler is bound to both the "ready
" and the "DOMNodeInserted
" events. This is so that you can insert new elements to the document, and the "data-forward
" attribute will be found and the event forwarding handlers attached to them.
If you're wary of attaching handlers to the "DOMNodeInserted
" event, you could wrap the function in a call to setTimeout()
to process them asychronously (the chances are that the user isn't going to do anything before the handlers are attached, but there is a risk), or, if you're not going to be inserting any elements with "data-forward
" attributes, then you could just remove that event from the code entirely.
Note that the "data-forward
" attribute is split on ",
", so you can add multiple forwards in one attribute, separated by commas. Note also that the original event is sent along with the forward event, so if need be, you can access it:
$(document).on('doAction', function(e) {
console.log('The original event was a ' + e.originalEvent.type);
});
Catch And Throw
There is no reason why the handler for a forwarded event should be a JavaScript handler. For example, it's not unusual to have a <form>
that gets submitted when a <select>
element within it is changed. Event forwarding can be used here to trigger the form submission, instead of having to attach a JavaScript handler to your <select>
to submit the form:
<form action="/someaction.do" data-forward="send=submit">
<select data-forward="change=send">
<option>1</option>
<option>2</option>
</select>
</form>
It would be nice to not need the "data-forward
" on the <form>
, but a "submit
" event must be triggered on the form itself. It's not enough for the select forward to be "change=submit
", as the forwarded "submit
" event is triggered on the <select>
rather than the form.
But it is possible for the <form>
to catch the "send
" event and forward it as a "submit
" to itself, causing the form to be submitted.
In ZK, Event Forwarding can have an id attribute as well as an event name. My implementation above wouldn't be difficult to change to support this, in which case the above example could be rewritten as:
<form id="myform" action="/someaction.do">
<select data-forward="change=myform.submit">
<option>1</option>
<option>2</option>
</select>
</form>
Going Further
Of course, it's entirely possible to forward forwarded events. In Bootstrap.jsp, you can create modal dialogs in predefined "molds" that have buttons set up and ready to use:
<%@ taglib uri="http://bootstrapjsp.org/" prefix="b" %>
<b:modaldialog id="mydialog" mold="confirm" label="Confirm">
<b:modalbody>Are you sure?</b:modalbody>
</b:modaldialog>
The output will be:
<div id="mydialog"
data-backdrop="static" role="dialog" class="modal" data-keyboard="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
<span class="glyphicon glyphicon-exclamation-sign"> </span> Confirm
</h4>
</div>
<div class="modal-body">
Are you sure?
</div>
<div class="modal-footer">
<button data-forward="click=cancel.bsjsp.modal"
data-dismiss="modal" class="btn btn-danger">
<span class="glyphicon glyphicon-remove"> </span> Cancel
</button>
<button data-forward="click=ok.bsjsp.modal"
data-dismiss="modal" class="btn btn-success">
<span class="glyphicon glyphicon-ok"> </span> OK
</button>
</div>
</div>
</div>
</div>
See how the buttons have forwarded events? That means you can do the following in JavaScript:
$('#mydialog').modal().on('ok.bsjsp.modal', function(e) {
});
But by adding a forward to forward the "ok.bsjsp.modal
" event, you could have the "OK" button trigger our "doAction
" event from above:
<%@ taglib uri="http://bootstrapjsp.org/" prefix="b" %>
<b:modaldialog forward="ok.bsjsp.modal=doAction" mold="confirm" label="Confirm">
<b:modalbody>Are you sure?</b:modalbody>
</b:modaldialog>
I have found Event Forwarding a useful tool in my arsenal, I hope you do too!
History
- 11th April, 2014: Initial version