JavaScript: Events — Event Propagation

JavaScript Event Propagation

Learning outcomes:

  • What is event propagation
  • The lifecycle of an event and its four phases
  • Event capturing and bubbling
  • The stopPropagation() method and its application
  • The stopImmediatePropagation() method
  • The currentTarget property

Introduction

In the previous chapter, we covered event objects in great detail, where we also came across the Event interface. There were a handful of properties and methods that we skipped in the chapter as they were all related to the idea of event propagation.

Now, in this chapter, we aim to decipher this strange-sounding term and explain the significance it holds as we develop complex JavaScript applications.

In particular, we'll see what exactly is meant by event propagation, how it's done in two stages, i.e. capturing and then bubbling, and some methods of the Event interface tied to it. Propagation is more than just important for every JavaScript developer to be aware of so make sure to follow along every detail carefully.

The good news is that propagation is really really simple to understand. Like seriously!

So what do you say, shall we begin learning?

What is event propagation?

Before we make intuition of the term "event propagation", let's first understand the meaning of just the word 'propagation'.

If we look up the meaning of 'propagation' in a dictionary, it turns out to be synonymous to the words 'spreading' and 'transmission'. You know, like the propagation, i.e. the transmission, of sound waves through air.

So does this imply that event propagation somehow refers to the 'transmission' of an event?

Absolutely yes!

So defining it formally:

Event propagation is the process of transmitting an event across a series of objects.

But what exactly is this 'series' that we're referring to?

To answer this, we'll have to deep dive into the concept itself.

Consider the example below:

HTML
<div>
   A div with...
   <p>A paragraph with a <button>simple button</button></p>
</div>

A <div> holds some text and a <p> element which then holds some further text and a <button> element. Not really complicated, is it?

Just to remind it, the HTML code above is assumed to be nested directly inside the <body> element.

Now, if we were to set up a click handler on the <div> container, as shown below:

JavaScript
var divElement = document.querySelector('div');

divElement.onclick = function() {
   alert('Div clicked');
}

what do you think would happen if we clicked the button?

Well, we'd see an alert. Yes, but why exactly? We just clicked the button, so shouldn't only button's own click handler be invoked rather than that of the <div>?

Well, when when we click the button, we click the containing <div> as well (the <button> is inside the <div> and so clicking it has to translate to a click of the <div> as well), and so likewise clicking the button should indeed invoke the click handler of <div>.

Now what just happened here is an instance of propagation in action.

The fired click event literally got propagated, i.e. transmitted, from the button all the way through its chain of ancestors, being fired on each and every object in this chain. This obviously includes the <div> ancestor of the button.

Speaking a lot more precisely, once the button is clicked, the click event gets fired on,

  • the <button> element, i.e. the target of the event;
  • then on its parent, i.e. the <p> element;
  • then on the <div> element;
  • then on the <body> element;
  • then on the document object;
  • and then finally on window;

before reaching the end of its propagation.

Yup, all that does happen each time when we make a gesture as simple as a click on a webpage! The browser handles it so efficiently that we don't even get to see a slight glitch. And that's purely amazing.

So to review the idea, propagation is the act of an event being transmitted across a series of objects, which is simply the ancestor chain of the event's target, including the target object itself.

Note that this is just a simplified overview of propagation in action. There are numerous ways in which we could tap into this series of objects through which a given event propagates; retrieve it programatically; prevent the event's transmission beyond a certain point; see if propagation has been stopped or not; and much more.

The following sections are all devoted to exploring these different aspects of propagation.

Phases of an event

The event propagation example we saw above was precisely an instance of event bubbling. Bubbling is one way of propagation. There is yet another way: capturing.

So what is capturing? In one line, it's the reverse of bubbling.

And what is bubbling? That's just what we saw above.

And what we saw above? Let's recall it.

In the example above, we saw that upon clicking the button, the dispatch of the click event began on <button> and ended on window, while traversing every parent in between. This was actually just one part of the complete picture.

The complete picture is yet to be covered.

As per the standard, when an event occurs in JavaScript, first the event's path is determined and then the event is fired one-by-one on each object in this path. So before we could understand bubbling and capturing, we have to familiarize ourselves with what exactly is the path.

Let's understand it.

The path of an event is computed right when the event occurs. It's simply a list of the ancestors of the target of the event, including the target itself.

As per the standard, the first element of the path is the event's target. The second element is then the parent of this target; the third element is the parent of this parent; and so on and so forth.

