37

I am trying to figure out how to bind an event to dynamically created elements. I need the event to persist on the element even after it is destroyed and regenerated.

Obviously with jQuery's live function its easy, but what would they look like implemented with native Javascript?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
jjhenry
  • 515
  • 2
  • 5
  • 8
  • 1
    You could always read the jQuery source :p. Not sure how far it would be from native JS though, since I'm sure it will quite heavily depend on itself by that point (in terms of using selectors and whatnot). – Corbin Feb 02 '12 at 02:27
  • 1
    Just one note: `.live()` is deprecated for a long, long time. It was replaced by `.delegate()`, which was replaced by `.on()`, so please use the last one. Furthermore, the last one shows the difference between binding and delegating, so you may wish to take a look. The most important is checking for event target. – Tadeck Feb 02 '12 at 02:48
  • This answer of mine may help http://stackoverflow.com/a/27373951/1385441 – Ram Patra Jan 06 '16 at 17:30

5 Answers5

28

Here's a simple example:

function live(eventType, elementId, cb) {
    document.addEventListener(eventType, function (event) {
        if (event.target.id === elementId) {
            cb.call(event.target, event);
        }
    });
}

live("click", "test", function (event) {
    alert(this.id);
});

The basic idea is that you want to attach an event handler to the document and let the event bubble up the DOM. Then, check the event.target property to see if it matches the desired criteria (in this case, just that the id of the element).

Edit:

@shabunc discovered a pretty big problem with my solution-- events on child elements won't be detected correctly. One way to fix this is to look at ancestor elements to see if any have the specified id:

function live (eventType, elementId, cb) {
    document.addEventListener(eventType, function (event) {
        var el = event.target
            , found;

        while (el && !(found = el.id === elementId)) {
            el = el.parentElement;
        }

        if (found) {
            cb.call(el, event);
        }
    });
}
Community
  • 1
  • 1
Andrew Whitaker
  • 124,656
  • 32
  • 289
  • 307
  • To the document or - more efficiently - to the container outside of which you do not expect elements of your interest. – Tadeck Feb 02 '12 at 02:44
  • So this is listening to just any click event and when click event occurs, it checks whether the target's id matches the given id or not and do the callback function. Pretty Interesting :) – jwchang Feb 02 '12 at 02:50
  • Right. And as @Tadeck points out, you can limit the bubbling to another containing element for a more efficient listener. – Andrew Whitaker Feb 02 '12 at 02:53
  • This won't work with children DOM elements, though we actually consider this a valid click event as well. – shabunc Dec 18 '13 at 15:27
  • @shabunc: Can you elaborate? Maybe provide an example? – Andrew Whitaker Dec 18 '13 at 17:01
  • @AndrewWhitaker, sure, why not. Here's what I'm talking about - http://jsfiddle.net/9hN22/ – shabunc Dec 18 '13 at 20:23
  • @shabunc: Yep, you're absolutely right. One way to fix this is to check ancestors and see if you hit one that matches the `id` you've specified. I'll update my answer to reflect this. – Andrew Whitaker Dec 19 '13 at 23:23
  • @AndrewWhitaker, what if the selector is not id but like '.nav .active a'? – Binyamin Dec 26 '13 at 21:13
  • 1
    @Binyamin: You would have to write something that parses that selector. If you're dealing with complex selectors you might be better off using jQuery or another framework. – Andrew Whitaker Dec 26 '13 at 22:27
25

In addition to Andrew's post and Binyamin's comment, maybe this is an option:

With this you can use 'nav .item a' as the selector. Based on Andrew's code.

function live (eventType, elementQuerySelector, cb) {
    document.addEventListener(eventType, function (event) {

        var qs = document.querySelectorAll(elementQuerySelector);

        if (qs) {
            var el = event.target, index = -1;
            while (el && ((index = Array.prototype.indexOf.call(qs, el)) === -1)) {
                el = el.parentElement;
            }

            if (index > -1) {
                cb.call(el, event);
            }
        }
    });
}



