Vanilla JS event delegation - dealing with child elements of the target element
Asked Answered
O

2

32

I'm trying to do event delegation in vanilla JS. I have a button inside a container like this

<div id="quiz">
    <button id="game-again" class="game-again">
        <span class="icon-spinner icon"></span>
        <span>Go again</span>
    </button>
</div>

And following David Walsh's nice instructions I'm adding an event handler to an ancestor of the button like so:

this.container.addEventListener('click', function(e){
    if (e.target && e.target.id == 'game-again') {
        e.stopPropagation();
        self.publish('primo:evento');
    }
});

Where this.container is the #quiz element. This works half the time, but the rest of the time the target of the click event is one of the spans inside the button, so my event handler isn't called. What's the best way to deal with this situation?

Overview answered 9/6, 2014 at 9:31 Comment(5)
What browsers do you have to support?Antenatal
IE9+ and the main modern onesOverview
Then you can use .matches as long as you get the unprefixed version and use matches.call(e.target,"#game-again,#game-again *") - see my answer for more details.Antenatal
If you want a more generic implementation of event delegation, with a few handy enhancements that's still vanilla JavaScript, check out Oxydizr. It uses HTML5 data attributes so you can completely decouple behavior from style: <button data-action="tasks.remove">X</button>. And you can pass custom data to each of your action handlers taken from the data-action-params HTML5 data attribute.Safford
Thanks Greg, I'll check it out.Overview
K
11

Alternate Solution:

MDN: Pointer events

Add a class to all nested child elements (.pointer-none)

.pointer-none {
  pointer-events: none;
}

Your mark-up becomes

<div id="quiz">
    <button id="game-again" class="game-again">
        <span class="icon-spinner icon pointer-none"></span>
        <span class="pointer-none">Go again</span>
    </button>
</div>

With the pointer set to none, the click event wouldn't fire on those elements.

https://css-tricks.com/slightly-careful-sub-elements-clickable-things/

Kenn answered 11/4, 2019 at 8:43 Comment(1)
See also: stackoverflow.com/a/48683414Billiards
A
46

Newer browsers

Newer browsers support .matches:

this.container.addEventListener('click', function(e){
    if (e.target.matches('#game-again,#game-again *')) {
        e.stopPropagation();
        self.publish('primo:evento');
    }
});

You can get the unprefixed version with

var matches = document.body.matchesSelector || document.body.webkitMatchesSelector || document.body.mozMatchesSelector || document.body.msMatchesSelector || document.body.webkitMatchesSelector

And then use .apply for more browsers (Still IE9+).

Older browsers

Assuming you have to support older browsers, you can walk up the DOM:

function hasInParents(el,id){
    if(el.id === id) return true; // the element
    if(el.parentNode) return hasInParents(el.parentNode,id); // a parent
    return false; // not the element nor its parents
}

However, this will climb the whole dom, and you want to stop at the delegation target:

function hasInParentsUntil(el,id,limit){
    if(el.id === id) return true; // the element
    if(el === limit) return false;
    if(element.parentNode) return hasInParents(el.parentNode,id); // a parent
    return false; // not the element nor its parents
}

Which, would make your code:

this.container.addEventListener('click', function(e){
    if (hasInParentsUntil(e.target,'game-again',container)) { // container should be 
        e.stopPropagation();                                  // available for this
        self.publish('primo:evento');
    }
});
Antenatal answered 9/6, 2014 at 9:39 Comment(7)
You're welcome, let me know how it works out for you and if you have any more questions about the code and how it works.Antenatal
Thanks, so I got the unprefixed version as you suggested, and made my condition "if (matches.apply(e.target, ['#game-again,#game-again *']))" and works fine.Overview
Tried using .matches which was fun ... just seems easier to use an identifier like a data attribute instead.Croom
You've entered document.body.matchesSelector twice in your example.Transpicuous
Why do answers like this get accepted. The way this is structured is very confusing. Don't use code like: self.publish('primo:evento'); in your examples and if you mention something like the unprefixed versions you should give an example of using that as well. On top of that you mention that you can use .apply and you don't include an example of that either. Very messy.Arsenic
@Arsenic the answer is written to answer the question. It is expected that readers are able to use their own critical thinking skills to determine which parts of the answer are relevant to their own needs. If you think that a better answer could be written, I encourage you to do so.Reprovable
How exactly does the * factor into e.target.matches("#game-again *")? I only understand that it allows for matching parent elements with that id. Where can I read more about this?Narcosis
K
11

Alternate Solution:

MDN: Pointer events

Add a class to all nested child elements (.pointer-none)

.pointer-none {
  pointer-events: none;
}

Your mark-up becomes

<div id="quiz">
    <button id="game-again" class="game-again">
        <span class="icon-spinner icon pointer-none"></span>
        <span class="pointer-none">Go again</span>
    </button>
</div>

With the pointer set to none, the click event wouldn't fire on those elements.

https://css-tricks.com/slightly-careful-sub-elements-clickable-things/

Kenn answered 11/4, 2019 at 8:43 Comment(1)
See also: stackoverflow.com/a/48683414Billiards

© 2022 - 2024 — McMap. All rights reserved.