Once an event's path is computed, the next step is to fire the event on each object in this path. This obviously requires a run over the path list.

But there isn't just one single run over the path — there are actually two of them. The first run (in reverse order) is for the capturing stage while the second run (in normal order) is for the bubbling stage.

And now is the high time to introduce the idea of an event's phase.

Throughout its lifecycle, an event can be in either of the following 4 states, also known as its phases:

  1. The 'none' phase is when event hasn't yet been fired on any object.
  2. The 'capturing' phase is when the event is being propagated in the capturing stage.
  3. The 'bubbling' phase is when the event is being propagated in the bubbling stage.
  4. The 'target' phase is when the event is fired on its target.

Let's see how an event goes through each of these 4 phases.

The lifecycle of an event

The moment an event is instantiated (i.e. an event object created), it is in the 'none' phase. During this phase, it's clear to us that the event hasn't yet been fired on any target object whatsoever.

Then the event's path is computed, as described above.

During the first run over the computed path, which is done in reverse order, as described before, the event transitions to the 'capturing' phase.

Finally when the iteration comes to the target on which the event occurred (during this first run), which is the first item of the path, the event's phase transitions again, this time to the 'target' phase.

The 'target' phase simply implies that the event is being fired at its target (i.e. the object on which it actually occurred).

With this done, the first run over the event's path, which is propagation via capturing, gets completed. Now, it's time for the second run.

In the second run over path, which is in the normal order, the event is bubbled. The iteration obviously begins with the event's target (recall that the target is the first element of the path), and so at this stage, akin to the run for capturing, the phase is set to 'target'.

After this, the phase gets transitioned to 'bubbling' and thus the event goes up and up until it touches the very last ancestor of its target (which, in the case of user-interface events, is typically window).

The Event interface provides us with a really handy property called eventPhase to inspect a given event's current phase.

eventPhase holds a number representing the phase that an event is currently in. The number can be easily compared against phase constants provided as static members of the Event interface, as shown below:

  • Event.NONE — holds the number 0 representing the 'none' phase.
  • Event.CAPTURING_PHASE — holds the number 1 representing the 'capturing' phase.
  • Event.AT_TARGET — holds the number 2 representing the 'target' phase.
  • Event.BUBBLING_PHASE — holds the number 3 representing the 'bubbling' phase.

We'll see examples of using these properties along with eventPhase in the following sections.

So now that we understand what exactly event phases are, and what's capturing and bubbling in this regard, it's time to solely focus on both of these stages of propagation in more detail along with the 'target' phase that an event transitions to during both of them.

Event capturing

Due to the fact that it's put into action first, we'll start by digging deeper into the capturing stage of an event's propagation in this section, followed by exploring the bubbling stage in the next one.

Defining it precisely:

Capturing is when an event is dispatched starting from its top-most ancestor, and then being propagated all the way down to the very target of the event.

For instance, if we click <body>, propagation via capturing would mean that first the click handler of window would get executed, then the handler of document, and then finally that of <body>.

In capturing, the target of the event is dealt with last whereas the top-most ancestor of the target is dealt with first.

Remember that in capturing, the run over the event's path begins in reverse order, starting from the very last element in the path, which is simply the target's top-most ancestor.

Now, if you think about it carefully, most of the times, if not every single time, capturing doesn't make sense.

For example, when we click a button, nested inside a <p> which is then nested inside a <div>, we expect the button's click handler to be triggered first, since it's what actually receives the click action directly. It seems quite counter-intuitive for the click action on button to trigger the click handler of window first, as it happens in the capturing stage.

And this is exactly the reason why almost all event-handling mechanisms in JavaScript, by default, apply to the bubbling phase, NOT to the capturing phase.

But what does it mean to 'apply to the bubbling phase'?

Well, in the dicussion above, we learnt that the moment an event occurs, its path is computed, and then the event is fired in two propagative stages: capturing followed by bubbling. In each stage, each object in the path receives the event, which ultimately triggers its registered set of handlers. Right?

But how do you think does the engine determine whether to call the handler in the capturing stage or the bubbling stage?

That's actually implicitly done by the engine when we set up event handlers using either of the three event handler–registering mechanisms we discussed before (i.e. event-handling properties, the addEventListener() method, and event-handling HTML attributes).

In particular, all event-handling mechanisms allows us to set up event handlers that only apply to the bubbling phase, with the exception of addEventListener().

Only addEventListener() allows us to set up an event handler to be executed in the capturing stage. And that's done with the help of its third options argument.

