18

I would emulate in pure Javascript the main functionality of jQuery .on( events , selector , data) method.

For example

$(document).on('click','.button',function() {
   console.log("jquery onclick"); 
});

I thought it was enough make something like this

document.addEventListener('click',function(e) {
    if(e.target.className == 'button2') {
         console.log("It works");   
    }
});

However when I have this html structure:

<button class="button2">Hello <span>World</span></button>

my script doesn't works when the click event is triggered on span element, because e.target is span. (I ignore for this question the complexity of elements with multiple class, and crossbrowsers compatibility)

The source of jQuery is not simple to read and I don't understand how it works (because the first piece of code, in jQuery, works with my html structure).

I need this method because my html is dynamic, and buttons with this class are created, deleted and re-created many times. I don't want add listeners every times.

I would avoid, if possible, to include jquery library.

So, I can do this?

Here the jsFiddle for testing.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Luca Rainone
  • 16,138
  • 2
  • 38
  • 52

4 Answers4

17

Update for 2017: current DOM standards like closest mean this is now much easier.

const addEventForChild = function(parent, eventName, childSelector, cb){      
  parent.addEventListener(eventName, function(event){
    const clickedElement = event.target,
    matchingChild = clickedElement.closest(childSelector)
    if (matchingChild) cb(matchingChild)
  })
};

To use it just:

addEventForChild(parent, 'click', '.child', function(childElement){
  console.log('Woo click!', childElement)
})

Here's a jsfiddle

Arsen K.
  • 5,494
  • 7
  • 38
  • 51
mikemaccana
  • 110,530
  • 99
  • 389
  • 494
  • this looks good - but it doesn't support IE11 or IE10? https://caniuse.com/#feat=element-closest – Andrew Newby Mar 20 '18 at 13:55
  • 4
    @AndrewNewby Not sure why you're supporting IE at all in 2018, but you could polyfill `element.closest` and use it until you drop IE. – mikemaccana Mar 20 '18 at 14:40
  • unfortunatly 5% of our users still use IE11, and 8% use Edge. Damn annoying! I'm just having a play with it. For some reason its being weird (triggering when I'm not expecting it to). Hopefully I can get it going, as this is a much nicer (and simpler) bit of code to all the other stuff I've found around :) – Andrew Newby Mar 20 '18 at 14:46
  • 1
    @AndrewNewby no worries and good luck! Edge is cool (sometimes ahead of Chrome) BTW. – mikemaccana Mar 20 '18 at 15:05
  • This would fail in case the delegated element is itself a descendant of an Element matching the selector. To be correct, it should stop at the delegated element (and ignore this delegated element). Here is a fiddle showing the issue: https://jsfiddle.net/hm3skn9p/ and here is the fix: https://jsfiddle.net/eu6zn0pw/ – Kaiido Oct 28 '21 at 00:55
11

This is actually surprisingly simple. You're on the right track, but it's not quite there.

Here's the functions I use:

window.addEvent = function(elem,type,callback) {
    var evt = function(e) {
        e = e || window.event;
        return callback.call(elem,e);
    }, cb = function(e) {return evt(e);};
    if( elem.addEventListener) {
        elem.addEventListener(type,cb,false);
    }
    else if( elem.attachEvent) {
        elem.attachEvent("on"+type,cb);
    }
    return elem;
};
window.findParent = function(child,filter,root) {
    do {
        if( filter(child)) return child;
        if( root && child == root) return false;
    } while(child = child.parentNode);
    return false;
};
window.hasClass = function(elem,cls) {
    if( !('className' in elem)) return;
    return !!elem.className.match(new RegExp("\\b"+cls+"\\b"));
};

The window.findParent is central to the whole thing, as you can see when I show you how to attach your desired on listener:

window.addEvent(document.body,"click",function(e) {
    var s = window.findParent(e.srcElement || e.target,function(elm) {
        return window.hasClass(elm,"button");
    },this);
    if( s) {
        console.log("It works!");
    }
});
Niet the Dark Absol
  • 320,036
  • 81
  • 464
  • 592
  • It's fine. But in `while` of findParent function, you should not to check if child != original elem? (in this case document.body). Because if it's a simple div, you should not check all parent until document.body) – Luca Rainone Feb 03 '13 at 21:02
  • That's true. I never really thought of that since I'm always using the body as the single "unchanging" element. I'll update the code posted here to allow a variable root. – Niet the Dark Absol Feb 03 '13 at 21:08
  • I also had a typo (missing close parens). Fixed that. – Niet the Dark Absol Feb 03 '13 at 21:09
  • OK thank you. +1. Are you sure that's the only way? Because, imho, it's heavy for each click in document body that doesn't match the original filter (a click in another button, or on simple anchor ecc). For each click, this script search every element until the main tag (body) and make a regexp on className for all this. Does jquery do this too? – Luca Rainone Feb 03 '13 at 21:22
  • I have no idea how jQuery works, but I can't imagine it being much different (just far less efficient) – Niet the Dark Absol Feb 03 '13 at 21:30
  • Just to let you know, I'm using this script myself, and it doesn't cause any performance impact, even in slow versions like IE7. – Niet the Dark Absol Feb 03 '13 at 21:32
  • OK Thank you. And I've checked: jQuery use the same process (https://github.com/jquery/jquery/blob/master/src/event.js#L399). So probably is the better method. – Luca Rainone Feb 03 '13 at 21:37
  • here's a jsFiddle I made where I modernized the above functions and wrapped it something that more resembles using jQuery: https://jsfiddle.net/ewmuuon4/1/ – Francisc0 May 25 '16 at 21:20
  • Thats amazing solution. But how to remove events when element is removed from the dom? – Pradeep May 23 '18 at 07:38
1

Easy and short code:

function onEvt(type, callback) {
    if (document.attachEvent) {
        document.attachEvent("on" + type, function (e) {
            callback(e.target);
        });
    } else {
        document.addEventListener(type, function (e) {
            callback(e.target);
        }, false);
    }
}

Call the function like this:

onEvt('click', function(elem){ // click, mouseover etc...
    // for class
    if(elem.classList.contains('classname')){
        // do stuff
    }
    // for attribute
    if(elem.hasAttribute('target')){
        // do stuff
    }
});
Pang
  • 9,564
  • 146
  • 81
  • 122
Krishna Torque
  • 623
  • 1
  • 7
  • 17
-1

You can use function Marchy8 on github via link or extend it by doing the following.

Firstly create $() selector function:

function $(selector) {
    return selector;
}

HTMLElement = typeof (HTMLElement) != "undefined" ? HTMLElement : Element;
String.prototype.on = function(type, callback){
    var selector = this;
    document.body.addEventListener(type, function (event) {
        if (event.target.matches(selector)) {
            callback.call(event.target);
        }
    });
}

$(".test").on("click", function(e) {
    console.log(this.innerHTML);
    this.style.color = "red";
});
<div class="test">Click Here</div>
<div class="test">Click Here-1</div>
  • can you find a solution without adding the `on` function to `String`? Perhaps by returning a custom object from the `$` function? – andy Jun 09 '21 at 22:43