javascriptdom-eventsaddeventlistenerevent-delegation

Why does addEventListener work with dynamically created element before it is added to the DOM, but not with its child in JavaScript?


I am trying to dynamically create an accordion info section, where one item is open at a time, and another one being clicked will close the already open item to open the new one.

Each item consists of a title (always visible), and text (the part that collapses/expands).

I do not want the handleExpand function to be run each time the text content is clicked, and run only when the titles are clicked. That's why I want an event listener for the item title divs.

function createInfo() {
    const info = document.querySelector('#info');

    const accordion = document.createElement('div');
    accordion('class', 'accordion');

    const accordionItemList = [...];

    for (const itemObject of accordionItemList) {
        const item = document.createElement('div');
        item.setAttribute('class', 'accordion-item');

        const title = document.createElement('div');
        title.setAttribute('class', 'accordion-title');
        title.innerHTML = `<p>${itemObject.title}</p><div class="arrow">`;
        item.appendChild(title);

        item.innerHTML += `<div class="accordion-text">${itemObject.text}</div>`;

        accordion.appendChild(item);

        // This does not work
        title.addEventListener('click', () => {
            handleExpand(item, accordion.children);
        });

        // This does
        item.addEventListener('click', () => {
            handleExpand(item, accordion.children);
        });
    }

    info.appendChild(accordion);
}

In order to have the titles respond to clicks, I tried adding an event listener to the title divs inside the loop after they are created. Done that way, click events do not fire.

However, the same event added to the item (which contains the title and the text), does fire. I tested this separately (one event commented out each time, not how it is in the code block below).

I thought this might be event delegation at first, but the event on the item firing while the event on the title not firing got me confused. Both are added to the DOM at the end of the function at the same time.

I am trying to understand what could be causing the events to work on one dynamically created element, but not its child.


Solution

  • Modifying innerHTML causes the content to be re-parsed and DOM nodes to be recreated, losing the handlers you have attached.

    You should in general avoid working with .innerHTML because it's considered dangerous. Here is a short video that shows its dangers. Instead you should rely on document.createElement for the <p> tags as well and append them accordingly. like so:

          // insecure 
          title.innerHTML = `<p>${itemObject.title}</p><div class="arrow">`;
    
          // secure
          const paragraph = document.createElement('p')
          paragraph.innerText = itemObject.title;
    
          const arrowDiv = document.createElement('div');
          arrowDiv.classList.add('arrow');
    
          const accordionTextDiv = document.createElement('div');
          accordionTextDiv.innerText = itemObject.text;
          accordionTextDiv.classList.add("accordion-text")
    
          arrowDiv.appendChild(accordionTextDiv);
          title.appendChild(paragraph);
          title.appendChild(arrowDiv)