When we set the third argument of addEventListener() to an object containing the capture property, assigned the value true, the given handler function gets registered for the capturing stage, i.e. it'll get executed while the event is propagated via capturing.

Let's try an example.

In the code below, we have the same HTML code as before:

HTML
<div>
   A div with...
   <p>A paragraph with a <button>simple button</button></p>
</div>

However, the JavaScript is a little bit different, and involves multiple handlers instead of just one:

JavaScript
var divElement = document.querySelector('div');
var buttonElement = document.querySelector('button');

window.addEventListener('click', function(e) {
   console.log('window');
}, { capture: true });

document.body.addEventListener('click', function(e) {
   console.log('body');
}, { capture: true });

divElement.addEventListener('click', function(e) {
   console.log('div');
}, { capture: true });

buttonElement.addEventListener('click', function(e) {
   console.log('button');
}, { capture: true });

Notice how each handler is set up using addEventListener() and registered for the capturing stage.

Stating it once more, addEventListener() is the only way to register an event handler for the capturing stage.

Live Example

Now, if we go on and click the <button>, here's what'll get logged in the console:

window body div button

As you can see, the logs start from the handler of window and go all the way down to the handler of <button>. It's like literally we're trying to capture the target.

To make the output a bit more informative, we could even log the phase that the fired event is in, during each handler's execution, with the help of the familiar eventPhase property.

Let's try this:

JavaScript
var divElement = document.querySelector('div');
var buttonElement = document.querySelector('button');

window.addEventListener('click', function(e) {
console.log('window', e.eventPhase); }, { capture: true }); document.body.addEventListener('click', function(e) {
console.log('body', e.eventPhase); }, { capture: true }); divElement.addEventListener('click', function(e) {
console.log('div', e.eventPhase); }, { capture: true }); buttonElement.addEventListener('click', function(e) {
console.log('button', e.eventPhase); }, { capture: true });

Live Example

Let's see the output now:

window 1 body 1 div 1 button 2

We do seem to get some kind of a different output, but it's not understandable right away.

Recall that eventPhase returns back a number and so, making sense out of it won't be an intuitive experience. Fortunately, as we mentioned beforehand, the Event interface provides constants to help us demystify the meaning of a given eventPhase value.

So what do you think could we do to make the cryptic numbers above more meaningful?

Well, we could simply create a function that maps a number to a string representing the phase's name. Simple.

We define this function below and consequently use it across all our event handlers:

JavaScript
function getPhaseName(eventPhase) {
   switch (eventPhase) {
      case Event.NONE:
         return 'none';
      case Event.CAPTURING_PHASE:
         return 'capturing';
      case Event.AT_TARGET:
         return 'target';
      case Event.BUBBLING_PHASE:
         return 'bubbling';
   }
}
JavaScript
var divElement = document.querySelector('div');
var buttonElement = document.querySelector('button');

window.addEventListener('click', function(e) {
console.log('window', getPhaseName(e.eventPhase)); }, { capture: true }); document.body.addEventListener('click', function(e) {
console.log('body', getPhaseName(e.eventPhase)); }, { capture: true }); divElement.addEventListener('click', function(e) {
console.log('div', getPhaseName(e.eventPhase)); }, { capture: true }); buttonElement.addEventListener('click', function(e) {
console.log('button', getPhaseName(e.eventPhase)); }, { capture: true });

Live Example

Now, let's try clicking the button and then witnessing the console logs again:

window capturing body capturing div capturing button target

Yup, a lot more readable now!

As is evident here, during the execution of the first three handlers (i.e. of window, <body> and <div>), the fired click event is in its 'capturing' phase. Then when the handler of <button> itself is executed, the event transitions to its 'target' phase, ultimately completing the capturing stage.

Simple?

Why is capturing even there if it doesn't make much sense?

The provision of two different ways of propagating an event in JavaScript isn't precisely a remarkable feature of the language, so to speak. It's purely a, yet another, step towards fostering backwards-compatibility with ancient browsers!

In the legacy DOM era, Netscape and Internet Explorer — the rivals of the time — both had different models of propagating an event. Netscape used capturing while IE used bubbling. Frankly speaking, bubbling seems much more logical than capturing and is, therefore, the default propagation mechanism adopted by JavaScript.

Event bubbling

Bubbling is the talk of the town when it comes to event propagation. It makes perfect sense for an event to start being fired right from its target and then sequentially going all the way up through its chain of ancestors.

As before, defining it precisely:

