191

I'm trying to insert html data dynamically to a list that is dynamically created, but when i try to attach an onclick event for the button that is dynamically created the event is not firing. Solution would be really appreciated.

Javascript code:

document.addEventListener('DOMContentLoaded', function () {
   document.getElementById('btnSubmit').addEventListener('click', function () {
        var name = document.getElementById('txtName').value;
        var mobile = document.getElementById('txtMobile').value;
        var html = '<ul>';
        for (i = 0; i < 5; i++) {
            html = html + '<li>' + name + i + '</li>';
        }
        html = html + '</ul>';

        html = html + '<input type="button" value="prepend" id="btnPrepend" />';
        document.getElementsByTagName('form')[0].insertAdjacentHTML('afterend', html);
    });

    document.getElementById('btnPrepend').addEventListener('click', function () {
        var html = '<li>Prepending data</li>';
        document.getElementsByTagName('ul')[0].insertAdjacentHTML('afterbegin', html);
    });
});

HTML Code:

<form>
    <div class="control">
        <label>Name</label>
        <input id="txtName" name="txtName" type="text" />
    </div>
    <div class="control">
        <label>Mobile</label>
        <input id="txtMobile" type="text" />
    </div>
    <div class="control">
        <input id="btnSubmit" type="button" value="submit" />
    </div>
</form>
shuboy2014
  • 1,350
  • 2
  • 18
  • 44
Manju
  • 2,520
  • 2
  • 14
  • 23
  • How are you creating the html? – jim0thy Jan 20 '16 at 09:29
  • 1
    I would say its because the element doesnt exist when you try to attach the event listener. - have a look at this https://learn.jquery.com/events/event-delegation/ – Craicerjack Jan 20 '16 at 09:31
  • 2
    Move your addEventListener into the event listener of btnSubmit – slebetman Jan 20 '16 at 09:34
  • Hey, I just wanted to mention that it seems like you're trying to create a ul element with li elements within it a hard way. Instead, you could just use `` (backticks) and put elements in the way you'd normally do it in HTML. – Dan Mar 21 '21 at 20:46
  • Closely related: [Vanilla JavaScript Event Delegation](/q/23508221/4642212). – Sebastian Simon Jan 15 '23 at 04:07

14 Answers14

333

This is due to the fact that your element is dynamically created, so it is attached to the DOM later, but your addEventListener call already occurred in the past. You should use event delegation to handle the event.

document.addEventListener("click", function(e){
  const target = e.target.closest("#btnPrepend"); // Or any other selector.

  if(target){
    // Do something with `target`.
  }
});

closest ensures that the click occurred anywhere inside the target element or is the target element itself. This is useful if, for example, instead of your <input id="btnPrepend"/> you had a <button id="btnPrepend"><i class="icon">+</i> prepend</button> and you clicked the <i class="icon">+</i>.

jQuery makes it easier:

$(document).on("click", "#btnPrepend", function(){
  // Do something with `$(this)`.
});

