Event handling is an important part of any JavaScript application. All JavaScript is tied to the UI through events, so most web developers spend much of their time coding and modifying event handlers. Unfortunately, this is also an area of JavaScript programming that hasn’t received much attention since the language was first introduced. Even as developers started to embrace more traditional concepts of architecture in JavaScript, event handling was one of those areas in which little has changed. Most event-handling code is very tightly coupled to the event environment (what is available to the developer at the time an event is fired) and therefore is not very maintainable.
Most developers are familiar with the
event object that is passed into an event
handler when the event is fired. The event
object contains all of the information related to the event, including the event
target as well as additional data based on the event type. Mouse events expose
additional location information on the event
object, keyboard events expose information about keys that have been pressed,
and touch events expose information about the location and duration of touches.
All of this information is provided so that the UI can react
appropriately.
In many cases, however, you end up using a very small subset of the
information present on event. Consider
the following:
// Bad
function handleClick(event) {
var popup = document.getElementById("popup");
popup.style.left = event.clientX + "px";
popup.style.top = event.clientY + "px";
popup.className = "reveal";
}
// addListener() from Chapter 5
addListener(element, "click", handleClick);This code uses just two properties from the event object: clientX and
clientY. These properties are used to
position an element on the page before showing it to the user. Even though
this code seems relatively simple and unproblematic, it’s actually a bad
pattern to use in code because of the limitations it imposes.
The previous example’s first problem is that the event handler contains application logic. Application logic is functionality that is related to the application rather than related to the user’s action. In the previous example, the application logic is displaying a pop up in a particular location. Even though this action should happen when the user clicks on a particular element, this may not always be the case.
It’s always best to split application logic from any event handler, because the same logic may need to be triggered by different actions in the future. For example, you may decide later that the pop up should be displayed when the user moves the cursor over the element, or when a particular key is pressed on the keyboard. Then you may end up accidentally duplicating the code into a second or third event handler attaching the same event handler to handle multiple events.
Another downside to keeping application logic in the event handler is for testing. Tests need to trigger functionality directly without going through the overhead of actually having someone click an element to get a reaction. By having application logic inside of event handlers, the only way to test is by causing the event to fire. That’s usually not the best way to test, even though some testing frameworks are capable of simulating events. It would be better to trigger the functionality with a simple function call.
You should always separate application logic from event-handling code. The first step in refactoring the previous example is to move the pop up–handling code into its own function, which will likely be on the one global object you’ve defined for your application. The event handler should also be on the same global object, so you end up with two methods:
// Better - separate application logic
var MyApplication = {
handleClick: function(event) {
this.showPopup(event);
},
showPopup: function(event) {
var popup = document.getElementById("popup");
popup.style.left = event.clientX + "px";
popup.style.top = event.clientY + "px";
popup.className = "reveal";
}
};
addListener(element, "click", function(event) {
MyApplication.handleClick(event);
});The MyApplication.showPopup()
method now contains all of the application logic previously contained in
the event handler. The MyApplication.handleClick() method now does
nothing but call MyApplication.showPopup(). With the application
logic separated out, it’s easier to trigger the same functionality from
multiple points within the application without relying on specific events
to fire. But this is just the first step in unraveling this event-handling
code.
After splitting out application logic, the next problem with the previous example is that the
event object is passed around. It’s
passed from the anonymous event handler to MyApplication.handleClick(), then to MyApplication.showPopup(). As mentioned
previously, the event object has
potentially dozens of additional pieces of information about the event,
and this code only uses two of them.
Application logic should never rely on the event object to function properly for the
following reasons:
The method interface makes it unclear what pieces of data are
actually necessary. Good APIs are transparent in their expectations
and dependencies; passing the event
object as an argument doesn’t give you any idea what it’s doing with
which pieces of data.
Because of that, you need to recreate an event object in order to test the method.
Therefore, you’ll need to know exactly what the method is using to
write a proper stub for testing.
These issues are both undesirable in a large-scale web application. Lack of clarity is what leads to bugs.
The best approach is to let the event handler use the event object to handle the event and then hand
off any required data to the application logic. For example, the MyApplication.showPopup() method requires only
two pieces of data: an x-coordinate and a y-coordinate. The method should
then be rewritten to accept those as arguments:
// Good
var MyApplication = {
handleClick: function(event) {
this.showPopup(event.clientX, event.clientY);
},
showPopup: function(x, y) {
var popup = document.getElementById("popup");
popup.style.left = x + "px";
popup.style.top = y + "px";
popup.className = "reveal";
}
};
addListener(element, "click", function(event) {
MyApplication.handleClick(event); // this is okay
});In this rewritten code, MyApplication.handleClick() now passes in the
x-coordinate and y-coordinate to MyApplication.showPopup() instead of passing the
entire event object. It’s very clear
what MyApplication.showPopup() expects
to be passed in, and it’s quite easy to call that logic directly from a
test or elsewhere in the code, such as:
// Great victory! MyApplication.showPopup(10, 10);
When handling events, it is best to let the event handler be the
only function that touches the event
object. The event handler should do everything necessary using the
event object before delegating to some
application logic. Thus actions such as preventing the default action or
stopping event bubbling should be done strictly in the event handler, as
in:
// Good
var MyApplication = {
handleClick: function(event) {
// assume DOM Level 2 events support
event.preventDefault();
event.stopPropagation();
// pass to application logic
this.showPopup(event.clientX, event.clientY);
},
showPopup: function(x, y) {
var popup = document.getElementById("popup");
popup.style.left = x + "px";
popup.style.top = y + "px";
popup.className = "reveal";
}
};
addListener(element, "click", function(event) {
MyApplication.handleClick(event); // this is okay
});In this code, MyApplication.handleClick() is the defined event
handler, so it makes the calls to event.preventDefault() and event.stopPropagation() before passing data to
the application logic, which is exactly how the relationship between event
handlers and the application should work. Because the application logic no
longer depends on event, it’s easy to
use that same logic in multiple places as well as to write tests.