How to use $.on in pure JavaScript: "$(…).on(event, selector, function)"?

Question:

The thing is, in jQuery we have on , any <a> element with class test and without class foo will trigger the function when clicked, even if you create the element after the event is already added:

$("#new").click(function () {
    $('<p><a class="test" href="#">Novo: (' + (new Date) + ')</a></p>').appendTo("#container");
});

$("#container").on("click", "a.test:not(.foo)", function () {
    console.log("Funcionou!");
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<button id="new">Adicionar novo</button><br>

<div id="container">
   <p><a href="#">Oi (não funciona)</a></p>
</div>

Note that in testing only the elements added later work, ie things like document.querySelector().forEach won't work unless you use MutationObserver , but then this would still be a third behavior .

At first I thought jQuery used MutationObserver , but after some testing I realized that the event is actually in document and #foobar , so as I tend to use Vanilla.js I started trying to recreate this, for that I used Element.addEventListener + Event.target , it was something like:

var container = document.getElementById("container");
var newBtn = document.getElementById("new");

on(container, "click", "a.test:not(.foo)", function () {
    console.log("achou:", this);
});

newBtn.onclick = function () {
    var n = document.createElement("p");

    n.innerHTML = '<a class="test" href="#">Novo: (' + (new Date) + ')</a>';

    container.appendChild(n);
};

function on(target, type, selector, callback)
{
    target.addEventListener(type, function (e)
    {
         var el = e.target,
             els = document.querySelectorAll(selector);

         for (var i = 0, j = els.length; i < els.length; i++) {
             if (els[i] === el) {
                 callback.call(el, e); //Passa o elemento como this e o event como primeiro argumento
                 break;
             }
         }
    });
}
<button id="new">Adicionar novo</button><br>

<div id="container">
   <p><a href="#">Oi (não funciona)</a></p>
</div>

However this does not seem very performative to me, the question is:

  • Is there any way to test a specific element with a queryselector ?

Answer:

This is event delegation with delegate element associated with a more complex CSS selector.

$(document).on("click" means that the event listener is tied to document . So we already have document.addEventListener .

Then we need to handle event.target to know that the event was on that element, or on a descendant. For that we need to check the selectors. We can use .matches() which checks if a given element .matches() a given CSS selector, and associate it with :not() which is already supported by modern browsers to ensure that the wrong class is not accepted.

We could do it like this:

document.addEventListener('click', function(e) {
  var correto = e.target.matches('a.test:not(.foo)');
  // ...
});

Example with tests:

document.addEventListener('click', function(e) {
  var correto = e.target.matches('a.test:not(.foo)');
  
  // o resto é só para o exemplo
  var seletor = [e.target.tagName.toLowerCase(), ...e.target.className.split(' ')].filter(Boolean).join('.');
  console.log(seletor, '|', correto ? 'encontrado!' : 'falhou...', e.target.innerHTML);

});
a {
  display: inline-block;
  margin: 5px;
  padding: 10px;
  border: 1px solid #005;
  width: 10px;
}

a:hover {
  color: #aaf;
  cursor: pointer;
}
<a class="qq-coisa">A</a>
<a class="test foo">B</a>
<a class="foo">C</a>
<a class="test">D</a>
Scroll to Top