Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add an Event Listener on elements that are not yet in the DOM [no-jQuery]?

I would like to attach an event listener (that calls a function) to a button that is not yet present in the DOM. I don't want to use inline-call in the HTML how can I do it?

So far i could achieve what I wanted by creating a global event listener that checks if the element clicked has a specific Id. But I found this solution to be dirty and not optimal.

const messageForm = document.querySelector("#message-form")
const messageTextarea = document.querySelector("#message-textarea");
const messageList = document.querySelector("#message-list");
const messageEmpty = document.querySelector("#message-empty");
let messageNumber = 0;

const messageFormatted = messageText => {
  return `
  <li class="message-item">
    <img class="message-avatar" src="./icons/boy.png" alt="Username">
      <article class="message-content">
        <p class="author">Ernesto Campese</p>
        <p class="message">${messageText}</p>
        <p class="datetime">A moment ago</p>
      </article>
      <div class="message-actions">
        <img id="message-edit" class="action-button" src="./icons/edit.png" alt="" width="22" height="22">
        <img id="message-delete" class="action-button" src="./icons/delete.png" alt="" width="22" height="22">
      </div>
  </li>
  `;
}

document.addEventListener("click", (e) => {
  if (e.target.id === "message-delete") {
    e.target.parentNode.parentNode.remove();
    messageNumber--;
    messageEmptyCheck();
  }
})

messageForm.addEventListener("submit", (e) => {
  e.preventDefault();
  if (messageTextarea.value !== "") {
    messageList.insertAdjacentHTML("beforeend", messageFormatted(messageTextarea.value));
    messageTextarea.value = null;
    messageTextarea.focus();
    messageNumber++;
    messageEmptyCheck();
  }
});

As you can see in the code, inside the <li> that I'm creating there are two IMG, one is for deleting and the other for editing. I want to add an event listener to the delete IMG, so when the user clicks it, the li gets eliminated.

The problem is that I cannot create any function if the element does not exist yet. I would love to do something like:

const messageDeleteButton = document.querySelector("#message-delete");

messageDeleteButton.addEventListener("click", (e) => {
    e.parentNode.parentNode.remove();
  }
})

Hope I was clear enough, thank you guys!

like image 516
RavenJeGames Avatar asked Oct 23 '25 23:10

RavenJeGames


2 Answers

You could parse your HTML string to a document in JS and then add the event listener to that element.

So you've got your Template Literal string.

const messageFormatted = messageText => {
  return `
  <li class="message-item">
    <img class="message-avatar" src="./icons/boy.png" alt="Username">
      <article class="message-content">
        <p class="author">Ernesto Campese</p>
        <p class="message">${messageText}</p>
        <p class="datetime">A moment ago</p>
      </article>
      <div class="message-actions">
        <img id="message-edit" class="action-button" src="./icons/edit.png" alt="" width="22" height="22">
        <img id="message-delete" class="action-button" src="./icons/delete.png" alt="" width="22" height="22">
      </div>
  </li>
  `;
}

You can transform your string into HTML by writing a function like the one below. It uses the DOMParser API which creates a new document that will contain all of your HTML you've written in your string.

const parseStringToHTML = (str) => {
  const parser = new DOMParser();
  return parser.parseFromString(str, 'text/html');
};

And this document works like the one you have in your page, its just a new one that only exists in your JS memory for the time being. Give your string as argument to parseStringToHTML to create HTML.

const message = messageFormatted('This is the message'); // Example message
const messageHTML = parseStringToHTML(message); // Returns a document object with all the features a document object has.

So now that your string is a document you can use methods on it like getElementById, querySelector, etc. But first you must select the element that you need.

const messageDeleteButton = messageHTML.querySelector("#message-delete");

See that I've used messageHTML instead of document. In this scenario messageHTML is a document and therefor we can query inside of it. And now you've found your element inside this document you can add an event listener to it.

messageDeleteButton.addEventListener("click", (e) => {
    e.parentNode.parentNode.remove();
});

Okay, so now the event listener has been added. All you have to do now is append the HTML you need from the messageHTML into the document of your page. The HTML can be found in the messageHTML.body property.

Now instead of insertAdjacentHTML use insertAdjacentElement to insert the element in the position of your choosing. The element you want to append is the <li class="message-item"> which would be the firstElementChild in the body of the messageHTML.body property.

So in your submit event listener of your form change the following line from:

messageList.insertAdjacentHTML("beforeend", messageFormatted(messageTextarea.value));

To all that I've explained above.

// Create string
const message = messageFormatted(messageTextarea.value);

// String to document
const messageHTML = parseStringToHTML(message);

// Select delete element.
const messageDeleteButton = messageHTML.querySelector("#message-delete");

// Add event listener to delete element.
messageDeleteButton.addEventListener("click", (e) => {   
    e.parentNode.parentNode.remove();
});

// Append the <li> element to the messageList.
messageList.insertAdjacentElement("beforeend", messageHTML.body.firstElementChild); 

Don't forget to include the parseStringToHTML function in your code. I understand that this is a lot to work out so if you have any question please do ask.

I hope that this will help you out.

Note I see that event delegation (global event listener) is not something you want to do, although as other have stated, it is more performant and easier to implement than other methods, even my own answer. It would also solve listening for elements that you've added or even removed from your list. You could set it even on the <ul id="message-list"> element like T.J. Crowder suggested.

like image 82
Emiel Zuurbier Avatar answered Oct 26 '25 13:10

Emiel Zuurbier


You're on the right track. The identifying characteristic doesn't have to be an id, it can be anything about the element — a class is a common approach.

There are three DOM methods that will help with making the implementation flexible:

  • closest - starting with the element you call it on, it finds the nearest ancestor matching a given CSS selector
  • contains - checks to see if a node contains another node
  • matches - checks to see if the element you call it on matches a given CSS selector

For instance, your current handler requires that the button be the element that was clicked (event.target). That's fine for your img use case, but what if it were a button with an img inside it? Then event.target might be the button of the img, depending on where the user clicked. closest and contains help with that:

document.addEventListener("click", (e) => {
  const button = e.target.closest("#message-delete");
  if (button) {
    button.closest("li").remove();
    messageNumber--;
    messageEmptyCheck();
  }
})

You'll also want to hook the events on the nearest container that will work. Sometimes, you're stuck with it being document or document.body. But oftentimes, you can do better than that. For instance, when listening for button clicks on buttons that are in the cells of a table, you can listen at the table or tbody/thead level rather than document. That's where contains comes into the equation, it makes sure closest didn't go too far up the document tree:

document.querySelector("selector-for-the-table").addEventListener("click", (e) => {
  const button = e.target.closest("#message-delete");
  if (button && e.currentTarget.contains(button)) {
  //        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    button.closest("li").remove();
    messageNumber--;
    messageEmptyCheck();
  }
})

It's not likely in your case that the search for #message-delete will have gone out of the containing element, so you may not need that bit, but sometimes it can happen and so you want that contains check.

like image 34
T.J. Crowder Avatar answered Oct 26 '25 13:10

T.J. Crowder



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!