Always consider event bubbling and capturing consequences of your listeners

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the javascript category.

Last Updated: 2024-04-26

I was working on a website's mailing list sign up modal.

This had an outer frame (that dimmed the background of the whole web page) and a smaller inner frame containing the actual form for harvesting emails. Here's the HTML structure:

<div class="dimmed-background">
  <div class="content">
  </div>
</div>

I wanted to implement a feature whereby clicking on the dimmed background (i.e. on the .dimmed-background div) would close the modal. So I attached a click listener to this outer frame. However, this didn't quite work: Clicking on the inner frame (.content) also hid the modal, thereby preventing all mailing list sign ups.

This happened because the code did not take into account the way JavaScript propagates events.

Events such as clicks that happen on an target element (.content here) first go through a "capture" stage - starting from the window element and then moving on to each nested element in turn until it reaches the target, the "capture listeners" are called (those registered with addEventListener(x,y, true) (true as the third parameter - useCapture in their docs). We don't have capture listeners in this code, so that can't be the bug.

Once the event reaches its target (.content here) it then "bubbles" up all the way to the window again - i.e. event propagation is bidirectional. The addEventListener(x,y) we were using without the 3rd, useCapture parameter being set defaults it to false, which has the effect that the listener responds to bubble events like these. We've found our bug! In our case, the listener on .dimmed-background was on the way up, so it got called during this bubbling stage.

What I should have done is either:

  1. Within the handler for the click event, ensure that the "close modal" action only happens when the dimmed-background modal is clicked. This is done by inspecting the event target:
dimmedBackground.addEventListener('click', function (event) {
    var clickedElem = event.target;
  // matches() takes any CSS selector string:
  https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
  if (clickedEl.matches('.dimmed-background')) {
    // code to actually close the modal
    ...
  }
});
  1. Stop the propagation of the event explicitly
function modalClick(e) {
  e.stopPropagation();
  return false;
}

contentDiv.addEventListener('click', modalClick)

This second way may be inadvisable for click events, because it prevents items further up the HTML tree from responding to these when bubbled up (e.g. click-tracking analytics software).

Aside on terminology

"Event propagation" is the blanket term for both "event bubbling" and "event capturing". The "event target" is the element you sent the (e.g. click) event to.

Resources