Tracking Image Error Events Using Event Delegation In JavaScript
At InVision, the primary asset is the Image. On the server-side, we know everything about these images: size, density, orientation, how long it takes to resize them, and whether or not they can even be thumbnailed properly. On the client-side, however, we don't do a lot of active tracking for images. This is something I've been thinking about changing. There are a number of ways in which image tracking can be added to a client-side application. For example, event-delegation. Now, the "error" event, emitted by a failed IMG object, doesn't bubble up in the DOM (Document Object Model). But, it does participate in the Capturing phase. Which means, we can use event delegation to capture image error events at the top of the DOM tree.
Run this demo in my JavaScript Demos project on GitHub.
When an event is fired on the DOM (Document Object Model), the browser determines the "propagation path" of the Event object through the DOM tree. The browser then passes the Event object through three different phases:
- Capture Phase
- Target Phase
- Bubble Phase
All events pass through the Capture phase; but, only some events have a Target phase handler defined; and, only some events enter the Bubble phase (either by default or because their bubbling has been canceled). Error events, such as those emitted by a failing IMG object, are among the Events that don't enter the Bubble phase. This means that we can't use event-delegation to handle IMG error events during Bubbling. But, we can still use event-delegation to handle IMG error events during the Capture phase.
ASIDE: In React.js, IMG error events can be handled using event-delegation because React uses a synthetic event system where all events are transparently handled using event-delegation. As such, even events that don't bubble natively in the browser can be treated as if they bubble inside a React application.
In order to add an event handler for the Capture phase of a DOM event, we need to pass in the optional third argument to the .addEventListener() method:
Element.addEventListener( eventType, handler, true )
The "true" parameter, in this .addEventListener() call, tells the browser to attach the given event handler to the Capture phase, not the Bubble phase. This means that this event handler will see the events as they are propagating down to the target object, regardless of whether or not they ever bubble back up through the DOM.
To see the capturing of IMG error events, I've put together a small demo in which you can load one of two groups of images: working and failing. The "working" images will all result in 200 OK response, which we ignore. And, the "failing" images will all result in a 404 Not Found response, which we will capture in our event-delegation handler.
In the following code, notice that I'm using the "true" parameter when configuring my handleErrorCapture() event-handler:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Tracking Image Error Events Using Event Delegation In JavaScript
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
Tracking Image Error Events Using Event Delegation In JavaScript
</h1>
<p>
<a class="initiator initiator--working">Load working images</a> —
<a class="initiator initiator--failing">Load failing images</a>
</p>
<div class="images">
<!-- Images to be injected dynamically. -->
</div>
<script type="text/javascript">
// Setup our DOM references for the demo.
var refs = {};
refs.workingButton = document.querySelector( ".initiator--working" );
refs.failingButton = document.querySelector( ".initiator--failing" );
refs.images = document.querySelector( ".images" );
refs.body = document.body;
// The working images are expected to result in a 200 OK response. The failing
// images are expected to result in a 404 Not Found response.
var sources = {
working: [ "one.png", "two.png", "three.png", "four.png", "five.png" ],
failing: [ "one.GIF", "two.GIF", "three.GIF", "four.GIF", "five.GIF" ]
};
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// Listen for image ERROR events at the BODY level of the DOM tree.
refs.body.addEventListener(
"error",
handleErrorCapture,
// According to the DOM (Document Object Model) specification, the ERROR and
// LOAD events for Images do not BUBBLE up through the DOM tree. As such, we
// have to use the CAPTURE phase, which starts at the top of the DOM and
// descends down into the DOM toward the target element.
true
);
// I handle ERROR events that have been captured going down on the DOM.
function handleErrorCapture ( event ) {
console.group( "Error Event Captured" );
console.log( "Target:", event.target );
console.log( "Image Source:", trimSrc( event.target.src ) );
console.groupEnd();
}
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// Setup click handlers.
refs.workingButton.addEventListener( "click", loadWorking, false );
refs.failingButton.addEventListener( "click", loadFailing, false );
// I load the working images.
function loadWorking ( event ) {
console.warn( "Loading working images..." );
renderImages( sources.working );
}
// I load the failing images (producing ERROR events).
function loadFailing ( event ) {
console.warn( "Loading failing images..." );
renderImages( sources.failing );
}
// I clear the existing images and render the given image sources.
function renderImages ( imageSources ) {
refs.images.innerHTML = "";
imageSources.forEach(
function operator ( src ) {
var image = new Image();
image.src = ( "./img/" + src );
image.classList.add( "images__img" );
refs.images.appendChild( image );
}
);
}
// I slice off the portion of the SRC that is relevant for the demo (just to make
// the output easier to consume).
function trimSrc ( src ) {
return( src.slice( src.indexOf( "/img/" ) ) );
}
</script>
</body>
</html>
As you can see, I'm attaching the handleErrorCapture() event-handler to the BODY element using the Capture phase. If we open this up in the browser and attempt to load the failing images, we get the following page output:
As you can see, each of the five images in the "failing" group emitted an Error event. And, each of the five emitted events was successfully observed on the BODY Element as the Error event propagated down to the Target object during the Capture Phase. In this way, even though the Error objects never bubble back up, we can still view and record them in a single point in the DOM tree using event-delegation.
When it comes to handling events on the DOM (Document Object Model), I generally use the Target phase or the Bubble phase. Literal years go by without me even thinking of the Capture phase. But, in some cases, like observing IMG error events, the Capture phase is what makes event-delegation possible.
A Note On The Phrase: "Event Delegation"
When I say "event delegation" in this context, what I mean is that we have one event handler listening for events triggered by a dynamic number of Elements located farther down in the DOM tree. Though, to be fair, "event delegation" may not be the most accurate term since we're not really handling the event for the Target; rather, we're simply "observing" the event emitted by the target. That said, the gesture is similar and the phrase is one people are comfortable with. As such, it feels like a reasonable phase to use for this context.
Want to use code from this post? Check out the license.
Reader Comments
Very clever! Love it when I learn something new and useful. Thanks!
@Chris,
Yeah, the Capturing stuff is very interesting. After posting this, I was talking to some people at work about it and we starting seeing some common use-cases:
- Observing events that don't inherently bubble (ie, what I'm doing in this demo).
- Observing events in situations where the program may inherently cancel the bubbling. For example, if you're a 3rd-party analytics script and you want to monitor "click" events (for sake of argument) and you want to record this event regardless of whether or not the underlying program calls event.stopPropagation().
I don't have much of any experience with the latter; but, one of our teams is building their event-handlers almost exclusively on the Capture phase since they are writing a Script that you can drop into any page and they want it to work regardless of what the parent page is actually doing.
This team also said that using the Capturing phase helps when the underlying page is using React.js, since React uses a synthetic event system that is entirely based on delegation. I've only dabbled with React.js, but this sounds pretty interesting.