Bubbling and Capturing

Let’s start with an example. This handler is assigned to <div>, but also runs if you click any nested tag like <em> or <code>:

<div onclick="alert(event.target.tagName)">
  <em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em>
</div>
If you click on EM, the handler on DIV runs.

Isn’t it a bit strange? Why the handler on <div> runs if the actual click was on <em>?

Bubbling

When an event happens on an element, it first runs the handlers on it, then on its parent, then all the way up on other ancestors.

Let’s say, we have 3 nested elements FORM > DIV > P with a handler on each of them:

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>

<form onclick="alert('form')">FORM
  <div onclick="alert('div')">DIV
    <p onclick="alert('p')">P</p>
  </div>
</form>
FORM
DIV

P

A click on the inner <p> first runs onclick:

The process is called “bubbling”, because events “bubble” from the inner element up through parents like a bubble in the water.

Almost all event bubbles

For instance, a focus event does not bubble. There are other examples too, we’ll meet them. But still it’s an exception, rather than a rule, most events do bubble.

event.target

A handler on a parent element can always get the details about where the event actually happened.

The most deeply nested element that caused the event is called a target element, accessible as event.target. Here is the difference between this and event.target -

event.currentTarget

Using this you can always get the element on which the handler runs. There is an alternative way to access it - event.currentTarget

	this = event.currentTarget;

Please note that it's possible that sometimes event.target is equals to event.currentTarget, if the clicked element is the one on which the handler is assigned to.

Stopping Bubbling : stopPropagation()

A bubbling event goes from the target element straight up. Normally it goes upwards till <html>, and then to document object, and some events even reach window, calling all handlers on the path.

But any handler may decide that the event has been fully processed and stop the bubbling. The method for it is event.stopPropagation(). Here is an example -

<p id="parent" onclick="parentHandler(event)">
	<code onclick="childHandler(event)">Lorem ipsum dolor sit amet, consectetur adipisicing elit</code>, sed do eiusmod
	tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
	quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
	consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
	cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
	proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<script type="text/javascript">
function parentHandler(event){
	alert("I am a Parent");
}
function childHandler(event){
	alert("I am a Child");
	event.stopPropagation();
}
</script>

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

event.stopImmediatePropagation()

Consider the following example -

<p id="parent1">
	<code id="child1">Lorem ipsum dolor sit amet, consectetur adipisicing elit</code>, sed do eiusmod
	tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
	quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
	consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
	cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
	proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<script type="text/javascript">
parent1.addEventListener("click", function(event){
	alert("I am in Parent !");
});
child1.addEventListener('click', function(event){
	alert("I am in Child !");
	event.stopPropagation();
});
child1.addEventListener("click", function(event){
	alert("Thank you for Clicking Me !");
});
</script>

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

In the above example, we have two handlers for click events. One of them stop bubbling. It means any handler on the parent element won't run. But if you notice, it doesn't stop the execution of other handlers on the same element. That's why you will get two alert. What if you want to stop the bubbling as well as other event handlers from being executed? The method stopImmediatePropagation() does exactly that.

Here is the same example with stopImmediatePropagation() -

<p id="parent2">
	<code id="child2">Lorem ipsum dolor sit amet, consectetur adipisicing elit</code>, sed do eiusmod
	tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
	quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
	consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
	cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
	proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<script type="text/javascript">
parent2.addEventListener("click", function(event){
	alert("I am in Parent !");
});
child2.addEventListener('click', function(event){
	alert("I am in Child !");
	event.stopImmediatePropagation();
});
child2.addEventListener("click", function(event){
	alert("Thank you for Clicking Me !");
});
</script>

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Orders of the event handlers matter

The following example won't work. It will stop the bubbling but it won't stop the previously assigned event handlers from being executed. But it will stop any afterwards assigned event handlers. So oders in which the event handlers are assinged matters in case of stopImmediatePropagation() method.

<p id="parent3">
	<code id="child3">Lorem ipsum dolor sit amet, consectetur adipisicing elit</code>, sed do eiusmod
	tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
	quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
	consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
	cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
	proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<script type="text/javascript">
parent3.addEventListener("click", function(event){
	alert("I am in Parent !");
});
child3.addEventListener('click', function(event){
	alert("I am in Child !");
});
child3.addEventListener("click", function(event){
	alert("Thank you for Clicking Me !");
	event.stopImmediatePropagation();
});
child3.addEventListener("click", function(){
	alert("After stopImmediatePropagation");
})
</script>

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

In the above example, you won't see the the third alert, but you will see the first and second alert.

Don’t stop bubbling without a need!

Bubbling is convenient. Don’t stop it without a real need: obvious and architecturally well-thought. Sometimes event.stopPropagation() creates hidden pitfalls that later may become problems. For instance:

There’s usually no real need to prevent the bubbling. A task that seemingly requires that may be solved by other means. One of them is to use custom events, we’ll cover them later. Also we can write our data into the event object in one handler and read it in another one, so we can pass to handlers on parents information about the processing below.

Capturing

There’s another phase of event processing called “capturing”. It is rarely used in real code, but sometimes can be useful. Here’s the picture of a click on inside a table, taken from the specification:

That is: for a click on the event first goes through the ancestors chain down to the element (capturing), then it reaches the target, and then it goes up (bubbles), calling handlers on its way.

Before we only talked about bubbling, because the capturing phase is rarely used. Normally it is invisible to us.

Handlers added using on<event>-property or using HTML attributes or using addEventListener(event, handler) don’t know anything about capturing, they only work on the bubbling phase by default.

To catch an event on the capturing phase, we need to set the 3rd argument of addEventListener to true.

There are two possible values for that optional last argument:

Let’s see it in action:

<form class="demo1" id="demo10">FORM
  <div id="demo10Div">DIV
    <p id="demo10P">P</p>
  </div>