Bubbling is when an event is dispatched starting from its target, and then being propagated all the way up to the top-most ancestor of its target.

By default, all event handler–registering mechanisms set up event handlers for the bubbling stage.

As with capturing, let's consider a fully-fledged example to help illustrate bubbling even better.

Here's the HTML markup, as before:

HTML
<div>
   A div with...
   <p>A paragraph with a <button>simple button</button></p>
</div>

And here's the JavaScript:

JavaScript
var divElement = document.querySelector('div');
var buttonElement = document.querySelector('button');

window.addEventListener('click', function(e) {
   console.log('window');
});

document.body.addEventListener('click', function(e) {
   console.log('body');
});

divElement.addEventListener('click', function(e) {
   console.log('div');
});

buttonElement.addEventListener('click', function(e) {
   console.log('button');
});

Live Example

Everything is the same as before, just the third argument to addEventListener() is omitted, as we want the handlers to be registered for the default bubbling stage.

Now, the moment we click the button, here's what gets displayed in the console:

button div body window

The logs clearly show that the bubbling begins with the button's click handler and goes up to window (in fact, it ends on window).

Let's also inspect the eventPhase in each handler, with the help of our very own getPhaseName() function:

JavaScript
var divElement = document.querySelector('div');
var buttonElement = document.querySelector('button');

window.addEventListener('click', function(e) {
   console.log('window', getPhaseName(e.eventPhase));
});

document.body.addEventListener('click', function(e) {
   console.log('body', getPhaseName(e.eventPhase));
});

divElement.addEventListener('click', function(e) {
   console.log('div', getPhaseName(e.eventPhase));
});

buttonElement.addEventListener('click', function(e) {
   console.log('button', getPhaseName(e.eventPhase));
});

Live Example

Here's the output produced the moment we click the button:

button target div bubbling body bubbling window bubbling

At the start, when the button's handler is under execution, the event is in its 'target' phase. But then it transitions to the 'bubbling' phase and continues in it until it reaches window, at which point, the propagation comes to an end.

Simple?

The stopPropagation() method

Uptil this point in this chapter, we've seen tons of details on what propagation is and how it works. But we still haven't talked about tapping into it and possibly preventing it from happening in the very first place.

In this section, we'll explore this aspect of handling events in JavaScript, i.e. preventing them from being propagated beyond a certain point.

The Event interface defines a super useful method called stopPropagation() which aims to prevent an event's propagation further downstream or upstream, depending on when it's invoked. As one can guess, it doesn't require any arguments.

The moment stopPropagation() is called on an event, it puts a halt to the whole propagation chain.

  • If it's called in the capturing stage, further capturing is stopped and even the bubbling stage gets ignored.
  • Similarly, if it's called in the bubbling stage, further bubbling is stopped.

Let's consider an example.

Below we have the familiar HTML setup that we saw in the section above:

HTML
<div>
   A div with...
   <p>A paragraph with a <button>simple button</button></p>
</div>

And here's the JavaScript:

JavaScript
var divElement = document.querySelector('div');
var buttonElement = document.querySelector('button');

window.addEventListener('click', function(e) {
e.stopPropagation(); console.log('window'); }, { capture: true }); document.body.addEventListener('click', function(e) { console.log('body'); }, { capture: true }); divElement.addEventListener('click', function(e) { console.log('div'); }, { capture: true }); buttonElement.addEventListener('click', function(e) { console.log('button'); }, { capture: true });

Live Example

As you can recognize, you've already seen this code in the section above. The only difference is the call to stopPropagation() inside the handler of window.

By virtue of this call, we expect the propagation (of a dispatched click event) beyond the window handler to be stopped, and likewise, the last three handlers to be skipped.

Let's click the button and see what actually happens:

window

Yeah, the outcome is just as we expected.

stopPropagation(), inside window's click handler, effectively prevented the event from being propagated further downstream in the capturing stage.

As stated before, calling stopPropagation() in the capturing stage not only gets the remaining capturing stage to be skipped, but also gets the bubbling stage of the event's dispatch to be ignored. In the example above, we weren't able to confirm this, simply because all the handlers were registered for the capturing stage.

To confirm this idea, we ought to set up handlers for both the stages.

That's exactly what we do below:

JavaScript
var divElement = document.querySelector('div');
var buttonElement = document.querySelector('button');

// Capturing stage handlers.
window.addEventListener('click', function(e) {
   console.log('window');
}, { capture: true });

