Grow your CSS skills. Land your dream job.

JavaScript Event Madness! Capturing *all* events without interference

Published by Guest Author

The following is a guest post by Matthias Christen and Florian Müller from Ghostlab. Ghostlab is cross-browser cross-device testing software for Mac and PC. One of the things I'm very impressed Ghostlab can do is sync the events from one browser to all the others. Scroll one page, the others you are testing scroll. Click somewhere on one page, that same click happens on the others. I asked them about how the heck it does that when there is so much that could interfere. Matthias and Florian explain:

We have been developing Ghostlab, a tool for cross-device and cross-browser testing your websites. In essence, it syncs any number of clients that view a website and makes it easy to go through that site and make sure it works great and looks good on all viewports and platforms. A central component is the replication of events across clients - when the user clicks on a button, scrolls, or enters text into a form field on one client, we have to make sure that the very same thing happens on all others.

Capturing missed events

The client side script component of Ghostlab is listening for all sorts of events that happen, tries to catch them, and replicate them to all the other clients. At some point, we noted that we didn't quite catch all events. We had to figure out what the problem was, and came up with a solution that allows you to catch any events that happen on your site, no matter how they are handled by any custom JavaScript.

How can it be that you are listening for an event, but don't catch it? That's because any event handler has the option of doing several things with an event. You'll know the ability to prevent the default action usually taken by the browser (preventDefault()). It allows you, for example, to have a link (<a>) for which the user does not go to its href on a click event.

In addition to telling the browser to not do the default action whenever the event occurs, an event handler can also stop the propagation of an event. When an event is triggered on an element, say a link, any event handler that is attached to this specific element will be allowed to handle the event first. After it is done, the event will bubble up until it reaches the document level. Every listener for this event at any parent of the original element will be able to react to the event - that is, unless a lower event handler decides to stop propagation, in which case the event will no longer go further up in the DOM.

Our example 1 demonstrates this. When you click on the inner div (Level 3), the click handler for this element will handle the event first. Unless it prevents propagation, the parent elements (Level 2, Level 1) will afterwards be able to react to the event in order. In case you tick the "Stop Propagation" checkbox, the event handler will prevent further propagation - so, click events on Level 3 will no longer reach Levels 1 and 2, and click events on Level 2 will no longer reach Level 1.

See the Pen Event Propagation Example I by Florian Mueller (@mueflo00) on CodePen.

In example 2, we demonstrate the effect of stopping immediate propagation. This method implicitly stops the bubbling up of the event, so if there were any parent elements, we would observe the same behavior as in example 1. In addition, it also prevents any additional handlers of the same event on the same element from being executed. In our example, we have to click event handlers registered on our element. If we choose to stop immediate propagation, only the first responder will be able to handle the event, and after calling stopImmediatePropagation, no other handler will be called.

See the Pen Event Propagation Example II by Florian Mueller (@mueflo00) on CodePen.

So if you would want to listen to all the events that happen in the DOM, that's quite difficult. To prevent missing events due to cancelled bubbling up, you'd have to register all the event handlers on every single element of the DOM. And even then, in case a developer chooses to stop immediate propagation, this would only work if you were the first one to register for the event.

If we want to be absolutely sure that we are informed of any event, no matter what its handlers do with it, we have to get in the loop at the very beginning of event registration. For that purpose, we override the addEventListener function of the EventTarget object. The basic idea is simple: every event handler registration will, in the end, call this method. If we override it, we have full control of what happens when any event handler registers.

The original addEventListener function takes an event handler function (the "original event handler") as its second argument. If we don't override the addEventListener function, the original event handler function will be called whenever the specified event occurs. Now, in our custom addEventListener function, we simply wrap the original event handler in our own event handler (the "wrapper function"). The wrapper function contains any logic we require, and can ultimately call the original event handler - if we desire to do so.

Example 3 demonstrates this. The three click events attached to the "Level" elements are registered through our custom addEventListener function, so any time a click event occurs on these elements, our wrapper function is called. There, we observe the status of the checkbox - if it is ticked, we simply do not call the original event handler, thereby preventing any click events to trigger any original event handler. A little side note: if you want to make sure that you take control of all events, you have to make sure that you override the addEventListener function before any event is registered.

See the Pen Event Override Example by Florian Mueller (@mueflo00) on CodePen.

While this solution has helped us improve Ghostlab, you may ask yourself what this could be good for? Well, there are several possibilities. We have sketched two possible use cases below - if you can come up with any other, please share!

Possible Application 1: Event Visualizer

There are tools to help you visualize events on your website (we love, for example, VisualEvent Allan Jardine). Using our technique we can quickly implement such a tool ourselves. In our example, for every registered event, we simply draw a little square on top of the element for which the event was registered. On hover, we display the source code of the (original) registered event handler function.

See the Pen Event Visualizer by Florian Mueller (@mueflo00) on CodePen.

Possible Application 2: Event Statistics

Instead of drawing event indicators onto the screen, you can also display that information in another manner. This example shows you a tabular overview of the registered events on any page (given that you inject the code into it), and updates the triggered events in real-time. This can, for example, be helpful when you are experiencing performance issues and are suspecting it might be because there are too many event handlers.

