DEV Community

Cover image for Are you sure you know how an event propagates in JavaScript?
Amandeep Singh
Amandeep Singh

Posted on • Edited on

Are you sure you know how an event propagates in JavaScript?

Events are everywhere in web programming — input change, mouse move, button click, and page scroll are all forms of events. These are the actions that get generated by the system so that you can respond to them however you like by registering event listeners.
This results in an interactive experience for the user. Understanding how the event model works in modern web browsers can help you build robust UI interactions. Get it wrong, and you have bugs crawling around.

My aim through this article is to elaborate some basics around the event propagation mechanism in the W3C event model. This model is implemented by all modern browsers.

Let's get started ⏰.


Event propagation

Imagine If we have two HTML elements, element1 and element2, where element2 is the child of element1 as shown in the figure below:

Nested elements with click handlers

And we add click handlers to both of them like this:

element1.addEventListener('click', () => console.log('element1 is clicked'));
element2.addEventListener('click', () => console.log('element2 is clicked'));
Enter fullscreen mode Exit fullscreen mode

What do you think will be the output when you click element2? 🤔

The answer is element2 is clicked, followed by element1 is clicked. This phenomenon is known as Event bubbling, and it's a core part of the W3C event model.

In event bubbling the innermost target element handles the event first, and then it bubbles up in the DOM tree looking for other ancestor elements with registered event handlers.

💡 In event bubbling the innermost target element handles the event first and then it bubbles up in the DOM tree

Now, the interesting bit is that event flow is not uni-directional, as you might have assumed. The event flow mechanism in the W3C event model is Bi-directional. Surprise Surprise! 😯.

We mostly have been dealing with event bubbling when working with frameworks like React and never think much of another phase which is Event Capturing.

💡 Event bubbling is just one side of the coin; Event capturing is the other.

In the event capturing phase, the event is first captured until it reaches the target element (event.target). And you, as a web developer, can register your event handler in this phase by setting true as the third argument inside the addEventListener method.

// With addEventListener() method, you can specify the event phase by using `useCapture` parameter.
addEventListener(event, handler, useCapture);
Enter fullscreen mode Exit fullscreen mode

By default, it's false indicating that we are registering this event in the bubbling phase.
Let's modify our example above to understand this better.

// Setting "true" as the last argument to `addEventListener` will register the event handler in the capturing phase.
element1.addEventListener('click', () => console.log('element1 is clicked'), true);

// Whereas, omitting or setting "false" would register the event handler in the bubbing phase. 
element2.addEventListener('click', () => console.log('element2 is clicked')));
Enter fullscreen mode Exit fullscreen mode

We have added true for useCapture parameter indicating that we are registering our event handler for element1 in the capturing phase. For element2, omitting or passing false will register the event handler in the bubbling phase.

Now, if you click element2, you will see element1 is clicked is printed first followed by element2 is clicked. This is the capturing phase in action.

💡 In the event capturing phase, the event is first captured until it reaches the target element

Here's the diagram to help you visualise this easily:

Demonstrating the event flow in W3C event model

The event flow sequence is:

  1. The "click" event starts in capturing phase. It looks if any ancestor element of element2 has onClick event handler for the capturing phase.
  2. The event finds element1, and invokes the handler, printing out element1 is clicked.
  3. The event flows down to the target element itself(element2) looking for any other elements on its way. But no more event handlers for the capturing phase are found.
  4. Upon reaching element2, the bubbling phase starts and executes the event handler registered on element2, printing element2 is clicked.
  5. The event travels upward again looking for any ancestor of the target element(element2) which has an event handler for the bubbling phase. This is not the case, so nothing happens.

So, the key point to remember here is that the whole event flow is the combination of the event capturing phase followed by the event bubbling phase. And as an author of the event handler, you can specify which phase you are registering your event handler in. 🧐

With this new knowledge in our bag, it's time to look back to our first example and try to analyse why the output was in reverse order. Here's the first example again so that you're not creating a scroll event 😛