document.body.addEventListener('click', function(e) {
   console.log('body');
}, { capture: true });

divElement.addEventListener('click', function(e) {
   console.log('div');
}, { capture: true });

buttonElement.addEventListener('click', function(e) {
   console.log('button');
}, { capture: true });


// Bubbling stage handlers.
window.addEventListener('click', function(e) {
   console.log('window');
});

document.body.addEventListener('click', function(e) {
   console.log('body');
});

divElement.addEventListener('click', function(e) {
   console.log('div');
});

buttonElement.addEventListener('click', function(e) {
   console.log('button');
});

First, let's try this example without calling stopPropagation() anywhere in any handler.

Live Example

Here's the output produced after clicking the button:

window body div button button div body window

The first four logs are from the capturing stage, whereas the last four logs are from the bubbling stage. Quite basic.

Now, let's bring back the call to stopPropagation(), but this time not inside the capturing-handle of window, but rather inside the capturing-handler of <body>, and see its effect:

JavaScript
var divElement = document.querySelector('div');
var buttonElement = document.querySelector('button');

// Capturing stage handlers.
window.addEventListener('click', function(e) {
   console.log('window');
}, { capture: true });

document.body.addEventListener('click', function(e) {
e.stopPropagation(); console.log('body'); }, { capture: true }); ...

Live Example

As the following output shows,

window body

the call to stopPropagation() inside the capturing-handler of <body> effectively limits the number of logs to 2, skipping the remaining capturing-stage handlers and all of the bubbling-stage handlers.

Not really that difficult to understand, was this?

Keep in mind that, as with preventDefault(), it isn't possible to reverse the effect of stopPropagation(). That is, once it's called, there is absolutely no way to get the propagation to resume as usual. Hence, use stopPropagation() carefully!

At times, stopPropagation() can prove to be really really useful. Let's consider one such realistic example where we could leverage its true potential.

An application of stopPropagation()

Suppose we have a dropdown as shown below:

HTML
<div class="dropdown-wrapper">
   <button class="dropdown-button">See dropdown ⬇</button>
   <div class="dropdown">
      <p>This is a dummy dropdown.</p>
   </div>
</div>

Live Example

The idea is to show the dropdown — that is, the .dropdown element — when the button is clicked and then hide it if a click is made anywhere outside the dropdown, including the button.

You might've seen such dropdowns quite a lot across the web. They are pretty common in applications. Now, a usual feature of dropdowns is that to close them, we could click anywhere outside. (Some dropdown implementations even provide explicit buttons to hide a given dropdown, or listen for the press of an Esc key on the keyboard to hide it.)

Showing the dropdown upon the button's click is pretty simple. It's accomplished as follows:

JavaScript
var dropdownButtonElement = document.querySelector('.dropdown-button');
var dropdownElement = document.querySelector('.dropdown');

dropdownButtonElement.onclick = function(e) {
   dropdownElement.classList.add('dropdown--shown');
}

The addition of the class 'dropdown--shown' displays the dropdown.

Let's try the feature thus far.

Live Example

So far, so good.

Moving on, the next concern of hiding the dropdown when a click is made outside it could easily be addressed by setting a click handler on window. The idea is simple: just remove the .dropdown--shown class from the dropdown inside this handler, and you're good to go.

Let's set up the handler:

JavaScript
var dropdownButtonElement = document.querySelector('.dropdown-button');
var dropdownElement = document.querySelector('.dropdown');

dropdownButtonElement.onclick = function(e) {
   dropdownElement.classList.add('dropdown--shown');
}

window.onclick = function(e) {
   dropdownElement.classList.remove('dropdown--shown');
}

Time to try the example again.

Live Example

Something has gone wrong. When we click on the button, the dropdown doesn't show up.

Why do you think might this be happening?

Well, the issue is coming from the newly-added window's click handler.

Here's how the issue pops up: when the button is clicked, its handler executes, consequently setting up the given class on the .dropdown element. But then, because of bubbling, the same click action also gets the handler of window to be executed, which then removes the added class.

The ultimate consequence is that the .dropdown element doesn't get any class added to it upon the button's click.

The solution is dead simple: to stop the bubbling beyond the button's handler.

The code below accomplishes this:

JavaScript
var dropdownButtonElement = document.querySelector('.dropdown-button');
var dropdownElement = document.querySelector('.dropdown');

dropdownButtonElement.onclick = function(e) {
e.stopPropagation(); dropdownElement.classList.add('dropdown--shown'); } window.onclick = function(e) { dropdownElement.classList.remove('dropdown--shown'); }