See the Pen Event Override: Stats by Florian Mueller (@mueflo00) on CodePen.


Events are just a small part of the huge complex world of Front End Development. At Vanamco we are excited to bring you tools that help simplify and streamline your processes whilst keeping you up to date with best practice.

Comments

  1. Ignacio
    Permalink to comment#

    in our custom addEventListener function, we simply wrap the original event handler in our own event handler (the “wrapper function”). The wrapper function contains any logic we require, and can ultimatelly call the original event handler – if we desire to do so.

    It reminds me of Jasmine Spies.

  2. alexander farkas
    Permalink to comment#

    Overriding addEventListener is dangerous. Instead of suggesting this technique. It’s actually quite simple to listen to all events of a specific type. Simply just use event capturing. by setting the third argument to true:

    document.documentElement.addEventListener('click', function(){
    //this function will be always called if a click happens,
    //even if stopImmediatePropagation is used on the event target
    }, true);

    • Nathan Wells
      Permalink to comment#

      Yeah, I was wondering why they don’t even mention this approach. I’ve used it, and prefer it to wrapping built-in browser APIs. I guess the GhostLab approach is nice(?) in that you’ll capture all events, including ones yet to be defined.

    • Jagi
      Permalink to comment#

      I agree that it’s the best solution. The one proposed by GhostLab does not work in all cases document.body.onclick = function () {}; and indeed is dangerous.

    • alexander farkas
      Permalink to comment#

      While the demo “Event Statistics”, can’t be done with useCapture. The description of the main problem is as follows:

      So if you would want to listen to all the events that happen in the DOM, that’s quite difficult. To prevent missing events due to cancelled bubbling up, you’d have to register all the event handlers on every single element of the DOM. And even then, in case a developer chooses to stop immediate propagation, this would only work if you were the first one to register for the event.

      In fact this problem isn’t difficult to solve it’s quite easy using event capturing. There are also a lot of events which simply do not bubble by spec and event capturing is the only method to use event delegation with those kind of events.

      While duck punching addEventListener can be still featured in this article. I really would appreciate it, if this article would be corrected, because it’s simply wrong and developers not reading the comments might learn something bad.

    • Jagi
      Permalink to comment#

      100% agree with alexander farkas, you should correct this article.

    • Florian
      Permalink to comment#

      Thank you very much for your input and mentioning event capturing. We should definitely have discussed this possibility in our article. If you are just interested in listening in on events as they occur, this approach may well be better and safer.

      The two sample applications we introduced are a little different in that they do not only want to listen to events, but have as much control as possible over how events are listened for and handled. This is also the case for Ghostlab. You mentioned that our approach is dangerous – yes, that is right, but what we’re doing is dangerous from the start: we inject our script into sites of which we have no a priori knowledge, and try to enable our own functionality while not breaking them.

      In our opinion, depending on what risks you are willing to take and what task you are trying to accomplish, different approaches are valid, and none is wrong per se.

    • alexander farkas
      Permalink to comment#

      @Florian

      In programming there are facts and opinions. And I thought, I have made a clear statement, but I will try to repeat some points:

      You are right. The examples you are providing can’t be done with event capturing and if you want to have full control over all event listeners themselfes, you simply have to monkey patch addEventListener (and removeEventListener). This is neither bad nor good, it’s the only way.
      But the main problem description of this article (inlcuding your title) and your main statement: “It is difficult to listen to all events using a simple global event listener, because of the existence of stopPropagation”, are simply proven wrong or at best simply misleading. This has nothing to do with an opinion, this is a fact.

      I don’t really care, wether you only wanted to write a marketing article for your product covered by a sketchy technical post. I care about the educational part of this article.

      This article could still have some technical and educational value, if only 5-8 lines would be changed. And this is something I think, I can expect from a high value website like css-tricks.

  3. justin
    Permalink to comment#

    Does “GhostLab” do what BrowserStack does? Is “GhostLab” going to directly compete with “BrowserStack” so people can test in other browsers without paying a monthly fee? Or am I confused and they do different things?

    • Permalink to comment#

      The idea is not to compete with BrowserStack, Ghostlab and BrowserStack can work together. Ghostlab syncs any client like a mobile, tablet or browser thats connected to the same network. You must have the device or browser installed and or accessible, this has numerous advantages.

  4. Jagi
    Permalink to comment#

    addEventListener is not gonna catch this one

    document.body.onclick = function () {};
    
  5. oddalot
    Permalink to comment#

    EventTarget is not defined on iPad Safari as far as I can tell.

    • Florian
      Permalink to comment#

      You are right – the code we present isn’t production code and will have to be adapted to support all possible clients. We wanted to focus more on presenting the concept, instead of providing run-everywhere code.

  6. Hamid
    Permalink to comment#

    Not working in safari on iPHONE!

  7. joan
    Permalink to comment#

    SCRIPT5009: ‘EventTarget’ is undefined

This comment thread is closed. If you have important information to share, you can always contact me.

*May or may not contain any actual "CSS" or "Tricks".