element1.addEventListener('click', () => console.log('element1 is clicked'));
element2.addEventListener('click', () => console.log('element2 is clicked'));
Enter fullscreen mode Exit fullscreen mode

Omitting the useCapture value registered the event handlers in the bubbling phase for both the elements. When you clicked element2, the event flow sequence was like:

  1. The "click" event starts in capturing phase. It looks if any ancestor element of element2 has onClick event handler for capturing phase and doesn't find any.
  2. The event travels down to the target element itself(element2). Upon reaching element2, the bubbling phase starts and executes the event handler registered on element2, printing element2 is clicked.
  3. The event travels upwards again looking for any ancestor of the target element(element2) which has an event handler for the bubbling phase.
  4. This event finds one on element1. The handler is executed and element1 is clicked is printed out.

Another interesting thing you can do is logging out the eventPhase property of the event. This helps you visualise which phase of the event is currently being evaluated.

element1.addEventListener("click", (event) =>
  console.log("element1 is clicked", { eventPhase: event.eventPhase })
);
Enter fullscreen mode Exit fullscreen mode

Here's the codepen demo if you like to play with it. Or you can paste the code snippet below in your browser and see it yourself.

const element1 = document.createElement("div");
const element2 = document.createElement("div");

// element1: Registering event handler for the capturing phase
element1.addEventListener(
  "click",
  () => console.log("element1 is clicked"),
  true
);

// element2: Registering event handler for the bubbling phase
element2.addEventListener("click", () => console.log("element2 is clicked"));

element1.appendChild(element2);

// clicking the element2
element2.click();
Enter fullscreen mode Exit fullscreen mode

Stopping the event propagation

If you wish to prevent further propagation of current event in any phase, you could invoke stopPropagation method available on the Event object.

So, it means invoking the event.stopPropagation() inside the element1 event handler (in capturing phase), would stop the propagation. And if even if you click element2 now, it won't invoke its handler.

The following example demonstrates that:

// Preventing the propagation of the current event inside the handler
element1.addEventListener(
  "click",
  (event) => {
    event.stopPropagation();
    console.log("element1 is clicked");
  },
  true
);
// The event handler for the element2 will not be invoked.
element2.addEventListener('click', () => console.log('element2 is clicked'));
Enter fullscreen mode Exit fullscreen mode

Note that event.stopPropagation stops the propagation only. It does not, however, prevent any default behaviour from occurring. For example, clicking on links are still processed. To stop those behaviours, you can use event.preventDefault() method.

Finally, here's another cool JSbin demo if you like to play along and see how can you stop the event propagation via event.stopPropagation.

I hope this article was helpful and has given you some insights. Thanks for reading 😍


Useful resources:

Top comments (15)

Collapse
 
jamesthomson profile image
James Thomson

TIL about eventPhase. It really doesn't matter how long you're in this profession, you really do learn something new every day.

Collapse
 
aman_singh profile image
Amandeep Singh

Totally agree on this one. 👍

Collapse
 
pgoldrbx profile image
Phil Gold

Thank you for this! I had a bad interview a month ago where the question came up as to how to execute the parent node handler first and despite 20 years of experience I really could not remember. There just hasn’t been a scenario I’ve needed to do this and my past reading of useCapture in the docs was not coming to me. Today I got the answer and it was a wonderful reminder. Thank you!

The only thing that could make this post better might be an example of how this might be useful. I can imagine some analytics cases but that’s not direct functionality.

Collapse
 
aman_singh profile image
Amandeep Singh

Thanks for your feedback. I am sorry to hear about your interview.

Regarding the capturing phase, I haven't seen/found any practical use cases of it yet.

But the bubbling phase is the core of event delegation.

Interestingly, React uses this mechanism to attach all of your event handlers at the root DOM container (v17).

Collapse
 
elabftw profile image
eLabFTW

What is interesting here is that Phil failed his interview for not remembering something that IS NOT USEFUL, or maybe is in some very weird corner case...

Anyway, nice article Amandeep!

Thread Thread
 
aman_singh profile image
Amandeep Singh

