Search code examples
javascriptevent-delegation

Add click event to element inserted by javascript


If I click on the first "Edit" I get a console.log('click happend') But if I add a one of these boxes via javascript (click on "Add box") and then the Edit click from this new box does not work. I know it's because the javascript run when the element was not there and that's why there is no click event listener. I also know with jQuery I could do like so:

$('body').on('click', '.edit', function(){ // do whatever };

and that would work.

But how can I do this with plain Javascript? I couldn't find any helpful resource. Created a simple example which I would like to be working. What is the best way to solve this?

So the problem is: If you add a box and then click on "Edit" nothing happens.

var XXX = {};
XXX.ClickMe = function(element){
    this.element = element;
    
    onClick = function() {
        console.log('click happend');
    };
    
    this.element.addEventListener('click', onClick.bind(this));
};

[...document.querySelectorAll('.edit')].forEach(
    function (element, index) {
        new XXX.ClickMe(element);
    }
);


XXX.PrototypeTemplate = function(element) {
    this.element = element;
    var tmpl = this.element.getAttribute('data-prototype');

    addBox = function() {
        this.element.insertAdjacentHTML('beforebegin', tmpl);
    };

    this.element.addEventListener('click', addBox.bind(this));
};


[...document.querySelectorAll('[data-prototype]')].forEach(
    function (element, index) {
        new XXX.PrototypeTemplate(element);
    }
);
[data-prototype] {
  cursor: pointer;
}
<div class="box"><a class="edit" href="#">Edit</a></div>

<span data-prototype="<div class=&quot;box&quot;><a class=&quot;edit&quot; href=&quot;#&quot;>Edit</a></div>">Add box</span>

JSFiddle here

This Q/A is useful information but it does not answer my question on how to solve the problem. Like how can I invoke the eventListener(s) like new XXX.ClickMe(element); for those elements inserted dynamically into DOM?


Solution

  • Here's a method that mimics $('body').on('click', '.edit', function () { ... }):

    document.body.addEventListener('click', function (event) {
      if (event.target.classList.contains('edit')) {
        ...
      }
    })
    

    Working that into your example (which I'll modify a little):

    var XXX = {
      refs: new WeakMap(),
      ClickMe: class {
        static get (element) {
          // if no instance created
          if (!XXX.refs.has(element)) {
            console.log('created instance')
            // create instance
            XXX.refs.set(element, new XXX.ClickMe(element))
          } else {
            console.log('using cached instance')
          }
          
          // return weakly referenced instance
          return XXX.refs.get(element)
        }
    
        constructor (element) {
          this.element = element
        }
        
        onClick (event) {
          console.log('click happened')
        }
      },
      PrototypeTemplate: class {
        constructor (element) {
          this.element = element
          
          var templateSelector = this.element.getAttribute('data-template')
          var templateElement = document.querySelector(templateSelector)
          // use .content.clone() to access copy fragment inside of <template>
          // using template API properly, but .innerHTML would be more compatible
          this.template = templateElement.innerHTML
          
          this.element.addEventListener('click', this.addBox.bind(this))
        }
        
        addBox () {
          this.element.insertAdjacentHTML('beforeBegin', this.template, this.element)
        }
      }
    }
    
    Array.from(document.querySelectorAll('[data-template]')).forEach(function (element) {
      // just insert the first one here
      new XXX.PrototypeTemplate(element).addBox()
    })
    
    // event delegation instead of individual ClickMe() event listeners
    document.body.addEventListener('click', function (event) {
      if (event.target.classList.contains('edit')) {
        console.log('delegated click')
        // get ClickMe() instance for element, and create one if necessary
        // then call its onClick() method using delegation
        XXX.ClickMe.get(event.target).onClick(event)
      }
    })
    [data-template] {
      cursor: pointer;
    }
    
    /* compatibility */
    template {
      display: none;
    }
    <span data-template="#box-template">Add box</span>
    
    <template id="box-template">
      <div class="box">
        <a class="edit" href="#">Edit</a>
      </div>
    </template>

    This uses WeakMap() to hold weak references to each instance of ClickMe(), which allows the event delegation to efficiently delegate by only initializing one instance for each .edit element, and then referencing the already-created instance on future delegated clicks through the static method ClickMe.get(element).

    The weak references allow instances of ClickMe() to be garbage collected if its element key is ever removed from the DOM and falls out-of-scope.