Event Bubbling v/s Event Capturing

Event Bubbling v/s Event Capturing

When designing a web application with multiple nested DOM elements, handling events across the DOM tree can get a bit tedious if you are not familiar with event propagations.

In this article I'll be explaining how an event propagates and how it can handled pertaining to our needs. This article will be covering the following topics:

  • Browser Events
  • How events propagate
    • Event Bubbling
    • Event Capturing
  • Handling event propagation

Browser Events

An event is a signal that something has happened. All DOM nodes generate such signals.

Let's say there is a button on your web page which alerts a welcome message whenever the user clicks the button. Now the event here is click because when the button is clicked a signal is generated indicating the click of of the button which in turn shows the alert message. The below piece of code demonstrates this behavior and gives a glimpse of browser events.

// HTML
<button id="btn">click me</button>
// JS
const btn = document.querySelector('#btn')

btn.addEventListener('click',()=>{
    alert("welcome")
})

Some of the most used browser events are:

  • click: when the mouse clicks on an element (touchscreen devices generate it on a tap)
  • contextmenu: when the mouse right-clicks on an element.
  • mouseover / mouseout: when the mouse cursor comes over / leaves an element.
  • mousedown / mouseup: when the mouse button is pressed / released over an element.
  • mousemove: when the mouse is moved.
  • submit: when the visitor submits a <form>.
  • focus: when the visitor focuses on an element, e.g. on an <input>.

Event Propagation

Now that we have understood what an event is the next question to address is how an event propagates. But first what exactly is event propagation?

Event propagation is nothing but the mechanism of how an event propagates or travel through the DOM tree to arrive at its target and what happens afterwards. In general, an event propagates from the window down to the target element. Let's say we have a hyper link inside a <p> tag in our DOM tree. Now on clicking the hyperlink the event initiates at the window level and then propagates to the link travelling across html tag, body tag, <p> tag and then reaching the link. window > html > body > p > a

    <html>
        <body>
            <p>
                <a href="#" > link </a>
            </p>
        </body>
    </html>

Moving ahead, event propagation has two phases namely: Capturing and Bubbling.

phase.png

Event Capturing

The first phase of event propagation is Event Capturing. In the capturing phase, events propagate from the window down through the DOM tree to the target element.
For example, if the user clicks a hyperlink, that click event would pass through the <html> element, the <body> element, and the <p> element containing the link.

Also if any ancestor (i.e. parent, grandparent, etc.) of the target element and the target itself has a specially registered capturing event listener for that type of event, those listeners are executed during this phase.

Before moving ahead with capturing and bubbling let's take a detour to understand how to handle any event on our DOM tree. So to handle any mouse or keyboard event or any such event we use an event-handler. We add event listeners to all DOM elements where an event might occur.

const element = document.querySelector('DOM_element')
element.addEventListener('event_type', callback_function)

This is a general syntax of an event handler. We select our DOM element using querySelector or getElement methods, totally your choice. Now on the selected element we add an eventListener to listen if an event occurs. This eventListener takes two mandatory arguments: event-type and a callback function. This callback function would be executed when the event occurs.

Now this eventListener also takes an optional third argument i.e useCapture: a boolean value. This boolean flag specifies the event propagation flow. When the useCapture flag is set to true it means execute the callback function in capturing phase when the specified event occurs. In case of false the callback function is executed in bubbling phase when the specified event occurs. The default value of useCapture flag is false. However, in case of any event first the capturing phase occurs, then on reaching the target element the event switches to bubbling phase.

So the eventListener would look something like

const element = document.querySelector('DOM_element')
element.addEventListener('event_type', callback_function, useCapture)

Event Bubbling

In the bubbling phase, the exact opposite of capturing occurs. In this phase event propagates or bubbles back up the DOM tree, from the target element up to the window, visiting all of the ancestors of the target element one by one. For example, if the user clicks a hyperlink, that click event would pass through the <p> element containing the link, the <body> element, the <html> element, and the document node.

Also, if any ancestor of the target element and the target itself has event handlers assigned for that type of event, those handlers are executed during this phase. In modern browsers, all event handlers are registered in the bubbling phase, by default.

Let's look at a few examples to better understand their difference

