Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs

Dashboard
Notifications
Mark all as read
Q&A

What is the main difference between event delegation to other event handling patterns in JavaScript?

+2
−5

I define a software event as any state of a program to which we can probably respond.

In JavaScript, how does event delegation differs from more simple and probably more common event handling patterns such as:

  • window.addEventListener('DOMContentLoaded', () => {} );
  • window.addEventListener('load', () => {} );

The terminology of event delegation is significantly more complex than of these and I hope to get an answer from which it will be clear to me what is the main reason that makes it more complex so to need terms such as delegation/propagation/bubbling, etc.

Why does this post require moderator attention?
You might want to add some details to your flag.
Why should this post be closed?

1 comment thread

I guess here we have a similar issue that happened in your [other question](/posts/284415): you downv... (1 comment)

1 answer

+6
−1

tl;dr

The purpose of addEventListener is to define what happens when an event is triggered at some element. But it also allows us to implement event delegation, due to the bubbling/propagation behaviour.


Explaining the terminology

Bubbling

Suppose I have this HTML:

<p id="text">A paragraph <span>with a span</span></p>

Let's add an event listener to the paragraph:

document.querySelector('#text').addEventListener('click', function(e) {
    // just prints the text contents of the clicked element
    console.log('clicked: ' + e.target.innerText);
});

If I click on the paragraph, it should print its text contents ("A paragraph with a span"), but the output actually depends on where I click.

If I click on the span (any part of the "with a span" text), the output will be clicked: with a span (it prints only the span's content). If I click on any part of the "A paragraph" text, it prints clicked: A paragraph with a span (the whole paragraph's content).

But why this happens if the event listener is assigned to the paragraph, and not to the span? That happens because when an event is triggered by an element, it first checks if that element has a corresponding event listener. Then it checks if the parent element has such listener. Then it checks the parent's parent, and so on, and if any listeners are found, they're executed.

One important detail is that the target element (the one we've got with e.target) is the one that triggered the event, even if the listener is in another element. That's why, when I click on the span, it prints only the span's contents (not the whole paragraph's), even if the listener is assigned to the paragraph.

