Understanding Events

When it comes to determining when an action occurs on our web pages, we rely on Events to do that. This is why it is important to understand how they really work, so we can get the most out of them.

Back in the day, I used to just attach event handlers to my html tags and get away with the result, and if something didn't work, there was always stackoverflow.

I can say that it works in over 90% of the cases, but when I started to get involved in more complex projects, I realized that I had to fully comprehend what is the whole deal behind events.

That being said...

There are four main concepts when it comes to Event handling. You may have already come across some of them. These are:

  • Event Delegation

  • Event Propagation

  • Event Bubbling

  • Event Capturing

Their names could make them sound confusing, but we'll break down each one of them.

Event Delegation#

Event Delegation is a pattern/technique that allows us to handle events in a more efficient way. With this pattern, instead of adding an event listener to multiple child elements individually, we can just add one to a common ancestor and handle the event from there.

Let's look at this example:

<div id="container">
     <button id="btn-1" value="1">Item #1</button>
     <button id="btn-2" value="2">Item #2</button>
     <button id="btn-3" value="3">Item #3</button>
</div>

Without Event Delegation, we would have to add an event handler to each button inside container. Something similar to this:

const btn1 = document.getElementById('btn-1');
btn1.addEventListener('onclick', (e) => {
    console.log(e.target.value); // Output: 1
});

const btn2 = document.getElementById('btn-2');
btn2.addEventListener('onclick', (e) => {
    console.log(e.target.value); // Output: 2
});

const btn3 = document.getElementById('btn-3');
btn3.addEventListener('onclick', (e) => {
   console.log(e.target.value); // Output: 3
});

This doesn't seem very efficient, right? We're adding multiple event listeners, which at the end, are doing pretty much the same thing.

Now with Event Delegation, we can put an event listener on a common ancestor, so that in case any of its childs (our buttons) is clicked, we're able to handle the event from there.

<div id=”container”>
     <button id="btn-1" value="1">Item #1</button>
     <button id="btn-2" value="2">Item #2</button>
     <button id="btn-3" value="3">Item #3</button>
</div>
const container = document.getElementById('container');
container.addEventListener('onclick', (e) => {
    console.log(e.target.value);
});

In this case, clicking any of the buttons would invoke the event handler attached to container as if the handler were attached to the element that was clicked.

This is a very common pattern and most people have probably done this before.

But how does it actually work? How does Javascript know that a child was clicked, and therefore its parent should execute the event handler attached to it?

Well, this is where Event Propagation comes in:

Event Propagation#

Event Propagation describes how events are executed when they occur.

When an event fires on an element, it also fires on its ancestors, just as we saw in our previous example. This is what allows us to delegate events to other ancestors.

Until now we've seen events propagating up, but this isn't the only way they can propagate.

There are two phases in which events propagate, these are the Bubbling and Capturing phases.

Let's take a look at this picture:

Event Phases

Capturing Phase (Event Capturing)#

This is the first phase.

When an event occurs, it is not triggered right away on the target element. It starts at the very top element, the window.

window is not part of the DOM, but for illustrative purposes, it's in the graphic.

After being triggered at the top, it then starts to descend through the DOM executing the event handlers attached to each node, until it reaches the Event Target's parent. This process is what is called the Capturing Phase.

After this phase, the Event Target is reached, and then the Bubbling Phase starts.

Bubbling Phase (Event Bubbling)#

This is the phase most people are used to. It works the same as the Capturing Phase, but it starts from the Event Target's parent, and then it propagates to the top.


But wait! Does that mean that event handlers attached to ancestors are executed twice? Once in the Capturing Phase and once again during the Bubbling Phase?

Well, no.

You see, when event handlers are added, you decide in which phase you would like to execute them.

We've seen that addEventListener takes two arguments: An event name and a handler in the form of a callback.

But, there's actually a third parameter which is called useCapture. If set to true, the event handler will run during the Capturing Phase, otherwise, it will run in the Bubbling Phase.

By default, useCapture is set to false. This is why most event handlers are invoked during the Bubbling Phase.

Example of an Event Listener set to be called during the Capturing Phase:

const container = document.getElementById('container');

container.addEventListener('click', (e) => {
   console.log(e.target.value);
}, true);

As to why there are two phases, it has to do with historical reasons. Netscape proposed Event Capturing and Internet Explorer Event Bubbling. Both of them were later included to the standards.

Note: When Events are added directly to the Target Element instead of an ancestor, they are invoked when they reach the Event Target, not in any of the other two phases. Event Target can be seen as its own phase.

Why is it important to know this?

Most of the time, invoking all your event handlers during the Bubbling Phase will work just fine. But there are cases where they won't.

Certain events don't bubble up, which means they cannot be invoked during the Bubbling Phase. One of these is the focus event.

If you want to detect that an element has been focused from one of its ancestors, then you would have to run the event handler in the Capturing Phase (setting useCapture to true).

There are also other cases where you would consider invoking handlers during the Capturing Phase:

  1. Add event listeners with a higher priority. Events are usually executed during the Bubbling Phase. As the Capturing Phase happens first, any event listener added to this phase will be executed first.

  2. Executing the event handlers attached to ancestors, but not the ones of the target element.

The last one can be achieved using e.stopPropagation() which will stop the event from propagating through the DOM. If it is invoked during the Capturing Phase, event handlers added to the Event Target and to the Bubbling Phase won't be executed.