// html
    <div id="grandparent">
        <div id="parent">
            <div id="child"></div>
        </div>
    </div>
//css
div{
    padding:50px;
}

#grandparent{
    background-color: #0284C7;
}

#parent{
    background-color: #16A34A;
}

#child{
    background-color: #EF4444;
}
//JS
document.querySelector('#grandparent')
.addEventListener('click', ()=>{
    console.log("Grandparent Clicked")
}, true)

document.querySelector('#parent')
.addEventListener('click', ()=>{
    console.log("Parent Clicked")
}, true)

document.querySelector('#child')
.addEventListener('click', ()=>{
    console.log("Child Clicked")
}, true)

Here I have three nested divs with id as grandparent, parent and child. Now I've attached eventListeners to each of these with useCapture flag as true. This means event propagates in capturing phase.

If I click the grandparent div it prints:

Grandparent Clicked

On clicking the parent div the output is

Grandparent Clicked
Parent Clicked

And on clicking the child div the output is

Grandparent Clicked
Parent Clicked
Child Clicked

Now in this case whenever a div is clicked the event starts from the document level and travels down the DOM tree checking for each parent of the target if it has an eventListener for the same event-type and if so then executes its callback function. So when the grandparent div was clicked the event starts from window and on reaching the target div (no parents with same event-type in this case) it prints the required output.

In second case the parent div is clicked and before reaching the target element the event encounters a parent with an eventListener for same event-type i.e the grandparent div so it first executes its callback function and then reaches our target. Similar was the case when our child div was clicked.

Let's look at an example for Bubbling phase:

document.querySelector('#grandparent')
.addEventListener('click', ()=>{
    console.log("Grandparent Clicked")
}, false)

document.querySelector('#parent')
.addEventListener('click', ()=>{
    console.log("Parent Clicked")
}, false)

document.querySelector('#child')
.addEventListener('click', ()=>{
    console.log("Child Clicked")
}, false)

Let's take the same example but this time the useCapture flag is set to false which results in Event Bubbling.

On clicking the grandparent div the output was

Grandparent Clicked

On clicking the parent div the output was

Parent Clicked
Grandparent Clicked

On clicking the child div the output was

Child Clicked
Parent Clicked
Grandparent Clicked

In bubbling phase the event starts propagating from the target element itself. So, in the first case on clicking the grandparent div the event starts from that div, then travels to its parent i.e the body element then to html element and then to document. But they do not have any eventListeners so it prints only Grandparent Clicked.

In the second case, when parent div is clicked the event starts from parent div printing Parent Clicked, then travels to grandparent div which has an eventListener for a click event so it ends up executing its callback function, hence prints Grandparent Clicked.

Similarly when the child div is clicked there exists two ancestors with eventListeners for click event. Thus their callbacks get executed. Hence the output received.

So to sum up, in capturing phase the event flow is from outside-to-inside while in bubbling phase the flow is from inside-to-outside.

Let's see another piece of code to understand the event flow:

document.querySelector('#grandparent')
.addEventListener('click', ()=>{
    console.log("Grandparent Bubbling ")
}, false)

document.querySelector('#parent')
.addEventListener('click', ()=>{
    console.log("Parent Bubbling")
}, false)

document.querySelector('#child')
.addEventListener('click', ()=>{
    console.log("Child Bubbling")
}, false)

document.querySelector('#grandparent')
.addEventListener('click', ()=>{
    console.log("Grandparent Capturing")
}, true)

document.querySelector('#parent')
.addEventListener('click', ()=>{
    console.log("Parent Capturing")
}, true)

document.querySelector('#child')
.addEventListener('click', ()=>{
    console.log("Child Capturing")
}, true)

In this example when the child div is clicked the following output is received:

Grandparent Capturing
Parent Capturing
Child Capturing
Child Bubbling
Parent Bubbling
Grandparent Bubbling

Here I've tried to show the event propagation across our DOM tree. So when the child div is clicked, the event starts propagating from the document level. On reaching the first ancestor it checks for an eventListener for click event which can be executed in capturing phase and it finds one so prints Grandparent Capturing. Then travelling forward the event encounters another ancestor with eventListener for click event in capturing phase so on executing its callback prints Parent Capturing. Then the event reaches the target element and executes the callback for target element in capturing phase printing Child Capturing.

