Skip to content

Instantly share code, notes, and snippets.

@domfarolino
Last active February 26, 2022 19:14
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save domfarolino/fdde99c1ad3fa1668a1849c33f87f437 to your computer and use it in GitHub Desktop.
Save domfarolino/fdde99c1ad3fa1668a1849c33f87f437 to your computer and use it in GitHub Desktop.
JavaScript Event Flow - A better understanding

This gist is deprecated and now exists here

This article is intended to further your knowledge of the various phases a DOM event goes through.

Great resources to refer to:

TL;DR

Experiment with this HTML file to learn about the JavaScript event flow.

Event Flow

When you click an element that is nested in various other elements, before your click actually reaches its destination, or target element, it must trigger the click event each of its parent elements first, starting at the top with the global window object. The click event trickles down parent by parent in what is called the capture phase until it finally arrives at the event's intended target element.

When the event can finally be handled by its intended target, we enter the target phase. The target phase is really just the very end of the capture phase and the very beginning of the bubble phase. Once the target phase is complete and the target element handles the event however it wishes, we begin the third and final phase of the event flow called the bubble phase. In this phase the event "bubbles" up from the event target through all of its parent elements back to the global window object. This is essentially the opposite of the capture phase.

How can I use this?

Let's take a look at some generic code you've probably seen before that registers an event listener on some DOM element.

document.getElementById('target-element').addEventListener('click', function(e) {
  // Handle click appropriately
});

This almost always does exactly what you want, however there is much to learn when exploring how exactly the default addEventListener() code above works with the aforementioned event phases. Let's consider the following HTML:

<div id="parent-element">
  <div id="target-element">
    Yay, some target text...
  </div>
</div>

If I were to register click event listeners on both the parent and target elements above that say do a simple console.log, we might see the following output after clicking the target-element directly:

target-element event listener triggered
parent-element event listener triggered

Hmm, since the parent-element's click listener got triggered after the target-element's it is safe to say that addEventListener()'s default usage registers a listener on the event's bubble phase. So how, you might ask, can we register an event listener on the event's capture phase? Well, EventTarget.addEventListener() actually has a neat optional third parameter you can pass in to indicate which phase you'd like to be listening too. The API looks like this:

target.addEventListener(type, listener[, useCapture]);

If you pass nothing in as the boolean useCapture argument, it defaults to false, thus registering an event listener on the event's bubble phase.

event.stopPropagation()

The event API makes it possible for you, the developer, to stop an event dead in its tracks to ensure it never completes the current phase it's on. By calling event.stopPropagation() you stop the event from moving onto the next element in its propagation path. This works with both the capture and bubble phases. To be clear, event.stopPropagation() will stop an event from traversing closer to its target if called in the capture phase, and stop an event from bubbling up to any parent elements if called in the bubble phase.

What good is any of this?

Say you have event listeners registered with all kinds of elements on your web page however you only want the logic in each of the event listeners to be fired if the user intended on triggering the event on that specific element. It is probably clear now that clicking on some nested element is essentially the same as clicking on all of its parents too. If you want to avoid triggering your logic for all of these collateral clicks, we can utilize the event object's API!

You can simply check your current element against event.target to see if the passed in event was truly intended for the current element on which the event was triggered, or if the event is just making its rounds through its propagation path to get to or from its target. This may look something like this:

document.getElementById('parent-element').addEventListener('click', function(e) {
  if (this !== event.target) return; // avoid collateral clicks from event flow

  // All kinds of logic down here that should only be hit
  // If the event was originally intended for this the `parent-element`
  // .....
}, true);

Caution with ES6 arrow functions

This is all neat and fancy stuff and if you're feeling extra fancy you may want to use ES6 style arrow functions as your second argument in a call to addEventListener() however I'd strongly recommend against this practice. When using call-back style functions it is often very helpful to have the this argument dynamically bound to whichever object the call-back is referring to.

ES6 arrow functions are not capable of dynamically binding this, therefore when using an arrow function as a callback in addEventListener() or something similar, this will often be statically set to the global window object in this case, or some outer origin element in others. This implicitly prevents you from performing the basic this !== event.target check we saw before since this will usually never be what you expect it to be! Note this does not make a difference when you're registering event listeners on the window object itself. To see more instances where ES6 arrow functions can be dangerous read this article.

Homework

I strongly urge you to experiment with this stuff. To get started feel free to check out this basic HTML file I've created. Open up the console and run it in your favorite browser. Happy coding!

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JavaScript Event Flow</title>
<link href="https://fonts.googleapis.com/icon?family=Roboto" rel="stylesheet">
</head>
<body>
<div id="parent-element">
<p id="target-element">
Yay I'm the child
</p>
</div>
<style>
body {
font-family: Roboto;
font-size: 20px;
background-color: #607D8B;
}
#parent-element {
width: 200px;
height: 200px;
padding: 10px;
margin: 0 auto;
background-color: #00695C;
}
#target-element {
width: 100px;
height: 100px;
padding: 10px;
margin: 35px auto;
background-color: #00897B;
}
</style>
<script>
// Window event listener 'click' on Capture Phase
window.addEventListener('click', function(e) {
console.log('Capturing, window got clicked!');
//e.stopPropagation();
}, true);
// Window event listener 'click' defaults to Bubble Phase
window.addEventListener('click', function(e) {
console.log('Bubbling, window got clicked!');
//e.stopPropagation();
}/*, false*/);
// parent-element event listener 'click' defaults to Bubble Phase
document.getElementById('parent-element').addEventListener('click', function(e) {
console.log(' Capturing, parent div got clicked!');
//e.stopPropagation();
}, true);
// parent-element event listener 'click' defaults to Bubble Phase
document.getElementById('parent-element').addEventListener('click', function(e) {
// if (this !== e.target) return; // try this with an arrow function!
console.log(' Bubbling, parent div got clicked!');
//e.stopPropagation();
}/*, false*/);
// target-element event listener 'click' defaults to Capture Phase
document.getElementById('target-element').addEventListener('click', function(e) {
console.log(' Capturing, FINALLY target div got clicked!');
//e.stopPropagation();
}, true);
// target-element event listener 'click' defaults to Bubble Phase
document.getElementById('target-element').addEventListener('click', function(e) {
console.log(' Bubbling, FINALLY target div got clicked!');
//e.stopPropagation();
}/*, false*/);
</script>
</body>
</html>
@towry
Copy link

towry commented Oct 31, 2017

Hi, so how is event.target being known in the capture phase if I stop the propagation in the top event handler ?

@shankarshastri
Copy link

Use this link to try out the above HTML.
https://jsfiddle.net/f9a9c6nv/

@abhay1483
Copy link

Hi, so how is event.target being known in the capture phase if I stop the propagation in the top event handler ?

An event is a message that is dispatched to event target's. So the dispatched event would already have the info about its target element. @towry

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment