在JavaScript中给动态元素附加事件

192

我正在尝试将HTML数据动态插入到动态创建的列表中,但是当我尝试为动态创建的按钮附加onclick事件时,事件没有触发。非常感谢您提供解决方案。

Javascript代码:

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 代码:

<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>

你是如何创建HTML的? - jim0thy
1
我会说这是因为当您尝试附加事件侦听器时,元素不存在。 - 看看这个https://learn.jquery.com/events/event-delegation/ - Craicerjack
2
将您的addEventListener移动到btnSubmit的事件侦听器中。 - slebetman
嘿,我只是想提一下,似乎你正在尝试以一种困难的方式创建一个包含li元素的ul元素。相反,你可以使用``(反引号)并按照在HTML中通常做的方式放置元素。 - Dan
紧密相关:纯JavaScript事件委托 - Sebastian Simon
密切相关:纯JavaScript事件委托 - undefined
15个回答

345

这是因为您的元素是动态创建的,因此它稍后才会附加到DOM中,但是您的 addEventListener 调用已经在过去发生了。 您应该使用事件委托来处理事件。

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

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

closest 确保了点击事件发生在目标元素内 任何位置 或者就是目标元素本身。 例如,假设你不是点击了 <input id="btnPrepend"/>,而是点击了这个代码:<button id="btnPrepend"><i class="icon">+</i> prepend</button> 中的 <i class="icon">+</i> 元素,则使用该 API 是非常有用的。

jQuery 使得这一切变得更加容易:

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

这里是一篇关于事件委托的文章


5
只要“父级”元素未动态创建,它就应该有效。我会将事件处理程序绑定到动态创建元素的第一个父级,以便不同事件处理程序之间没有冲突。 - jilykate
1
但有时候即使“父级”元素没有动态创建,它在iPhone上也无法正常工作。这里是一个例子,它可以在所有浏览器上运行,但在iPhone(Safari甚至Chrome)上无法运行。我不明白为什么会这样。 - Kholiavko
2
@Kholiavko 这只是因为CodePen使用沙盒来包装所有内容。如果您在普通页面中编写这些代码,它将正常工作。 - jilykate
1
如果您还想为元素的子代添加事件侦听器: - Mawardy
3
考虑添加一个额外的等号(===),以防止意外的类型强制转换。 - Staccato
显示剩余2条评论

47

有一个解决方法,通过捕获对 document.body 的点击,然后检查事件目标。

document.body.addEventListener( 'click', function ( event ) {
  if( event.target.id == 'btnSubmit' ) {
    someFunc();
  };
} );

6
最近的集装箱。 - mplungjan
10
注意:event.srcElement已被弃用。建议使用event.target代替。 - dsanchez

16

区别在于如何创建和添加DOM元素。

如果您通过document.createElement创建一个元素,添加事件监听器,并将其附加到DOM中,则事件将被触发。

如果您像这样将元素作为字符串创建:html += "<li> test </li>"`,则该元素在技术上只是一个字符串。字符串无法具有事件监听器。

一种解决方案是使用document.createElement创建每个元素,然后直接将它们添加到DOM元素中。

// Sample
let li = document.createElement('li')
document.querySelector('ul').appendChild(li)

有类似的东西。很有趣看到这个工作。然而,不确定是否是偶然的工作。有没有任何解释为什么这个工作的文档链接? - qwertynik
有类似的经历。很有趣看到这个工作。然而,不确定是否是偶然的。有没有任何解释为什么这个工作的文档链接? - undefined
2
您的代码示例缺少添加监听器的重要部分,因此会给人一种错误印象,即通过使用document.createElement就能以某种神奇的方式实现这一点。 - connexo

13

在插入元素后,必须附加事件,这样您不会在document上附加全局事件,而是在插入的元素上附加特定事件。

例如:

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>


2
这种方法只适用于CSR(客户端渲染)。这对于SSR(服务器端渲染)不起作用。要使其在CSR和SSR上都起作用,事件委托是解决方案:javascript document.addEventListener('click',function(e){ if(e.target && e.target.id== 'brnPrepend'){ //do something } }); - Amit
但出于性能考虑,这种方法更好。因此,它取决于您的用例。 - R3tep
我的做法是:我使用appendChild将我的元素附加到一个div上,然后在下一行使用innerHTML来添加元素后的空格。结果,事件监听器就在innerHTML的下一行被阻止了!我的解决方案(除了你的答案)是完全避免使用innerHTML:直接在变量上使用普通的事件监听器即可。 - Petr L.

6

以下是一个可重复使用的函数,它利用了 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);
        }
    });
}

这是如何在点击事件中使用它:

delegate_event('click', document, '.alert-button', your_function_here);

4
您可以类似这样做:

您可以像这样做类似的事情:

// 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);

3
我已经创建了一个小型库来帮助处理这个问题: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>

这个功能类似于jQuery.on()。

该库使用Element.matches()方法来测试目标元素是否符合给定的选择器。当触发事件时,只有目标元素与给定的选择器匹配时才会调用回调函数。


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;
    }

现在您可以像这样注册事件:

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

  });

浏览器支持

谷歌浏览器 - 4.0,Edge浏览器 - 9.0,火狐浏览器 - 3.5,Safari浏览器 - 3.2,欧朋浏览器 - 10.0及以上版本。


2

这里是我想到的另一个解决方案。与事件目标相比,我发现这种方法更容易处理,因为如果单击子元素,则不会触发它们。该方法是设置事件侦听器,并在将新元素附加到文档后重置它们:

function listen() {
    document.querySelectorAll('.btn').forEach(btn => {
        btn.addEventListener("click", function(){
            console.log("Hello World");
        });
    });
}

listen();

document.body.innerHTML += "<p class='btn'>Click Here 2.0 .</p>";

listen();
<!DOCTYPE html>
<html>
<body>

<p class='btn'>Click Here.</p>

</body>
</html>

编辑: 为了避免同一事件的堆叠侦听器,请参见此处:答案


1
有两个问题,1)每次添加新项时,您将不得不触发listen()函数,2)当您运行listen()函数时,会将多个类型为“click”的事件绑定到先前的元素。 - Airy

1
我发现jillykate发布的解决方案可行,但仅适用于目标元素是最嵌套的情况。如果不是这种情况,则可以通过迭代父级来纠正,即:
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);

此外,请注意我们可以通过任何属性处理事件,或在任何级别附加侦听器。上面的代码使用自定义属性和 window。我怀疑各种方法之间没有实际差异。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接