Once the event has reached the target element, it then switches to bubbling phase. Now it'll propagate out the DOM tree. So it first prints Child Bubbling since an eventListener exist for click event in bubbling phase. Then the event travels up the DOM tree and finds eventListeners on ancestors for click event in bubbling phase and executes their callback functions. Thus, prints Parent Bubbling Grandparent Bubbling.

Lets look at another example where we use a combination of bubbling and capturing:

document.querySelector('#grandparent')
.addEventListener('click', ()=>{
    console.log("Grandparent Clicked")
}, true)

document.querySelector('#parent')
.addEventListener('click', ()=>{
    console.log("Parent Clicked")
}, false)

document.querySelector('#child')
.addEventListener('click', ()=>{
    console.log("Child Clicked")
}, true)

When the child div is clicked the following output is produced:

Grandparent Clicked
Child Clicked
Parent Clicked

Now we know much about event propagation so lets see the event flow. When the child div is clicked the event starts in the window and propagates down the DOM tree. On encountering the first ancestor it checks for an eventListener for click event in capturing phase. it founds one and executes the callback function printing Grandparent Clicked. Now the event travels down the tree and reaches another ancestor. It again check for an eventListener. It founds one but here the useCapture flag is set to false. Thus it can't be executed in capturing phase. Then the event moves down the tree and reaches our target with an eventListener in capturing phase so it executes the callback and prints Child Clicked.

At this stage the event now switches to bubbling phase and starts propagating up the DOM tree. There is no eventListener for bubbling phase on child div so it travels to its parent and reaches the parent div. There it founds an eventListener for click event with useCapture flag as false which means it can be executed. Thus it now prints Parent Clicked. Then it travels up to the next parent and founds no eventListener there for bubbling phase and then reaches the document. Thus finishing the program execution.

Simple !!

Now you've seen how an event propagates across the DOM tree in capturing and bubbling phases. As you've seen in case of nested DOM elements this event propagation would end up executing unnecessary operations. Thus for a better performance event propagation must be handled.

Handling Event Propagation

We've seen that when an event is fired, let's say by clicking a button then it'll be propagated to all its ancestors in the DOM tree. This would definitely affect the performance of our application. To stop this unnecessary propagation we can use event.stopPropagation() method.

document.querySelector('#btn')
.addEventListener('click', (e)=>{
    e.stopPropagation()
    console.log("Button Clicked")
}, false)

When we handle any event, in our case say a click then that event can be accessed inside the callback function using a variable as an argument to the callback, here e. Then we can call the stopPropagation method on this event to stop any further propagation across the DOM tree.

Let's look at an example for this

document.querySelector('#grandparent')
.addEventListener('click', ()=>{
    console.log("Grandparent Clicked")
}, false)

document.querySelector('#parent')
.addEventListener('click', ()=>{
    console.log("Parent Clicked")
}, false)

document.querySelector('#child')
.addEventListener('click', (e)=>{
    e.stopPropagation()
    console.log("Child Clicked")
}, false)

Using the same example as above, we have eventListeners for our three divs and useCapture flag is set to false. Now when the child div is clicked the output should be

Child Clicked
Parent Clicked
Grandparent Clicked

But instead the output is

Child Clicked

The reason for this being when the child div is clicked the event when in bubbling phase executes the callback for child div's eventListener. But in this callback function we are also accessing the event occurred and calling the stopPropagation method on it which prevents its further propagation up the DOM tree.

Thus this time we stopped the event from travelling all the way up the tree and executing each parent's callbacks. This is definitely an improvement on the earlier behavior.

Conclusion

Whenever you are using nested DOM elements and there are events on any of those elements then you need to take into account the event propagation across the DOM tree. Event propagates in two phases: Capturing (travelling down the DOM tree) and Bubbling (travelling up the DOM tree). Whenever an event occurs it propagates first in the capturing phase from window to target DOM element and then on reaching the target node it switches to Bubbling phase travelling from the target DOM element to window. This event propagation can be handled by calling stopPropagation method on the event.

Hope this article helped you understand Event propagation.

Read more about Javascript Browser Events, stopPropagation