Capturing DOM events

June 20 2021
Archived

There are a few use cases for which we might want to attach handlers to events globally.

You may want to generalize DOM updates based on event type, without attaching handlers atomically, or perhaps you simply want to track mouse movements.

The one way one might imagine doing this is just to handle the events after propagation.

That is, in a parent element.

Some jquery-esque code:

<div class="parent">Parent<span></span>
    <div class="child">Child<span></span>
    </div>
</div>
var $stopPropagation = $('input');

function clickHandler (event) {
  console.log("event fired");
}

$('.parent').on('click', clickHandler);
$('.child').on('click', clickHandler);

This would work fine, and we would see any clicks on the child be propagated up, first being handled by the child elements handler and then by the parent handler.

Unfortunately, a developer could stop this with a simple change. Stopping the event from ever reaching a handler one DOM layer up.

function clickHandler (event) {
  console.log("event fired");
  // stop default action behavior from occuring
  event.preventDefault();
  // stop bubbling to the parent
  event.stopPropogation();
}

A developer could take this a step further, and stop even lateral event propagation to other handlers for the same event in the same child component.

function clickHandler (event) {
  console.log("event fired");
  // stop default action behavior from occuring
  event.preventDefault();
  // stop bubbling to the parent + other handlers for `event`
  event.stopImmediatePropogation();
}

This makes it chaotic and unreliable to track DOM events by trusting event bubbling or handler chaining.

In fact, the only way to track all events reliably and without obstruction, would be to jack into the process when events are registered. We can do this with a sneaky override of a function in EventTarget called addEventListener.

Looking at the call signature for addEventListener, we see that it takes the event handler function as a second argument.

addEventListener(type, listener);

We can wrap this handler in our own customer handler that does the work we need, before calling the original handler function.

This will ensure that events are registered with our custom functionality.

In code:

var eventListenerProto = EventTarget.prototype.addEventListener;

function logEvent(eventName) {
  console.log("An event occurred: ", eventName);
}
EventTarget.prototype.addEventListener = function(eventName, eventHandler)
{
  eventListenerProto.call(this, eventName, function(event) {
     // our injection -> we have to ensure this is not breaking
     logEvent(eventName);
     eventHandler(event);
  });
};

This seems to work, but a reasonable extension would be to extend the solution to a React (for my own devious purposes). I wonder if React fiddles with event bubbling enough such that this will no longer work?

References #