Javascript event handler disapearing when creating element dynamically

Viewed 36

Given the minimal code sample below I would expect that each dynamically created button in each table row would produce an alert based on the associated onclick event handler.

class A {
  constructor() {
    var root = document.getElementById('root');
    root.innerHTML =
      `
            <button id="btn-add">+</button>
            <table id="tbl"></table>
          `
    document.getElementById('btn-add').onclick = () => {
      this.add()
    }
    this.counter = 0
  }
  add() {
    var tbl = document.getElementById('tbl').innerHTML +=
      `
            <li>
              <button id="${this.counter}-btn-rmv">-</button>
            </li>
            
          `
    document.getElementById(this.counter + '-btn-rmv').onclick = () => {
      alert('Click event!')
    } //<-- each should have an event handler!
    this.counter++
  }
}
new A()
<div id="root" />

However only the last button in the last row has a functional event handler and produces an alert. The event handlers on the previously created buttons, for some reason are removed. I'm not sufficiently famillier with javascript to understand what is going wrong here, please let me know.

1 Answers

The problem is

var tbl = document.getElementById('tbl').innerHTML +=
`
<li>
<button id="${this.counter}-btn-rmv">-</button>
</li>
`

This is functionally equivalent to

const existingHTML = document.getElementById('tbl').innerHTML;
document.getElementById('tbl').innerHTML = '';
document.getElementById('tbl').innerHTML = existingHTML + ...

The container element essentially gets cleared, and then its contents are re-parsed by the browser according to the new concatenated string with +=. The old elements no longer exist in the document, so the listeners attached to the old elements aren't triggerable (and clicking on any of the - buttons other than the last button will do nothing).

Use insertAdjacentHTML instead, which does not corrupt the existing elements:

class A {
  constructor() {
    var root = document.getElementById('root');
    root.innerHTML =
      `
            <button id="btn-add">+</button>
            <table id="tbl"></table>
          `
    document.getElementById('btn-add').onclick = () => {
      this.add()
    }
    this.counter = 0
  }
  add() {
    document.getElementById('tbl').insertAdjacentHTML('beforeend', `
            <li>
              <button id="${this.counter}-btn-rmv">-</button>
            </li>

          `);
    document.getElementById(this.counter + '-btn-rmv').onclick = () => {
      alert('Click event!')
    } //<-- each should have an event handler!
    this.counter++
  }
}
new A()
<div id="root" />

That said, dynamically created IDs are quite a code smell - consider using event delegation instead:

class A {
  constructor() {
    var root = document.getElementById('root');
    root.innerHTML =
      `
            <button id="btn-add">+</button>
            <table id="tbl"></table>
          `
    root.addEventListener('click', ({
      target
    }) => {
      if (target.matches('[data-btn-rmv]')) {
        console.log('Clicked on', target.dataset.btnRmv);
      }
    });

    document.getElementById('btn-add').onclick = () => {
      this.add()
    }
    this.counter = 0
  }
  add() {
    document.getElementById('tbl').insertAdjacentHTML('beforeend', `
            <li>
              <button data-btn-rmv=${this.counter}>-</button>
            </li>

          `)
    this.counter++
  }
}
new A()
<div id="root" />

Related