Implementing jQuery's "live" binder with native Javascript
Asked Answered
R

5

38

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?

Runner answered 2/2, 2012 at 2:25 Comment(3)
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).Flake
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.Highlands
This answer of mine may help https://mcmap.net/q/45133/-event-binding-on-dynamically-created-elementsSalado
G
27

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);
        }
    });
}
Geiger answered 2/2, 2012 at 2:33 Comment(9)
To the document or - more efficiently - to the container outside of which you do not expect elements of your interest.Highlands
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 :)Codd
Right. And as @Highlands points out, you can limit the bubbling to another containing element for a more efficient listener.Geiger
This won't work with children DOM elements, though we actually consider this a valid click event as well.Manful
@shabunc: Can you elaborate? Maybe provide an example?Geiger
@AndrewWhitaker, sure, why not. Here's what I'm talking about - jsfiddle.net/9hN22Manful
@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.Geiger
@AndrewWhitaker, what if the selector is not id but like '.nav .active a'?Untoward
@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.Geiger
C
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'); });
Cuyler answered 2/8, 2014 at 17:18 Comment(1)
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 pageDanish
F
14

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.

Fugitive answered 1/2, 2019 at 15:19 Comment(5)
THIS IS PURE GOLD...especially working with ajax functions when html is added in after the fact. it's perfect. thank you !Habitat
@Habitat Thank you! For that use-case you can write document.querySelector('.ajax-container') instead of document to improve performance.Fugitive
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 <button> itself, not the document. check it out codepen.io/oneezy/pen/QWGyXzdHabitat
@Habitat 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).Fugitive
Good question. i guess i'll find out haha.. bout to use this everywhereHabitat
A
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.

Asphyxia answered 19/1, 2018 at 12:18 Comment(2)
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 ;)Fugitive
@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: jsbin.com/dabetonasi/edit?html,js,outputAsphyxia
K
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
})
Kosher answered 24/7, 2023 at 17:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.