How to add event listener on dynamically created div with interactive content

Viewed 70

When searching how to attach an event listener to a dynamically loaded div the accepted answer seems to be event delegation. I did not find another suggested way when HTML is added with insertAdjacentHTML.

I am adding divs to the DOM like follows:

document.getElementById('container').insertAdjacentHTML(
    'beforeend', `<div class="card">
<div class="card-header">Card header content</div>
  <div class="card-body">
    <h2>Card body</h2>
    <select>
      <option value="value1">Value 1</option>
    </select><br>
    Some more text
  </div>
</div>`
);

// Event delegation
document.addEventListener('click', function (e) {
    console.log(e.target.className);
});
.card{
  background: rosybrown; 
  text-align: center; 
  width: 50%;
}
h2{
  margin-top: 0;
}
<div id="container">
</div>

I hope you can see the console output when clicking anywhere on the card.

Event delegation how I know it works by attaching the click event on a "big" parent element like document and then comparing attributes like the className of the clicked element and making something if it matches the intended class name.

The issue is that this event.target targets the precise clicked elements inside the card, but I can't do much with that info, can I? I need the whole card (a parent of those elements) to be able to make the right action for the user (in this case, redirect to a more detailed view of what the card previewed).

A dirty possible solution, I thought, would be wrapping the div with an <a>-tag, but then the select and other buttons / interactive elements of the card wouldn't work, and the user would just be directed to the link.

Edit - Solution

Thank you so much @epascarello and @KooiInc and the others for your answers!
Not sure if this should be an own answer or if it's better in an edit of the question but as I need to target the card only when the user doesn't click in the interactive element, in my case the select. I made a little addition and this works beautifully now:

document.getElementById('container').insertAdjacentHTML(
    'beforeend', `<div class="card" data-id="1">
<div class="card-header">Card header content</div>
  <div class="card-body">
    <h2>Card body</h2>
    <select>
      <option value="value1">Value 1</option>
    </select><br>
    Some more text
  </div>
</div>`
);

// Event delegation
document.addEventListener('click', function (e) {
    console.clear();
    const card = e.target.closest('.card');
    if (card && e.target.tagName !== 'SELECT'){
      console.log('Redirect to ' + card.dataset.id);
    }
});
.card{
  background: rosybrown; 
  text-align: center; 
  width: 50%;
}
h2{
  margin-top: 0;
}
<div id="container">
</div>

If someone has a more efficient suggestion than using e.target.tagName to manually check each HTML element that we don't want the event listener to be triggered on, I would love to see it.

3 Answers

You can use Element.closest to retrieve the element you need.

document.getElementById('container').insertAdjacentHTML(
    'beforeend', `<div class="card">
<div class="card-header">Card header content</div>
  <div class="card-body">
    <h2>Card body</h2>
    <select>
      <option value="value1">Value 1</option>
    </select><br>
    Some more text
  </div>
</div>`
);

// Event delegation
document.addEventListener('click', function (e) {
    console.clear();
    // exclude select element action ...
    if (!(e.target instanceof HTMLSelectElement)) {
      return console.log(e.target.closest(`.card`));
    }
});
.card{
  background: rosybrown; 
  text-align: center; 
  width: 50%;
}
h2{
  margin-top: 0;
}
<div id="container">
</div>

You can try create the cards and assign listener to them in this way, then use this instead of e.target:

const cardElement = document.createElement('div')
cardElement.classList.add('card')
cardElement.insertAdjacentHTML(
    'beforeend', `
<div class="card-header">Card header content</div>
  <div class="card-body">
    <h2>Card body</h2>
    <select>
      <option value="value1">Value 1</option>
    </select><br>
    Some more text
</div>`)
document.getElementById('container').appendChild(cardElement);

// Event delegation
cardElement.addEventListener('click', function (e) {
  console.clear()
  if (e.target instanceof HTMLSelectElement) return;
  console.log(this);
});
.card{
  background: rosybrown; 
  text-align: center; 
  width: 50%;
}
h2{
  margin-top: 0;
}
<div id="container">
</div>

I'm just expanding a bit on @KooiInc's answer, all credit and upvotes should go to KooiInc... .closest() is a great tool for event delegation.

I think the concept can be demonstrated more clearly by putting in a second card. I've added a cardId so it can be clearly seen in the console output which card was found.

I also moved the delegation from document to the #container — you handle fewer click events if you push the delegation listener down to the closest element that contains the objects you are interested in.

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

let cardId = 1;

// Add the 1st card
container.insertAdjacentHTML(
    'beforeend', `<div class="card" id="${cardId}">
<div class="card-header">Card header content</div>
  <div class="card-body">
    <h2>Card ${cardId} Body</h2>
    <select>
      <option value="value${cardId}">Value ${cardId}</option>
    </select><br>
    Some more text
  </div>
</div>`
);
// Add a 2nd card
++cardId;
container.insertAdjacentHTML(
    'beforeend', `<div class="card" id="${cardId}">
<div class="card-header">Card header content</div>
  <div class="card-body">
    <h2>Card ${cardId} Body</h2>
    <select>
      <option value="value${cardId}">Value ${cardId}</option>
    </select><br>
    Some more text
  </div>
</div>`
);

// Event delegation
container.addEventListener('click', function (e) {
    console.clear();
    console.log(e.target.closest(`.card`));
});
.card{
  background: rosybrown; 
  text-align: center; 
  width: 50%;
}
.card + .card {
  margin-top: 1rem;
}
h2{
  margin-top: 0;
}
<div id="container">
</div>

In real life I would likely be putting most of this in a addCard() function.

Related