Time for testing.

Live Example

At least, when we click the button, the dropdown does show up, which is awesome. But, there still is a problem.

When we click anywhere outside the dropdown, after it's shown, it goes get hidden. But if we try clicking inside the dropdown, it faces the same fate. Ideally, we don't want to hide the dropdown if the click originates inside itself.

So how to fix this problem?

Well, you guessed it — using stopPropagation().

The problem is that when a click is made inside the dropdown, it once again gets bubbled up to window, at which point it causes the execution of window's click handler, which then hides the dropdown. The solution to this problem is simply to stop the bubbling when the click gets made inside the dropdown.

So, below we set up a click handler on the .dropdown element and then call stopPropagation() inside it:

JavaScript
var dropdownButtonElement = document.querySelector('.dropdown-button');
var dropdownElement = document.querySelector('.dropdown');

dropdownButtonElement.onclick = function(e) {
   e.stopPropagation();
   dropdownElement.classList.add('dropdown--shown');
}

dropdownElement.onclick = function(e) {
e.stopPropagation();
} window.onclick = function(e) { dropdownElement.classList.remove('dropdown--shown'); }

Now, let's try the example again.

Live Example

Better still. Just one last problem.

When the button is clicked, the dropdown indeed gets displayed. But when the button is clicked again, at this stage, the dropdown doesn't get hidden. This is the problem.

Ideally, we want the button to act as a toggler, i.e. if the dropdown is shown, it must hide it, or else it must show it.

Now how to solve this one?

Well, this time, we don't need stopPropagation(), but rather some pure logical reasoning to solve the problem at hand, which is, in fact, already hinted in the paragraph above.

We literally need to make the button act as a toggler for showing/hiding the .dropdown element, and that could be achieved by none other than the toggle() method of classList.

Below shown is the new version of the click handler of the button:

JavaScript
dropdownButtonElement.onclick = function(e) {
   e.stopPropagation();
dropdownElement.classList.toggle('dropdown--shown'); }

Let's not forget to test the dropdown.

Live Example

And it works flawlessly.

Note that calling stopPropagation() isn't the only way to accomplish the aforementioned dropdown-hiding behavior.

Another option, although not as simple as calling stopPropagation(), is to perform a check inside window's click handler to see if the target of the click is a descendant of the .dropdown element (similar to what we did in the JavaScript Events — Div Clicked? exercise).

If it doesn't turn out to be a descendant of the .dropdown element, that means that we clicked outside the dropdown and so, we must continue with hiding it.

The stopImmediatePropagation() method

Recall from the JavaScript Events — Basics chapter that addEventListener() could be used to set up multiple click handlers on a given target. And when a target does have multiple event handlers registered, they're all called in the order they were originally set up in, the moment the event gets dispatched on the target.

So for instance, if we have a button with three click handlers, the click event's occurrence on the button would invoke all three of these handlers, one-by-one.

The method stopPropagation(), called from within any of these event handlers, would only stop the event from propagating to other objects in the chain — it won't stop the event from travelling to the rest of the handlers in the current object.

For this, we have the provision of stopImmediatePropagation() on the Event interface.

The purpose of stopImmediatePropagation() is to do two things:

  1. Stop the propagation of the event to other objects in the event's path, akin to stopPropagation().
  2. Stop other listeners registered on the current object from being invoked.

Let's consider a quick and simple example.

Here's the HTML (you're already familiar with it):

HTML
<div>
   A div with...
   <p>A paragraph with a <button>simple button</button></p>
</div>

And here's the JavaScript:

JavaScript
var divElement = document.querySelector('div');
var buttonElement = document.querySelector('button');

window.addEventListener('click', function(e) {
   console.log('window');
});

document.body.addEventListener('click', function(e) {
   console.log('body');
});

divElement.addEventListener('click', function(e) {
e.stopImmediatePropagation(); console.log('div 1'); }); divElement.addEventListener('click', function(e) { console.log('div 2'); }); divElement.addEventListener('click', function(e) { console.log('div 3'); }); buttonElement.addEventListener('click', function(e) { console.log('button'); });

As you can see, we've set up three click handlers on the <div> element, calling stopImmediatePropagation() in the first one.

This should stop the event from being bubbled further upstream and even the rest of the two click handlers of <div> from firing.

Let's witness the effect for real.

Live Example

Here's the output produced after we click the button:

button div 1

The output exactly matches our expectations. Superb!