2

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.

Samuel Gfeller
  • 840
  • 9
  • 19
  • Which element are you wanting to add the event listener to? After you've inserted your adjacent html, you should be able to add it like any other event listener and other other html elements: `document.querySelector(".card-body select").addEventListener(...); – mykaf Aug 18 '22 at 16:58
  • You can attach the delegation handler at a lower level than `document` — for example you can `document.getElementById('container').addEventListener('click', function(e)...` ; You may also want to read [this other SO question](https://stackoverflow.com/q/9106329/17300). – Stephen P Aug 18 '22 at 17:07
  • 1
    What exactly are you trying to detect when they click??? To get the card travel up the DOM and select the card from the target. `const card = event.target.closest(".card"); if (card) { /* do whatever */}` Other option, add the events to the elements after you add them to the DOM. – epascarello Aug 18 '22 at 18:14
  • Thanks a lot @epascarello! I added an edit with what I wanted to do on a click. It's redirecting the user to a more detailed view of what the card previewed, and for this I added a `data-id` to the card div. – Samuel Gfeller Aug 19 '22 at 08:23

3 Answers3

3

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>
KooiInc
  • 119,216
  • 31
  • 141
  • 177
  • Thank you so much for this answer! That's practically it, but when clicking into the `select`, the event is fired as well. Do you know a way to trigger the event listener only when clicking outside of an "interactive" element like buttons, select, inputs etc that might be in the card? – Samuel Gfeller Aug 19 '22 at 08:03
  • Hi @SamuelGfeller, glad to be of assistance. See the edited answer for a way to exclude the select element from handling. – KooiInc Aug 19 '22 at 08:24
  • Perfect! I had the same idea but used `e.target.tagName !== 'SELECT'`. Is there a difference, would you rather use `instanceof HTMLSelectElement` or `element.tagName`? – Samuel Gfeller Aug 19 '22 at 08:25
  • If you give the select element a unique id, or perhaps a [dataset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) value you can retrieve it by that ;) – KooiInc Aug 19 '22 at 08:46
2

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>
realSamy
  • 189
  • 6
  • I like this approach and often use it myself, where you can use `cardElement.addEventListener(...)` to attach the handler directly to each card. Note that this _does_ create many listeners instead of just one, which is an advantage of delegation, but this is another useful approach. @Saman I'm not upvoting (yet) because I think you need much more detail and explanation in your answer, and adding a Stack Snippet would also be useful. (and I would use _const_ instead of _let_ — `const cardElement = document.createElement(...)`) – Stephen P Aug 18 '22 at 17:30
  • Very nice, here's an upvote! – Stephen P Aug 18 '22 at 18:17
0

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.

Stephen P
  • 14,422
  • 2
  • 43
  • 67