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?

C.OG
  • 6,236
  • 3
  • 20
  • 38
And Finally
  • 5,602
  • 14
  • 70
  • 110
  • 2
    What browsers do you have to support? – Benjamin Gruenbaum Jun 09 '14 at 09:34
  • IE9+ and the main modern ones – And Finally Jun 09 '14 at 09:43
  • 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. – Benjamin Gruenbaum Jun 09 '14 at 09:44
  • 1
    If you want a more generic implementation of event delegation, with a few handy enhancements that's still vanilla JavaScript, check out [Oxydizr](https://github.com/gburghardt/oxydizr). It uses HTML5 data attributes so you can completely decouple behavior from style: ``. And you can pass custom data to each of your action handlers taken from the `data-action-params` HTML5 data attribute. – Greg Burghardt Jun 09 '14 at 12:51
  • Thanks Greg, I'll check it out. – And Finally Jun 09 '14 at 13:58

2 Answers2

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');
    }
});
Community
  • 1
  • 1
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • 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. – Benjamin Gruenbaum Jun 09 '14 at 09:50
  • 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. – And Finally Jun 09 '14 at 10:07
  • Tried using .matches which was fun ... just seems easier to use an identifier like a data attribute instead. – backdesk Dec 04 '14 at 15:13
  • 2
    You've entered `document.body.matchesSelector` twice in your example. – thdoan Aug 15 '16 at 06:28
  • 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. – Tomas Feb 08 '17 at 23:07
  • 3
    @Tomas 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. – zzzzBov Jul 26 '17 at 14:26
  • 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? – Ginger and Lavender Mar 12 '23 at 09:31
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/

C.OG
  • 6,236
  • 3
  • 20
  • 38