you-dont-need-x / you-dont-need-jquery

The next evolutive state of, "You Might Not Need jQuery", because you definitely don't need jQuery.
The Unlicense
121 stars 10 forks source link

Suggestion: Multiple elements event binding and delegated event handlers #14

Open rafaelcastrocouto opened 2 years ago

rafaelcastrocouto commented 2 years ago

A reason you "still might need jquery" is to bind multiple elements, specially if you need delegated events. From jquery docs https://api.jquery.com/on/

Delegated event handlers have the advantage that they can process events from descendant elements that are added to the document at a later time. ... another advantage of delegated event handlers is their potential for much lower overhead when many elements must be monitored.

$( "table tbody" ).on( "click", "tr", function() {
  console.log( $( this ).text() );
});

It would be really nice to show how to properly do it in vanilla js!

jakearchibald commented 2 years ago
document.body.addEventListener('click', (event) => {
  const tr = event.target.closest('tr');
  if (!tr) return;
  console.log(tr.textContent);
});
rafaelcastrocouto commented 2 years ago

~~but it has the same issue as the one below: what if, for eg, the callback is from a lib and it needs the event.target object to point to the tr element that was clicked?~~

that works great! @jakearchibald found this one here https://www.tech-wiki.online/en/javascript-event-delegation.html seems realy good:

const on = (selector, eventType, childSelector, eventHandler) => {
  const elements = document.querySelectorAll(selector)
  for (element of elements) {
    element.addEventListener(eventType, eventOnElement => {
      if (eventOnElement.target.matches(childSelector)) {
        eventHandler(eventOnElement)
      }
    })
  }
}
jakearchibald commented 2 years ago

You could construct a new event object, but ideally libraries shouldn't have a tight dependency like this.

rafaelcastrocouto commented 2 years ago

Actually i just tested and in your implementation event.target would point to the tr element.

I agree that libraries shouldn't have a tight dependency (as you mention on you blog post about functions as callbacks)

But it's nice to replicate it as it was very useful! So here's the exact functional equivalent acording to @TheTrueNemo

const on = (selector, eventType, childSelector, eventHandler) => {
  const elements = document.querySelectorAll(selector);
  for (element of elements) {
    element.addEventListener(eventType, eventOnElement => {
      const closest = eventOnElement.target.closest(childSelector)
      if (!closest || !element.contains(closest)) {
        return;
      }
      eventHandler(eventOnElement);
    });
  }
}
MariusVatasoiu commented 2 years ago

I usually use composedPath.

document.body.addEventListener('click', (event) => {
  const tr = event.composedPath().find(el=> el.nodeName === 'tr');
  if (!tr) return;
  console.log(tr.textContent);
});

In most cases I use classes to identify the right elements, but you can use whatever check fits you.

Edit:

I think you can also use event.composedPath()[0].closest('tr').

rafaelcastrocouto commented 2 years ago

So here's what I ended up with:

myRootElement.addEventListener('click', (event, ...args) {
  const matches = event.composedPath()
    .filter( (el) => el instanceof HTMLElement )
    .find( (el) => el.matches( myTargetElements ) );
  if (matches) 
    eventHandler.call(event.currentTarget, event, matches, ...args);
});

I'm using call to keep this pointing to the event currentTarget as it described here, passing the matches to the callback to cover the shadow-dom case and using the spread operator to pass whatever arguments might appear (just to be sure). Thank u all, I'm making a pr right now!