Thanks for your comment. 🙂👍

I am surprised that someone could test you based on if you know how event capturing works. These things are not what we do everyday and thus hard to come by, or you just forget. And that's why docs are there when you need a refresher.

I totally don't condone these interviews. I personally would stay away from these if I could.

Thread Thread
 
elabftw profile image
eLabFTW

Agreed. It's stupid to expect someone to know everything there is to know about web technologies. There is so much to know (and then more) that questioning someone without the ability to google something is pointless. A better test would be to look at how someone would solve a particular problem with all the tools at their disposal.

Thread Thread
 
jamesthomson profile image
James Thomson

A better test would be to look at how someone would solve a particular problem with all the tools at their disposal.

Exactly this. Testing how a candidate can memorise and regurgitate documentation is useless. Test how they solve problems and how they interact with you and your team. This is how you find good developers that your team can work with (which is just as, if not more, important as the candidates skillset).

Collapse
 
peerreynders profile image
peerreynders • Edited

Peculiar how React's synthetic event system ignores the handleEvent portion of the EventListener interface which allows objects to listen to events.

function makeListener(refs) {
  let stopGrandparent = refs.stopGrandparent.checked;
  let stopParent = refs.stopParent.checked;
  let stopClick = refs.stopClick.checked;

  const log = (msg) => {
    const current = refs.log.value;
    refs.log.value =
      current.length > 0 && msg.length > 0 ? current + `\n` + msg : msg;
  };

  return {
    handleEvent(e) {
      switch (e.currentTarget) {
        case refs.grandparent:
          if (e.type === 'click') clickGrandparent(log, stopGrandparent, e);
          break;

        case refs.parent:
          if (e.type === 'click') clickParent(log, stopParent, e);
          break;

        case refs.clickMe:
          if (e.type === 'click') clickButton(log, stopClick, e);
          break;

        case refs.stopGrandparent:
          if (e.type === 'change') stopGrandparent = e.target.checked;
          break;

        case refs.stopParent:
          if (e.type === 'change') stopParent = e.target.checked;
          break;

        case refs.stopClick:
          if (e.type === 'change') stopClick = e.target.checked;
          break;

        case refs.clear:
          if (e.type === 'click') log('');
          break;
      }
    },
  };
}

function initialize(grandparent) {
  const root = grandparent.parentElement;
  const parent = root.querySelector('.js-parent');
  const clickMe = root.querySelector('.js-click-me');
  const [stopGrandparent, stopParent, stopClick] = Array.from(
    document.querySelectorAll('input[type="checkbox"]')
  );
  const clear = root.querySelectorAll('button')[1];
  const log = root.querySelector('textarea');
  const refs = {
    parent,
    grandparent,
    clickMe,
    stopGrandparent,
    stopParent,
    stopClick,
    clear,
    log,
  };

  const listener = makeListener(refs);

  grandparent.addEventListener('click', listener, { capture: true });
  parent.addEventListener('click', listener);
  clickMe.addEventListener('click', listener);
  stopGrandparent.addEventListener('change', listener);
  stopParent.addEventListener('change', listener);
  stopClick.addEventListener('change', listener);
  clear.addEventListener('click', listener);
}
Enter fullscreen mode Exit fullscreen mode

JS Fiddle

Seems Preact is looking into adding support for it (see also radEventListener: a Tale of Client-side Framework Performance).

Collapse
 
white_shadow profile image
Abhijeet

Wow...really helpful👍

Collapse
 
sreepati profile image
Sreepati

wow didn't have any idea about this. Thanks.

Collapse
 
jwp profile image
John Peters

Love this article Clear, Complete and Consise.

Collapse
 
aman_singh profile image
Amandeep Singh

Thank you. Glad that it was helpful 🙂

Collapse
 
brettcnelson profile image
𝙱𝚛𝚎𝚝𝚝

Very helpful post, thanks. Should the last comment in the last code block say element2?

Collapse
 
aman_singh profile image
Amandeep Singh

Thanks for the feedback and that Typo 🙂👍. Cheers