</form>

<script>
var handler = function(event){
	alert("Capturing = " + event.currentTarget.tagName);
}
demo10.addEventListener('click', handler, true);
demo10Div.addEventListener('click', handler, true);
demo10P.addEventListener('click', handler, true);
</script>
FORM
DIV

P

As you can see, in capturing phase, the event first occurs on the root element and goes down to it's child element, and ends on the target element.

Phase Precedence

When an event occurs, the both phase runs. First the capture phase runs and after that the bubbling phase. It means when an event occurs it first occurs on the topmost ancestor element and goes all the way down to the target element, then then it goes all the way up to the topmost ancestor element and ends there. The event journey from topmost ancestor to the target element is called capture phase and the journey from the target element to the topmost element is called bubbling phase. Here is a demo -

<form class="demo1" id="demo10">FORM
  <div id="demo10Div">DIV
    <p id="demo10P">P</p>
  </div>
</form>

<script>
var handler = function(event){
	alert("Capturing = " + event.currentTarget.tagName);
}
demo10.addEventListener('click', handler, true);
demo10Div.addEventListener('click', handler);
demo10P.addEventListener('click', handler);
</script>
FORM
DIV

P

Event Delegation

Capturing and bubbling allow us to implement one of most powerful event handling patterns called event delegation. The idea is that if we have a lot of elements handled in a similar way, then instead of assigning a handler to each of them – we put a single handler on their common ancestor. In the handler we get event.target, see where the event actually happened and handle it.

Here are some few practicle example given below -

Highlighting a Table Cell

<style type="text/css">
.highlight{
	background: crimson;
	color: white !important;
}
</style>


<table id="demoTable" class="table is-bordered is-striped is-hovered">
	<tr>
		<th>Name</th>
		<th>City</th>
		<th>Gender</th>
	</tr>
	<tr>
		<td>Santanu Bera</td>
		<td>Kolkata</td>
		<td>Male</td>
	</tr>
	<tr>
		<td>Sanchita Das</td>
		<td>Delhi</td>
		<td>Female</td>
	</tr>
	<tr>
		<td>Anindita Karmakar</td>
		<td>Mumbai</td>
		<td>Female</td>
	</tr>
</table>



<script type="text/javascript">
let selectedTd;

demoTable.onclick = function(event) {
  let target = event.target; // where was the click?

  if (!target.closest('td')) return; // not on TD? Then we're not interested

  highlight(target); // highlight it
};

function highlight(td) {
	if (td.tagName != "TD") {
		td = td.closest('td');
	}
  if (selectedTd) { // remove the existing highlight if any
    selectedTd.classList.remove('highlight');
  }
  selectedTd = td;
  selectedTd.classList.add('highlight'); // highlight the new td
}
</script>
Name City Gender
Santanu Bera Kolkata Male
Sanchita Das Delhi Female
Anindita Karmakar Mumbai Female

In the example above, we have assigned only one event handler on the table element. So any click on the td or it's nested element would be caught by that handler. In the handler we are checking if the click happened inside the td element using event.target. If it does, we are calling highlight() function to highlight the cell. If the element is not td, we are doing nothing.

In the highlight() function, we are storing the td in the variable selectedTd, so that on the next click we can remove the highlight class from it and assign the highlight class on the new clicked td element.

The above example doesn't depends on how many td the table has.

Actions in markup

Let's see another example in which we have three buttons. The same handler gets called when clicking on them. The single handler on the parent element acts like an entry point. Here is the example -

<div id="demomenu">
  <button data-action="save">Save</button>
  <button data-action="load">Load</button>
  <button data-action="search">Search</button>
</div>

<script>
  class Menu {
    constructor(elem) {
      this._elem = elem;
      elem.onclick = this.onClick.bind(this); // (*)
    }

    save() {
      alert('saving');
    }

    load() {
      alert('loading');
    }

    search() {
      alert('searching');
    }

    onClick(event) {
      let action = event.target.dataset.action;
      if (action) {
        this[action]();
      }
    };
  }

  new Menu(demomenu);
</script>

Please note that this.onClick is bound to this in (*). That’s important, because otherwise this inside it would reference the DOM element (elem), not the menu object, and this[action] would not be what we need.

In this approach, with the help of data-* attribute, we can have only one handler for many buttons. Now if you want to add an extra button, you just have to do the following -

// Add a button --
<button data-action="exit">Exit</button>


// Add a method 'exit' to the Menu class -
exit(){
	alert("Exiting");
}

This way without adding event listener to the button you can easily make a button functional.

The “behavior” pattern

We can also use event delegation to add “behaviors” to elements declaratively, with special attributes and classes.

The pattern has two parts:

For instance, here the attribute data-counter adds a behavior: “increase on click” to buttons:

Counter: <input type="button" value="1" data-counter>
One more counter: <input type="button" value="2" data-counter>

<script>
  document.addEventListener('click', function(event) {

    if (event.target.dataset.counter != undefined) { // if the attribute exists...
      event.target.value++;
    }

  });
</script>
Counter: One more counter:

There can be as many attributes with data-counter as we want. We can add new ones to HTML at any moment. Using the event delegation we “extended” HTML, added an attribute that describes a new behavior.

Toggler

One more example. A click on an element with the attribute data-toggle-id will show/hide the element with the given id. This kind of approach can be seen a lot in the Bootstrap framework.

<button data-toggle-id="subscribe-mail">
  Show the subscription form
</button>

<form id="subscribe-mail" hidden>
  Your mail: <input type="email">
</form>

<script>
  document.addEventListener('click', function(event) {
    let id = event.target.dataset.toggleId;
    if (!id) return;

    let elem = document.getElementById(id);

    elem.hidden = !elem.hidden;
  });
</script>