What is Event bubbling and its benefits

Event bubbling is a DOM mechanism where an event, like a click, starts on the target element that initiated it.

What is Event bubbling and its benefits
Event Bubbling in JavaScript

Event bubbling is a propagation mechanism in the Document Object Model (DOM) and where an event, like a click, a paste, or a keypress event, is initially triggered on the target element that initiated it. From there, the event then propagates upward (or “bubbles”) through the DOM tree to the root of the document.

Initiating Bubbling Phase

In the bubbling phase, the event starts at the target element and moves upward through its ancestor elements in the DOM tree, like moving from a leaf to the root in the binary tree. The meaning of this is, that we can potentially either attach an event handler to the target element or its ancestors to receive and respond to the event.

Let’s look at an example implementation of event bubbling:

HTML:

<div id="parent" >
    <button id="child" >Start bubbling!</button>
</div>

JavaScript:

const parent = document.querySelector('#parent')
const child = document.querySelector('#child')

parent.addEventListener('click', () => {
    console.log('Parent element clicked!')
})

child.addEventListener('click', () => {
    console.log('Child element clicked!')
})

When we click the “Start bubbling!” button, both the child and its parent event handler will be triggered because of the event bubbling.

Sometimes we may want to stop this behaviour when we don’t want the bubbling to go further. Let’s see how we can achieve this.

Stopping the bubbling phase

When we want to stop the bubbling behaviour, we can use the stopPropagation() method to prevent the event bubble up further in the DOM tree. This ensures that only event handlers up to stopPropagation() invoked can receive the event. Let’s modify the previous example to see how we can achieve this.

HTML:

    <div id="parent" >
        <div id="stopbubbling">
            <button id="child" >Start bubbling!</button>
        </div>
    </div>

JavaScript:

const parent = document.querySelector('#parent')
const child = document.querySelector('#child')
const stopBubblingDiv = document.querySelector("#stopbubbling")

parent.addEventListener('click', () => {
    console.log('Parent element clicked!')
})

stopBubblingDiv.addEventListener('click', (event) => {
    console.log('Event bubbling will stop here!')
    event.stopPropagation()
})

child.addEventListener('click', () => {
    console.log('Child element clicked!')
})

Where it can be used?

Event bubbling enables us to implement one of the most powerful event-handling patterns called event delegation. Event Delegation is simply attaching a single event handler to a common ancestor of a lot of elements instead of attaching an event handler to every one of them, you can imagine how it can decrease the performance of the application if we do some complex stuff in the event handlers.

Let’s look at a simple example of how it can be implemented:

HTML:

    <div id="parent" >
            <button id="child" >Start bubbling!</button>
    </div>

JavaScript:

const parent = document.querySelector('#parent')
parent.addEventListener('click', (event) => {
    if(event.target && event.target.id === 'child'){
        console.log('Child element clicked!')
    }

    if(event.target && event.target.id === 'parent'){
        console.log('Parent element clicked!')
    }
})

In real-world applications, event bubbling can be useful with event delegation in accordion, dropdown menus or checkboxes. Let’s look at a dropdown menu example that uses event bubbling.

HTML:

    <label for="countries">Choose a country:</label>
    <select id="countries" name="countries">
        <option value="USA">United States</option>
        <option value="Japan">Japan</option>
        <option value="Turkey">Turkey</option>
        <option value="Germany">Germany</option>
        <option value="Spain">Spain</option>
    </select>

JavaScript:

const countries = document.querySelector('#countries')
countries.addEventListener('click', (event) => {
    if(event.target.tagName === "OPTION"){
        console.log(`Clicked on ${event.target.textContent}`)
    }
})

What are the benefits?

  • Handling events efficiently: Instead of attaching lots of event listeners, we can attach one event listener and this minimizes performance overhead
  • Improves Readability and Maintainability: Reducing the number of event handlers improves code readability and maintainability, especially if we do similar stuff in more than one event listener.
  • More Flexible: Events on the child elements can be managed without attaching event listeners directly to them.

There are some drawbacks:

Be mindful that event delegation has some specific drawbacks:

  • May cause unintended actions: If identification of the event target hasn’t been done properly, it may cause some unintended actions.

  • Not works on all events: Not all events can be delegated because they are not bubbled. Some non-bubbling events are: focus, scroll, resize and mouse events in general (such as mouseentermouseleave)

  • Event overhead: Although event delegation is typically more efficient, it often requires complex logic to be implemented within the root event listener to recognize the element that triggered the event and respond accordingly. This can lead to additional overhead and may become more complicated if not handled appropriately.

Conclusion

Event bubbling with event delegation can be very useful to write cleaner and maintainable code and to minimize performance overhead, but it should be used mindfully otherwise it may cause unintended actions or event overhead. Also, it doesn’t work on all events.