Avoid Too Many Events

How many event listeners are created with this code?

const myCustomDiv = document.createElement('div');

for (let i = 1; i <= 200; i++) {
    const newElement = document.createElement('p');
    newElement.textContent = 'This is paragraph number ' + i;

    newElement.addEventListener('click', function respondToTheClick(evt) {
        console.log('A paragraph was clicked.');
    });

    myCustomDiv.appendChild(newElement);
}

document.body.appendChild(myCustomDiv);

Enter the number of event listeners below. (enter numbers, only)


200

RESET

Refactoring The Number of Event Listeners

Let's look at the code another time:

const myCustomDiv = document.createElement('div');

for (let i = 1; i <= 200; i++) {
    const newElement = document.createElement('p');
    newElement.textContent = 'This is paragraph number ' + i;

    newElement.addEventListener('click', function respondToTheClick() {
        console.log('A paragraph was clicked.');
    });

    myCustomDiv.appendChild(newElement);
}

document.body.appendChild(myCustomDiv);

We're creating a<div>element, attaching two hundred paragraph elements and attaching an event listener with arespondToTheClickfunction to each paragraph as we create it. There are a number of ways we could refactor this code. For example, as of right now, we're creating two hundred differentrespondToTheClickfunctions (that all actually do the exact same thing!). We could extract this function and just reference the function instead of creating two hundred different functios

const myCustomDiv = document.createElement('div');

function respondToTheClick() {
    console.log('A paragraph was clicked.');
}

for (let i = 1; i <= 200; i++) {
    const newElement = document.createElement('p');
    newElement.textContent = 'This is paragraph number ' + i;

    newElement.addEventListener('click', respondToTheClick);

    myCustomDiv.appendChild(newElement);
}

document.body.appendChild(myCustomDiv);

This is a great step in the right direction!

However, we still have two hundred event listeners. They're all pointing to the same listener function, but there are still two hundred_different_event listeners.

What if we moved all of the listeners to the<div>instead?

The code for this would look like:

const myCustomDiv = document.createElement('div');

function respondToTheClick() {
    console.log('A paragraph was clicked.');
}

for (let i = 1; i <= 200; i++) {
    const newElement = document.createElement('p');
    newElement.textContent = 'This is paragraph number ' + i;

    myCustomDiv.appendChild(newElement);
}

myCustomDiv.addEventListener('click', respondToTheClick);

document.body.appendChild(myCustomDiv);

Now there is only:

  • a single event listener
  • a single listener function

Now the browser doesn't have to store in memory two hundred different event listeners and two hundred different listener functions. That's a great for performance`!

However, if you test the code above, you'll notice that we've lost access to the individual paragraphs. There's no way for us to target a specific paragraph element.So how_do_we combine this efficient code with the access to the individual paragraph items that we did before?

We use a process called event delegation.

Event Delegation

Remember the event object that we looked at in the previous section? That's our ticket to getting back the original functionality!

The event object has a.targetproperty. This property references the_target_of the event. Remember the capturing, at target, and bubbling phases?...these are coming back into play here, too!

Let's say that you click on a paragraph element. Here's roughly the process that happens:

  1. a paragraph element is clicked
  2. the event goes through the capturing phase
  3. it reaches the target
  4. it switches to the bubbling phase and starts going up the DOM tree
  5. when it hits the <div> element, it runs the listener function
  6. inside the listener function, event.target is the element that was clicked

Soevent.targetgives us direct access to the paragraph element that was clicked. Because we have access to the element directly, we can access its.textContent, modify its styles, update the classes it has - we can do anything we want to it!

const myCustomDiv = document.createElement('div');

function respondToTheClick(evt) {
    console.log('A paragraph was clicked: ' + evt.target.textContent);
}

for (let i = 1; i <= 200; i++) {
    const newElement = document.createElement('p');
    newElement.textContent = 'This is paragraph number ' + i;

    myCustomDiv.appendChild(newElement);
}

document.body.appendChild(myCustomDiv);

myCustomDiv.addEventListener('click', respondToTheClick);

Checking the Node Type in Event Delegation

In the code snippet we used above, we added the event listener directly to the<div>element. The_listener_function logs a message saying that a paragraph element was clicked (and then the text of the target element). This works perfectly! However, there is nothing to_ensure_that it was actually a<p>tag that was clicked before running that message. In this snippet, the<p>tags were direct children of the<div>element, but what happens if we had the following HTML:

<article id="content">
  <p>Brownie lollipop <span>carrot cake</span> gummies lemon drops sweet roll dessert tiramisu. Pudding muffin <span>cotton candy</span> croissant fruitcake tootsie roll. Jelly jujubes brownie. Marshmallow jujubes topping sugar plum jelly jujubes chocolate.</p>

  <p>Tart bonbon soufflé gummi bears. Donut marshmallow <span>gingerbread cupcake</span> macaroon jujubes muffin. Soufflé candy caramels tootsie roll powder sweet roll brownie <span>apple pie</span> gummies. Fruitcake danish chocolate tootsie roll macaroon.</p>
</article>

In this filler text, notice that there are some<span>tags. If we want to listen to the<article>for a click on a<span>, you _might _think that this could would work:

document.querySelector('#content').addEventListener('click', function (evt) {
    console.log('A span was clicked with text ' + evt.target.textContent);
});

This will work, but there's a major flaw. The listener function will still fire when either one of the paragraph elements is clicked, too! In other words, this listener function is not verifying that the target of the event is actually a<span>element. Let's add in this check:

document.querySelector('#content').addEventListener('click', function (evt) {
    if (evt.target.nodeName === 'SPAN') {  // ← verifies target is desired element
        console.log('A span was clicked with text ' + evt.target.textContent);
    }
});

Remember that every element inherits properties fromthe Node Interface. One of the properties of the Node Interface that is inherited is.nodeName. We can use this property to verify that the target element is actually the element we're looking for. When a<span>element is clicked, it will have a.nodeNameproperty of"SPAN", so the check will pass and the message will be logged. However, if a<p>element is clicked, it will have a.nodeNameproperty of"P", so the check will fail and the message will_not_be logged.

⚠️ ThenodeName's Capitalization ⚠️

The.nodeNameproperty will return a_capital_string, not a_lowercase_one. So when you perform your check make sure to either:

  • check for capital letters
  • convert the .nodeName to lowercase
// check using capital letters
if (evt.target.nodeName === 'SPAN') {
    console.log('A span was clicked with text ' + evt.target.textContent);
}

// convert nodeName to lowercase
if (evt.target.nodeName.toLowerCase() === 'span') {
    console.log('A span was clicked with text ' + evt.target.textContent);
}

Recap

In this section, we looked at Event Delegation. Event Delegation is the process of delegating to a parent element the ability to manage events for child elements. We were able to do this by making use of:

  • the event object and its .target property
  • the different phases of an event

Further Research

results matching ""

    No results matching ""