Goal: reload only the div after ajax calls (add or remove tags) without losing event listeners on the reloaded div. Using insertAdjacentHTML
does not seem optimal.
Front-end: I have a sidebar with ul/li for tags. There's a modal to add/create tags; there is an icon "X" next to each tag to remove it on click.
<ul>
<li><span>A</span><span class=li-untag>X</span>
<li><span>B</span><span class=li-untag>X</span>
<li><span>C</span><span class=li-untag>X</span>
<li><span>D</span><span class=li-untag>X</span>
</ul>
My failed setup was:
send an ajax (fetch) call to the php back-end to add or remove tags
if no error, the back-end sends an updated and twig-formatted list of tags for the article
innerHTML
.The innerHTML
update would lose all the event listeners of the list (such as remove tag when clicking X) until a real refresh of the page. I tried to run the addeventlisteners after the ajax call but it didn't work.
My current setup based on previous SO answers (see below) uses insertAdjacentHTML
. That (sort of) works, but it's clunky.
insertAdjacentHTML
is to append new tags to the list, but it's no longer alphabetically ordered and I have to use some hacky javascript to format the output to match the existing list.I would really prefer having php send a whole updated and formatted list for the div.
Any suggestions for a more elegant way to do this in vanilla js?
FYI: The main answers I have relied on:
Why can event listeners stop working after using element.innerHTML?
Is it possible to append to innerHTML without destroying descendants' event listeners?
If a DOM Element is removed, are its listeners also removed from memory?
JS eventListener click disappearing
Edit: the listener is attached to the "li-untag" class
Edit 2: answering the comment request for some more code. Here is for the removing tag, using the atomic ajax library:
for(let liUntag of document.querySelectorAll('.li-untag')){
liUntag.addEventListener("click", () => altUntag(liUntag))};
const altUntag = (el) => {
atomic("/tags", {
method: 'POST',
data: {
type: 'untag',
slug: el.getAttribute('data-slug'),
tag: el.getAttribute('data-untag')
}
}).then(function (response) {
el.closest('li').style.display = 'none';
console.log(response.data); // xhr.responseText
console.log(response.xhr); // full response
})
.catch(function (error) {
console.log(error.status); // xhr.status
console.log(error.statusText); // xhr.statusText
});
};
The best solution for something like this would be to use event delegation to attach a single listener to the ul
instead of attaching multiple listeners to each li-untag
:
document.querySelector('ul').addEventListener('click', ({ target }) => {
if (target.matches('.li-untag')) {
target.closest('li').remove();
}
});
If you don't want to do that and want to keep a listener on each li-untag
element, you can insert the new HTML, then sort the <li>
s, and attach listeners to each new li-untag
:
const addListeners = () => {
document.querySelectorAll('.li-untag').forEach((span) => {
span.addEventListener('click', ({ target }) => {
target.closest('ul').remove();
});
});
};
// on document load:
addListeners();
// after inserting new HTML:
const lis = [...document.querySelectorAll('ul li')];
lis.sort((a, b) => a.children[0].textContent.localeCompare(b.children[0].textContent));
const ul = document.querySelector('ul');
lis.forEach((li) => {
ul.appendChild(li);
});
addListeners();