Here is an article about event delegation.

Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
jilykate
  • 5,430
  • 2
  • 17
  • 26
  • 1
    Thank you! Sometimes pure JS solution does not work on iphone, but if change document on some another parent div, for example, it works. What the best solution for delegation ? – Kholiavko May 10 '17 at 14:16
  • 5
    @Kholiavko I would say as long as the "parent" element is not dynamically created, it should work. I would bind event handler to the first parent of dynamic create d element so that there is no conflicts between different event handlers – jilykate May 11 '17 at 07:47
  • 1
    But sometimes it does not work on iphone, even "parent" element is not created dynamically. Here is [exapmle](https://codepen.io/kholiavko-roman/pen/WjMyMj), it works on all browser, but does not on iphone (safary and even chrome). And I don`t understand why. – Kholiavko May 11 '17 at 09:57
  • 2
    @Kholiavko this is just because codepen use a sandbox to wrap everything. If you really write those code in a normal page it will work – jilykate May 11 '17 at 13:04
  • 1
    if you want to add the event listener to the children of the element also : https://stackoverflow.com/questions/16863917/check-if-class-exists-somewhere-in-parent-vanilla-js – Mawardy Mar 24 '19 at 15:36
  • 3
    Consider adding an extra equal sign (===) to prevent unexpected type coercion – Staccato Sep 04 '19 at 20:54
  • Nowadays, `const target = e.target.closest("#brnPrepend"); if(target){`…`}` should be used. – Sebastian Simon Dec 08 '21 at 09:54
45

There is a workaround by capturing clicks on document.body and then checking event target.

document.body.addEventListener( 'click', function ( event ) {
  if( event.target.id == 'btnSubmit' ) {
    someFunc();
  };
} );
carry0987
  • 117
  • 1
  • 2
  • 14
Manasov Daniel
  • 1,348
  • 9
  • 17
16

The difference is in how you create and append elements in the DOM.

If you create an element via document.createElement, add an event listener, and append it to the DOM. Your events will fire.

If you create an element as a string like this: html += "<li>test</li>"`, the elment is technically just a string. Strings cannot have event listeners.

One solution is to create each element with document.createElement and then add those to a DOM element directly.

// Sample
let li = document.createElement('li')
document.querySelector('ul').appendChild(li)
Richard
  • 2,396
  • 23
  • 23
  • Had something similar. Interesting to see this work. However, uncertain if works accidentally. Any documentation link explaining why this works? – qwertynik Jan 20 '23 at 08:01
  • 2
    Your code example lacks the important part of adding a listener, hence giving the false impression that somehow magically this is achieved by using `document.createElement`. – connexo Jan 21 '23 at 08:59
13

You must attach the event after insert elements, like that you don't attach a global event on your document but a specific event on the inserted elements.

e.g.

document.getElementById('form').addEventListener('submit', function(e) {
  e.preventDefault();
  var name = document.getElementById('txtName').value;
  var idElement = 'btnPrepend';
  var html = `
    <ul>
      <li>${name}</li>
    </ul>
    <input type="button" value="prepend" id="${idElement}" />
  `;
  /* Insert the html into your DOM */
  insertHTML('form', html);
  /* Add an event listener after insert html */
  addEvent(idElement);
});

const insertHTML = (tag = 'form', html, position = 'afterend', index = 0) => {
  document.getElementsByTagName(tag)[index].insertAdjacentHTML(position, html);
}
const addEvent = (id, event = 'click') => {
  document.getElementById(id).addEventListener(event, function() {
    insertHTML('ul', '<li>Prepending data</li>', 'afterbegin')
  });
}
<form id="form">
  <div>
    <label for="txtName">Name</label>
    <input id="txtName" name="txtName" type="text" />
  </div>
  <input type="submit" value="submit" />
</form>
R3tep
  • 12,512
  • 10
  • 48
  • 75
  • 2
    This approach will only work for CSR (Client Side Rendering). This will not work for SSR (Server-Side Rendering). To make it work for both CSR and SSR, [Event Delegation](https://stackoverflow.com/questions/1687296/what-is-dom-event-delegation) is the solution: ```javascript document.addEventListener('click',function(e){ if(e.target && e.target.id== 'brnPrepend'){ //do something } });``` – Amit May 14 '20 at 14:59
  • But for performance reason, this approach is better. So it's depend of your use case. – R3tep May 14 '20 at 15:06
  • My take: I used appendChild to append my element to a div, and ON THE NEXT LINE messed with innerHTML to add a whitespace after the element: Result the event listener right the next line after innerHTML, was blocked! My solution (apart from your answer) was avoiding innerHTML altogether: the normal event listener on the variable worked – Petr L. Aug 01 '22 at 12:26
6

Here's a reusable function that takes advantage of element.matches:

function delegate_event(event_type, ancestor_element, target_element_selector, listener_function)
{
    ancestor_element.addEventListener(event_type, function(event)
    {
        if (event.target && event.target.matches && event.target.matches(target_element_selector))
        {
            (listener_function)(event);
        }
    });
}

Here's how you would use it for a click event:

delegate_event('click', document, '.alert-button', your_function_here);
Pikamander2
  • 7,332
  • 3
  • 48
  • 69
4

You can do something similar to this:

// Get the parent to attatch the element into
var parent = document.getElementsByTagName("ul")[0];

// Create element with random id
var element = document.createElement("li");
element.id = "li-"+Math.floor(Math.random()*9999);

// Add event listener
element.addEventListener("click", EVENT_FN);

// Add to parent
parent.appendChild(element);
PRDeving
  • 679
  • 3
  • 11
3

I have created a small library to help with this: Library source on GitHub

<script src="dynamicListener.min.js"></script>
<script>
// Any `li` or element with class `.myClass` will trigger the callback, 
// even elements created dynamically after the event listener was created.
addDynamicEventListener(document.body, 'click', '.myClass, li', function (e) {
    console.log('Clicked', e.target.innerText);
});
</script>

The functionality is similar to jQuery.on().

The library uses the Element.matches() method to test the target element against the given selector. When an event is triggered the callback is only called if the target element matches the selector given.

XCS
  • 27,244
  • 26
  • 101
  • 151
2
var __ = function(){
    this.context  = [];
    var self = this;
    this.selector = function( _elem, _sel ){
        return _elem.querySelectorAll( _sel );
    }
          this.on = function( _event, _element, _function ){
              this.context = self.selector( document, _element );
              document.addEventListener( _event, function(e){
                  var elem = e.target;
                  while ( elem != null ) {
                      if( "#"+elem.id == _element || self.isClass( elem, _element ) || self.elemEqal( elem ) ){
                          _function( e, elem );
                      }
                      elem = elem.parentElement;
                  }
              }, false );
     };

     this.isClass = function( _elem, _class ){
        var names = _elem.className.trim().split(" ");
        for( this.it = 0; this.it < names.length; this.it++ ){
            names[this.it] = "."+names[this.it];
        }
        return names.indexOf( _class ) != -1 ? true : false;
    };

    this.elemEqal = function( _elem ){
        var flg = false;
        for( this.it = 0; this.it < this.context.length;  this.it++ ){
            if( this.context[this.it] === _elem && !flg ){
                flg = true;
            }
        }
        return flg;
    };

}

    function _( _sel_string ){
        var new_selc = new __( _sel_string );
        return new_selc;
    }

Now you can register event like,

_( document ).on( "click", "#brnPrepend", function( _event, _element ){
      console.log( _event );
      console.log( _element );
      // Todo

  });

Browser Support

chrome - 4.0, Edge - 9.0, Firefox - 3.5 Safari - 3.2, Opera - 10.0 and above

paranjothi
  • 103
  • 6
1

I have found the solution posted by jillykate works, but only if the target element is the most nested. If this is not the case, this can be rectified by iterating over the parents, i.e.

function on_window_click(event)
{
    let e = event.target;

    while (e !== null)
    {
        // --- Handle clicks here, e.g. ---
        if (e.getAttribute(`data-say_hello`))
        {
            console.log("Hello, world!");
        }

        e = e.parentElement;
    }
}

window.addEventListener("click", on_window_click);

Also note we can handle events by any attribute, or attach our listener at any level. The code above uses a custom attribute and window. I doubt there is any pragmatic difference between the various methods.

c z
  • 7,726
  • 3
  • 46
  • 59
0

I've made a simple function for this.

The _case function allows you to not only get the target, but also get the parent element where you bind the event on.

The callback function returns the event which holds the target (evt.target) and the parent element matching the selector (this). Here you can do the stuff you need after the element is clicked.

I've not yet decided which is better, the if-else or the switch

var _case = function(evt, selector, cb) {
  var _this = evt.target.closest(selector);
  if (_this && _this.nodeType) {
    cb.call(_this, evt);
    return true;
  } else { return false; }
}

document.getElementById('ifelse').addEventListener('click', function(evt) {
  if (_case(evt, '.parent1', function(evt) {
      console.log('1: ', this, evt.target);
    })) return false;

  if (_case(evt, '.parent2', function(evt) {
      console.log('2: ', this, evt.target);
    })) return false;

  console.log('ifelse: ', this);
})


document.getElementById('switch').addEventListener('click', function(evt) {
  switch (true) {
    case _case(evt, '.parent3', function(evt) {
      console.log('3: ', this, evt.target);
    }): break;
    case _case(evt, '.parent4', function(evt) {
      console.log('4: ', this, evt.target);
    }): break;
    default:
      console.log('switch: ', this);
      break;
  }
})
#ifelse {
  background: red;
  height: 100px;
}
#switch {
  background: yellow;
  height: 100px;
}
<div id="ifelse">
  <div class="parent1">
    <div class="child1">Click me 1!</div>
  </div>
  <div class="parent2">
    <div class="child2">Click me 2!</div>
  </div>
</div>

<div id="switch">
  <div class="parent3">
    <div class="child3">Click me 3!</div>
  </div>
  <div class="parent4">
    <div class="child4">Click me 4!</div>
  </div>
</div>

Hope it helps!

ssten
  • 1,848
  • 1
  • 16
  • 28
0

I know that the topic is too old but I gave myself some minutes to create a very useful code that works fine and very easy using pure JAVASCRIPT. Here is the code with a simple example:

String.prototype.addEventListener=function(eventHandler, functionToDo){
  let selector=this;
  document.body.addEventListener(eventHandler, function(e){
    e=(e||window.event);
    e.preventDefault();
    const path=e.path;
    path.forEach(function(elem){
      const selectorsArray=document.querySelectorAll(selector);
      selectorsArray.forEach(function(slt){
        if(slt==elem){
          if(typeof functionToDo=="function") functionToDo(el=slt, e=e);
        }
      });
    });
  });
}

// And here is how we can use it actually !

"input[type='number']".addEventListener("click", function(element, e){
 console.log( e ); // Console log the value of the current number input
});
<input type="number" value="25">
<br>
<input type="number" value="15">
<br><br>
<button onclick="addDynamicInput()">Add a Dynamic Input</button>
<script type="text/javascript">
  function addDynamicInput(){
    const inpt=document.createElement("input");
          inpt.type="number";
          inpt.value=Math.floor(Math.random()*30+1);
    document.body.prepend(inpt);
  }
</script>
Adnane Ar
  • 683
  • 7
  • 11
0
First of all add the dynamic class to the dynamically created inputboxes
var ele = document.createElement('textarea');
ele.className = "css-class-name"; // set the CSS class
ele.setAttribute('type', 'textarea');
ele.setAttribute('value', '');
ele.setAttribute("id", `row${rcount}_${c}`);
 then do the following 
            const btns = document.querySelectorAll('.css-class-name');
            for (let i = 0; i < btns.length; i++) {
                btns[i].addEventListener('keyup', function (e) {
                    console.log(e.target.id);
                    let textValues = $(`#${e.target.id}`).val()
                    console.log("=============values =====", textValues)
                    //on key press take id and set value of that id what i am inputting.
                });
            }
Sajid Khaki
  • 121
  • 1
  • 3
0

I like @Pikamander2 solution because it does not involve an event binding on document, or body (you will wake up someday with each click on document triggering dozens of event handlers...).

Here is an improvement of Pikamander2 solution.

If the dynamically added child is itself a dom element with children (ex: <button><u>label</u></button>, the e.target may return the <u> element. So you may use :

function delegateEvent( eventType, ancestorElem, childSelector, eventHandler ) {

    // Cancel if ancestorElem does not exists
    if( ! ancestorElem || ( typeof ancestorElem === 'string' && ! ( ancestorElem = document.querySelector( ancestorElem ) ) ) ) {
        return
    }

  ancestorElem.addEventListener( eventType, e => {
    if( e.target && e.target.closest && e.target.closest( childSelector ) ) {
      ( eventHandler )( e )
    }
  } )

}

I also added a snippet to make the function accept a selector for ancestor instead of an element only

ZalemCitizen
  • 474
  • 7
  • 13
-1

This is an old question - but I spent a lot of hours with this problem. Maybe my solution helps someone.

Context: Attaching event listeners to elements dynamically created from an Ajax response. Making an selectable by click, or by keyboard navigation: In both cases, the same function is called.

The first line worked, the second line did not allow me to click on the item:

item.addEventListener("mouseover", this.mouseOver.bind(this) )
item.addEventListener("click", this.clickedItem.bind(this) )

However, this continued to work:

item.click();

(Suggesting the click event was added, and calling the function - but the mouse was not dispatching a click event)

The solution was to change :

item.addEventListener("click", this.clickedItem.bind(this) )

to this:

item.addEventListener("mousedown", this.clickedItem.bind(this) )

I saw on old SO message that suggested that <div>s do not respond to mouse click events - but all the documentation I read contradicted that.

So, I don't know what's going on here.