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

Asked

Viewed 910 times

19

It is as follows, in jQuery we have the on, any element <a> with class test and without the class foo will trigger the function when clicked, even if you create the element later 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 the test only the elements added later work, ie things like document.querySelector().forEach will not work unless you use Mutationobserver, but then this would still be a third behaviour.

At first I thought jQuery wore Mutationobserver, but after a few tests I realized that actually the event really is in document and #foobar, then as I have custom to wear Vanilla.js I started trying to recreate this, so I used the Element.addEventListener+Event.target, 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 to me very performatic, the doubt is the following:

  • Is there any way to test a specific element with a queryselector?
  • In case, you’re trying to recreate the behavior of on jQuery with Vanilla?

  • @Andersoncarloswoss this "pure Javascript jquery.on", of course my example that works is not perfect, it is more a pseudo-code, but I will adjust it just to be aware

  • @Guilhermenascimento, I erased everything else, never mind. There was a print proving that I did not deny you. I don’t know if that’s what you want this time, I won’t post it as an answer, but that’s what I would reformulate, based on Sergio’s answer and the example function you gave - https://jsfiddle.net/vco5ptwm/1/ - I’m sorry I didn’t understand your question at first.

  • I don’t want anything and whoever it is has already removed the downvote, understand one thing @Renancavalieri, I’m not against downvotes, as long as used properly, see this situation of my past where I received a downvote https://answall.com/questions/155013/o-que-e-para-que-serve-jwt/155045#comment320258_155045, although disagreeing with the downvote at that time, within the understanding of the Kaio it makes sense, so much even though my response suffered I supported. The problem was not the downvote, but his motivation, so I want when I receive a comment as criticizes:

3 answers

22


This is delegation of events with a delegated element associated with a more complex CSS selector.

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

Then we need to treat the event.target to know that the event was in that element, or in a descending. For this we need to check the selectors. We can use the .matches() that checks if a given element hits with a given CSS selector, and associate to :not() which is already supported by modern browsers to ensure that the class wrong 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>

  • The Matches cannot receive the :not, have any reason to separate? The idea would be to use any selector. Thanks already seems to be the best way even.

  • 1

    @Guilhermenascimento can! I thought I was less well supported, IE 9 accepts. I will edit.

  • 3

    I’ll read about it and I’ll be right back with the ;) +1 guaranteed

  • 1

    Thank you very much and congratulations! It worked exactly as expected, I left a reply with a polyfill :) See you more

  • 1

    @Guillhermenascimento was reading your answer +1!

8

An addendum to Sérgio’s response is the question of the prefixes, the Element.matches is not supported by some older browsers, or had a different name (such as matchesSelector).

Navigators and Element.Matches

Even Chrome 33, Opera 15 and Safari 7.0 used the prefix:

.webkitMatchesSelector

Firefox until version 33 used:

.mozMatchesSelector

Internet Explorer uses since version 9 (no version uses the matches):

.msMatchesSelector

Opera Presto from the 11.5 wore:

.oMatchesSelector

Alternative to Element.Matches and prefixes

Older browsers can support querySelectorAll, but they may not have support for matches and prefixed as webkitMatchesSelector, for this the website of MDN I found an example that uses Element.document or Element.ownerDocument, a simple version to test would be this:

function elementMatchs(el, selector) {
    var i = matches.length,
        matches = (el.document || el.ownerDocument).querySelectorAll(selector);

    while (--i >= 0 && matches.item(i) !== el) {}
    return i > -1;
}

Polyfill Element.matchs

The website of MDN also has an interesting example of to have a greater compatibility with old and new browsers:

if (!Element.prototype.matches) {
    Element.prototype.matches = 
    Element.prototype.matchesSelector || 
    Element.prototype.mozMatchesSelector ||
    Element.prototype.msMatchesSelector || 
    Element.prototype.oMatchesSelector || 
    Element.prototype.webkitMatchesSelector ||
    function(s) {
        var matches = (this.document || this.ownerDocument).querySelectorAll(s),
            i = matches.length;

        while (--i >= 0 && matches.item(i) !== this) {}
        return i > -1; 
    };
}

1

This version of the answer works with the element created later. See if the behavior is closer than you need.

Note that on jQuery’s on, the event goes up from the clicked target to the element where the event was attached (in the example, Document), and I tried to simulate it here too, where I attached an event for when clicking on some part of the parent div of the links tests that part.

Finally, by bringing together the ideas of colleagues and understanding the question better, we can have something like this:

<html>
<head>
<title>Sem necessidade para jQuery</title>

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

a:hover {
  color: #aaf;
  cursor: pointer;
}
</style>
</head>

<body>

<div class='links'>
  <a class="qq-coisa">A</a>
  <a class="test foo">B</a>
  <a class="foo">C</a>
  <a class="test">D</a>
</div>

</body>

<script>
Node.prototype.meuOn = function(tipo, seletor, callback) {
  this.addEventListener(tipo, function(evento) {
    var el = evento.target;
    do {
      if (el.matches(seletor)) {
        callback(evento);
        break;
      }
    } while ((el = el.parentNode) != this);
  });
}

document.meuOn('click', 'a.qq-coisa', function(evento) { console.log('qualquer coisa'); console.log(evento); });
document.meuOn('click', 'a.foo', function() { console.log('foo'); });
document.meuOn('click', 'a.test:not(.foo)', function() { console.log('meuOn correto'); });
document.meuOn('click', 'div.links', function() { console.log('clicou no div?'); });

var newLink = document.createElement('a');
newLink.classList.add('qq-coisa');
newLink.innerHTML = 'E?';
document.body.appendChild(newLink);


</script>

</html>

Useful links

  • foreach I know how to use, this is nowhere near the behavior of $.on, Do not rely on the answers is based on the question, understand her, neither you nor Renan understood the expected behavior of the script. Read on the "question" in particular the part I wrote this: mesmo que você crie o elemento depois do evento já estar adicionado

  • with its latest editions and the comment understood better the behavior, I will edit my reply.

  • Thank you very much, I’m waiting ;)

  • it took me a while, but there it is

  • Thank you so much! Just a tip, change Node for Element, Node represents #text also what can be a problem ;)

  • If you swap Node for Element, you cannot attach events in Document as in the example. Every Document object implements the Document interface (and consequently the Node and Eventtarget interfaces). (https://developer.mozilla.org/en-US/docs/Web/API/Document)

  • My fault, it’s true.

Show 2 more comments

Browser other questions tagged

You are not signed in. Login or sign up in order to post.