Event binding on dynamic HTML, using fetch and within forEach

Viewed 905

I am trying to add an event listener on the HTML element show belown, generated dynamically within a forEach loop. I'm getting the proper response from the JSON Placeholder API initially, however, none of my attempts to bind an event on any of the HTML elements I generate after the initial response seem to produce result when I try out the event.

I'm either getting no 'Clicked' at all or ... addEventListener is not a function, depending on where I put the addEventListener code.

Is there any way to make this possible in a clean and reusable way? Also, is this the preferred way of generating dynamic HTML through vanilla JS? Or should I generate it manually, by using document.createElement() and appendChild() for example?

Ultimately, I am meaning to pass the ID of the clicked target to the getPosts function fetch URL.

Any help would be greatly appreciated!

fetch('https://jsonplaceholder.typicode.com/users')
  .then(res => res.json())
  .then(data => createResult(data));

function createResult(data) {
  const container = document.getElementById('result');

  data.forEach((user) => {
    const { id, name, email, address: { city, street } } = user;

    let result =
      `<div class="user" data-uid=${id}>
          <h5 id="user-${id}"> User ID: ${id} </h5>
            <ul class="w3-ul">
              <li> User Full Name : ${name}</li>
              <li> User Email : ${email} </li>
              <li> User Address : ${city}, ${street} </li>
            </ul>
        </div>`;

    container.innerHTML += result;
    
    document.body.addEventListener("click", function (e) {
      if (e.target &&
        e.target.classList.contains("user")) {
        console.log('Clicked');
      }
    });
  });
}

function getPosts(e) {
  fetch(`https://jsonplaceholder.typicode.com/users/${user.id}`)
    .then(res => res.json())
    .then(data => console.log(data));
}
<div id=result></div>

3 Answers

You should make each result an element and add event listener to that element only

let element = document.createElement('div')
element.innerHTML = result
element = element.firstChild

element.addEventListener("click", function (e) {
  console.log('Clicked', element.getAttribute('data-uid'));
});

container.appendChild(element);

Runnable example

fetch('https://jsonplaceholder.typicode.com/users')
  .then(res => res.json())
  .then(data => createResult(data));

function createResult(data) {
  const container = document.getElementById('result');

  data.forEach((user) => {
    const { id, name, email, address: { city, street } } = user;

    let result =
      `<div class="user" data-uid=${id}>
          <h5 id="user-${id}"> User ID: ${id} </h5>
            <ul class="w3-ul">
              <li> User Full Name : ${name}</li>
              <li> User Email : ${email} </li>
              <li> User Address : ${city}, ${street} </li>
            </ul>
        </div>`;

    let element = document.createElement('div')
    element.innerHTML = result
    element = element.firstChild
    
    element.addEventListener("click", function (e) {
      console.log('Clicked', element.getAttribute('data-uid'));
    });
    
    container.appendChild(element);
  });
}

function getPosts(e) {
  fetch(`https://jsonplaceholder.typicode.com/users/${user.id}`)
    .then(res => res.json())
    .then(data => console.log(data));
}
<div id=result></div>

Should work if

  1. You attach the eventListener ONCE to document.body
  2. You add a third param to the addEventListener call, specifying to use CAPTURE not BUBBLING.

function createResult(data) {
  const container = document.getElementById('result');

  data.forEach((user) => {
    const { id, name, email, address: { city, street } } = user;

    let result =
      `<div class="user" data-uid=${id}>
          <h5 id="user-${id}"> User ID: ${id} </h5>
            <ul class="w3-ul">
              <li> User Full Name : ${name}</li>
              <li> User Email : ${email} </li>
              <li> User Address : ${city}, ${street} </li>
            </ul>
        </div>`;

    container.innerHTML += result;
  });
}

document.body.addEventListener("click", function (e) {
  if (e.target &&
    e.target.classList.contains("user")) {
    console.log('Clicked element ' + e.target.data.uid);
  }
}, false);

There are two problems with your approach. The first problem is that you add the click handler to document.body for every element in data.

If you call createResult() only once then move the .addEventListener() call before or after the .forEach()

container.addEventListener("click", ...);
data.forEach(...);

If you call createResult() multiple times then move the .addEventListener() out of it and maybe call it on DOMContentLoaded

window.addEventListener("DOMContentLoaded", () => {
  document.getElementById('result').addEventListener("click", ...);
});

The second problem is e.target. e.target is the element that triggered the click event. And this most likely wont be div.result but an element in div.result. You have to traverse up the DOM until you find div.result. A possible way can be to use Element.closest() or a loop and Node.parentElement.

fakeFetch().then(createResult);

function createResult(data) {
  const container = document.getElementById('result');

  container.addEventListener("click", function (e) {
    const user = e.target.closest(".user");
    
    if (user) {
      getPosts(user.dataset.uid);
    }
  });

  data.forEach((user) => {
    const { id, name, email, address: { city, street } } = user;

    let result =
      `<div class="user" data-uid="${id}">
          <h5 id="user-${id}"> User ID: ${id} </h5>
            <ul class="w3-ul">
              <li> User Full Name : ${name}</li>
              <li> User Email : ${email} </li>
              <li> User Address : ${city}, ${street} </li>
            </ul>
        </div>`;

    container.innerHTML += result;
  });
}

function getPosts(userId) {
  console.log(userId);
}

function fakeFetch() {
  return new Promise(resolve => {
    const user = [
      { id: 1, name: "a", mail: "a@a.a", address: { city: "a", street: "a" } },
      { id: 2, name: "b", mail: "b@b.b", address: { city: "b", street: "b" } }
    ];
    
    setTimeout(resolve(user), 1000);
  })
}
<div id="result"></div>

And as a second note:
Don't use .innerHTML. This will overwrite the content of the element and therefor remove any event handlers. Use Element.insertAdjacentHTML() instead.

container.innerHTML += result

would then be

container.insertAdjacentHTML("beforeend", result);
Related