Tracking attached event handlers doesn’t need to be a pain

Bartosz Polnik
3 min readJul 16, 2019

--

If you’ve ever been working with dropdowns, modals and other components relying on absolute positioning and event listeners, you probably faced the problem of events that didn’t propagate properly, callbacks that weren’t invoked or libraries didn’t work at all. There can be many reasons for that, but usually, the most effective way to understand what went wrong is to inspect event handlers. These are the single source of truth.

Chrome has a feature that makes this simple - Event listeners panel. Here you can see event handlers attached to a selected element, along with event handlers attached to its parents. Toggling between only node handlers and also parents’ listeners is done with Ancestors checkbox.

Event listeners panel in Chrome

Ancestors

The Ancestors toggle is especially helpful if you rely on libraries using delegated events, like jQuery. These libraries attach listeners to document, listen for all events and invoke callbacks only if elements triggering the event match the selector of choice. This can be achieved in jQuery with:

$(document).on(‘click’, ‘modal’, function (event) { }

This pattern is quite common because you don’t need to care if elements you want to interact with are in DOM or not. You can write code that will work regardless of component initialization or presence.

One library that heavily uses this technique is Bootstrap. But just as Chrome can show you event handlers attached directly to DOM elements, this is not what jQuery does behind the hood. jQuery attaches only one callback to listen for an event. When this callback is invoked, it iterates over an internal array containing all attached event handlers.

The result is a subpar experience because Event listeners panel will no longer show you the location in the code where event handlers were attached, but the location in jQuery source code where it attaches the only externally visible listener. Fortunately, for that problem, Chrome also has a fix.

Framework listeners

The fix is Framework listeners checkbox in Events listeners panel. When you toggle it on, Chrome will include all event listeners attached using jQuery.

There is one caveat though. This will work only if jQuery is loaded globally, that is, added to window.jQuery. This isn’t the case if you use a bundler like Webpack or Rollup. With these, by default, jQuery is local and inaccessible. If we want to see event handlers in this situation, we need to write some additional code or configure jQuery as global. Let’s focus on the first approach.

Chrome exposes an API that allows us, developers, to enrich Event Listeners panel with listeners from other frameworks and libraries. To do it, we need to create a function that for a given dom node returns a list of event handlers attached to it. The code of this function will heavily depend on the library we want to add support for, but for popular jQuery, the reference implementation is in Chromium repo at:

https://chromium.googlesource.com/chromium/src/third_party/+/65facf445687bf160188ba7cccd59743758283a4/WebKit/Source/devtools/front_end/event_listeners/EventListenersUtils.js#414

There you may also find a great example of the value returned by that function.

https://chromium.googlesource.com/chromium/src/third_party/+/65facf445687bf160188ba7cccd59743758283a4/WebKit/Source/devtools/front_end/event_listeners/EventListenersUtils.js#261

In my local setup with Webpack, I decided to copy the original function and modify it to use a local version of jQuery. This solution isn’t elegant, but works! With the function in place, I had to add it to window.devtoolsFrameworkEventListeners and voila!, I had all event listeners in place.

The source code of modified function and attaching to window.devtoolsFrameworkEventListeners below. I hope you find it useful:)

--

--