This mechanism of checking an event listener in the element, and then checking the parent (and the parent's parent, etc) is called "bubbling".

Another way of explaining:

<p id="text"> ← This paragraph has a listener for the click event
A paragraph   ← If I click here, it triggers the event (and e.target is the paragraph)
 <span>       ← If I click inside the span, the event bubbles to the parent
 with a span  ← The parent executes the listener, but e.target will be the span, not the paragraph
 </span>
</p>

More interesting, if the span also has an event listener, both (the span's and the paragraph's listeners) will be triggered:

// add a listener for the paragraph
document.querySelector('#text').addEventListener('click', function(e) {
    console.log('clicked: ' + e.target.innerText);
});

// add a listener only for the span
document.querySelector('#text span').addEventListener('click', function(e) {
    console.log('span-only listener: ' + e.target.innerText);
});

Now if I click on the span, the output will be:

span-only listener: with a span
clicked: with a span

That happens because it first checks if span has a listener. It does, hence it's executed. Then, the event bubbles to the span's parent (the paragraph), which also has a listener, thus it's executed as well. And note again that the event's target will always be the clicked element (in this case, the span), regardless of the element that has the listener.

Side note: inside the span's listener you could call e.stopPropagation(), which interrupts the bubbling: it prevents the event from propagating, and only the span's listener would be triggered.


Bubbling is one type of Propagation

"Propagation" is a generic term that refers to how an event "travels" (or propagates) through the DOM. Bubbling is one type of propagation - we could say it's a "bottom-up" one, because it starts on the target element and it goes "up" in the hierarchy (to the element's parent, then to the parent's parent and so on).

But there's also a "top-down" propagation, called "Capturing" (it wasn't mentioned in the question, but it's directly related to the other terms and I think it's worth talking about it). It's the opposite of bubbling, because it starts in the document's root and then checks its children (and the children's children, and so on), until it reaches the target element.

The default behaviour of addEventListener is to use the bubbling propagation, but you can change that by passing true in the third parameter (useCapture):

// add a listener for the paragraph
document.querySelector('#text').addEventListener('click', function(e) {
    console.log('clicked: ' + e.target.innerText);
}, true); // third parameter "true": uses capturing instead of bubbling

// add a listener only for the span
document.querySelector('#text span').addEventListener('click', function(e) {
    console.log('span-only listener: ' + e.target.innerText);
}, true); // third parameter "true": uses capturing instead of bubbling

Now, if I click on the span, it first executes the parent's listener, and the output will be:

clicked: with a span
span-only listener: with a span

Check the documentation for a more detailed explanation about the event propagation mechanism.


Event Delegation

Let's suppose we have a lot of elements that must be handled in a similar way (ex: the same listener should be used for all of them). Instead of calling addEventListener in each one of them, we do that in their common ancestor. That's the whole idea of Event Delegation.

Example, given the HTML below:

<div id="main">
  <p>first paragraph</p>
  <p>second paragraph</p>
  <h3>Not a paragraph</h3>
</div>

Suppose all the paragraphs must have the same event listener. Of course I could call addEventListener for all of them, but I could also do this:

// if I click anywhere in the main div, it calls the callback function
document.querySelector('div#main').addEventListener('click', function(e) {
    if (e.target) {
        let element = e.target;
        // handle only paragraphs
        if (element.nodeName === 'P')
            console.log('clicked: ' + element.innerText);
    }
});

This way, if I click in any element inside the div (either one of the paragraphs, or the h3 element), it'll first check if that element has an event listener. It doesn't, but then the event bubbles to the div, which has a listener. Thus, it executes the function, which checks if the target element is a paragraph and do whatever it has to with it. Any other elements are ignored.

This is also useful in situations where the paragraphs are "dynamic" (there are some scripts manipulating the main div, adding and removing paragraphs frequently). If I want to keep the event listeners for all those paragraphs, it'll be very tedious to remember to add a listener every time a new paragraph is added. By using event delegation (attaching a listener on the parent), all the new paragraphs will automatically have a listener.

In another words, if I programatically add a paragraph to the div:

const p = document.createElement('p');
p.innerText = 'some random text';
document.querySelector('div#main').appendChild(p);

I don't need to add an event listener to it. Thanks to bubbling, if I click on this new paragraph, the event will bubble to its parent (the div), and it'll call the callback function, which will print the paragraph's contents.

This is useful if the child elements share some common stuff to be done when an event happens (or when you're dinamically adding new child elements, and their listeners should be the same). But it's not a silver bullet: if each paragraph requires a completely different action, it'd be better to have a specific listener for each one.


Actually, it's more complicated

The explanation above is a simplified version. What actually happens when an event is triggered is this:

Event propagation

The event propagation runs three phases:

  1. Capturing phase: the event starts at the document root, and propagates down the DOM hierarchy, until the target element is reached.
  2. Target phase: the event reaches its target (the element that actually triggered the event, the one we get with e.target).
  3. Bubbling phase: the event is propagated up the DOM hiearchy, starting from the target, until it reaches the document root.

During those phases, any element that has a listener for the triggered event will execute it. What changes is when it's executed:

  • If the element is the event's target, it's executed in the target phase.
  • For all the other elements, the third parameter of addEventListener that we saw above controls when it'll be executed. The default is to execute it in the bubbling phase, but setting it to true makes it execute in the capturing phase.

But that's just me being pedantic. Saying that "bubbling is when the event is propagated up" and "capturing is when the event is propagated down", isn't technically accurate (because it might give the impression that the three phases are not executed), but I admit it might be enough for most people to understand the basic mechanism of event propagation.

As a side note, it's worth to remind that not all events bubble.


Further reading

Why does this post require moderator attention?
You might want to add some details to your flag.

2 comment threads

Replying to the answer and to comments made outside its scope (4 comments)
Hello, I down voted because I think the longevity is extreme and unjustified and because I think you ... (5 comments)

Sign up to answer this question »