live('click', 'nav .aap a', function(event) { console.log(event); alert('clicked'); });
Ro NL
  • 450
  • 5
  • 9
  • Thanks so much. One thing I'd suggest though is suporting `e.preventDefault()` inside `addEventListener`. If used, then you'll need to change it to `document.querySelector(elementQuerySelector).addEventListener(eventType, function (event) {` else it will prevent you from clicking any other elements on the page – Lodder Jan 31 '17 at 09:50
11

The other solutions are a little overcomplicated...

document.addEventListener('click', e => {
   if (e.target.closest('.element')) {
       // .element has been clicked
   }
}

There is a polyfill in case you need to support Internet Explorer or old browsers.

Fabian von Ellerts
  • 4,763
  • 40
  • 35
  • 2
    THIS IS PURE GOLD...especially working with ajax functions when html is added in after the fact. it's perfect. thank you ! – Oneezy Feb 05 '21 at 18:40
  • @Oneezy Thank you! For that use-case you can write `document.querySelector('.ajax-container')` instead of `document` to improve performance. – Fabian von Ellerts Feb 10 '21 at 10:36
  • 1
    not a bad idea! What's crazy is that this little piece of code allows me to stop using my `for (const element of elements) { ... }` on things where i need a click event with ajax. I'm curious to know what the performance downsides are though..it seems like there might be a few. I spoke w/ my friend Joah and he re-wrote the code to even be more performant. w/ his version the click event happens on the actual ` – Oneezy Feb 11 '21 at 09:35
  • 1
    @Oneezy Nice, yeah if you create the element with JavaScript it's definitely best to directly add a listener. But if you get raw HTML from the server, a live binding can save you a lot of effort :) I wonder how long it takes to have a visible performance impact, I never had any problems in testing (with click listeners). – Fabian von Ellerts Feb 11 '21 at 15:00
  • Good question. i guess i'll find out haha.. bout to use this everywhere – Oneezy Feb 11 '21 at 15:10
3

An alternative to binding an event to dynamically to a specific element could be a global event listener. So, each time you update the DOM with another new element event on that element will also the "catches". An example:

var mybuttonlist = document.getElementById('mybuttonlist');

mybuttonlist.addEventListener('click', e=>{
  if(e.target.nodeName == 'BUTTON'){
    switch(e.target.name){
      case 'createnewbutton':
        mybuttonlist.innerHTML += '<li><button name="createnewbutton">Create new button</button></li>';
        break;
    }
  }
}, false);
ul {
  list-style: none;
  padding: 0;
  margin: 0;
}
<ul id="mybuttonlist">
  <li><button name="createnewbutton">Create new button</button></li>
</ul>

In this example I have an event listener on the <ul> for click events. So, an event happens for all child elements. From the simple event handler I created, you can see that it is easy to add more logic, more buttons (with different or repeating names), anchors etc.

Going all in, you could add the eventlistener to document instead of the list element, catching all click events on the page and then handle the click events in the event handler.

Mr Lister
  • 45,515
  • 15
  • 108
  • 150
chrwahl
  • 8,675
  • 2
  • 20
  • 30
  • That's a cool idea! But I would rather use `data-event` which has no semantic meaning. And add some way to change the event options. For code like this I recommend `Vue.js`, handles all that stuff for you ;) – Fabian von Ellerts Jul 19 '19 at 19:26
  • @FabianvonEllerts thanks, but I don't see it as a cool idea - this is just how plain JavaScript and DOM works. In my example you could have the event listener listen to the entire document and then use the "data-" attribute to select the proper action - that is just a small rewrite of the example: https://jsbin.com/dabetonasi/edit?html,js,output – chrwahl Aug 29 '19 at 08:55
0

This becomes simpler with the .matches() element api.

const selector = '.item'

document.addEventListener('click', (e) => {
  if ( ! e.target.matches(selector) ) {
    return
  }

  // do stuff here
})

The jQuery equivalent:

$('.item').live( 'click', (e) => {
  // do stuff here
})