jquery event delegation in plain js

jquery event delegation in plain js

When you add an event handler, do you delegate it?

Thanks to jQuery, I do. jQuery 1.3 shipped a function called .live() with which you could add event handlers for elements, that are in your DOM and/or may appear later - and I am talking about January 2009!

This function was so great, the jQuery team deprecated it in version 1.7 (November 2011) and removed it in version 1.9 (January 2013).
The void was filled with .delegate() and finally replaced with .on().

The reason I am telling this, is that the .live() and the .on() function, changed my way of event handling entirely.
Content you add or replace via ajax or things like infinite loaders work with only one initial setup. Every click is pointing to one (1) same function, and not to its own anonymous function:

// `n` anonymous handler functions
$('.my-class').on('click', function () {
  $(this).toggleClass('x');
});

// 1 handler function, bound to every existing element
$('.my-class').on('click', toggleSth);

// 1 delegated handler function, working with future DOM `.my-class` element
$(document).on('click', '.my-class', toggleSth);

I compared some ways of event handling in January 2015: jQuery vs Vanilla JS featuring JSLint = good practice.

You Might Not Need jQuery

When it comes to this point in any discussion I am torn back and forth. Sadly, they do not show a delegate alternative there: youmightnotneedjquery.com/#on. And still (June 2017) using jQuery feels like staying behind the buzz - filling the gap with other libs or frameworks does not make sense to me, and maybe your plain js code is not as good as jQuery's implementation. So it might be that without jQuery your website is loading faster, but running slower.

In too many projects I need to support IE11 and taking jQuery onboard means less browser testing. A .next(), .closest(), .remove(), addClass(), etc. will work in every browser, no surprises.

But every dependency needs some time. Time to download, time being parsed, maybe time it is render blocking, and this is the time before my code is downloaded, parsed and run… This should be my time!

time evaluating scripts

Tipp: A good article about how to embed javascripts best in your page is this: async vs defer.

I prefer <script … defer></script>, so the browser can build its DOM, its CSSOM and when the browser is ready, my JS knocks everything off ;-). But make your own decision, every way of js embedding has its place.

Example

At we-love.ai there is a video tile. If you configure a video, a play button is displayed and when you click on it, a video starts playing.

Let's have a look on how event delegation looks in jQuery and in plain js.

Both examples will expect some markup like this:

<div class="my-div">
  <button data-video-file="path-to-video-file">
     <svg…>
       <use…/>
     </svg>
  </button>
</div>

A click on the button should be caught, and if the content gets paginated via ajax, I do not want to setup any listeners again, a case for event delegation.

jQuery

$(document).on('click', '[data-video-file]', playVideo);

Every click on the document runs against the [data-video-file] selector and is only handed over to the playVideo function, if the event occurs on the direct matching element or a child.

The genious part of jQuery is the extending of your event object with a .currentTarget. This property stores your delegated element.
If you click the <button> event.target equals to event.currentTarget'. If you hit the , event.targetis thesvgbutevent.currentTargetsticks to thebutton`.

Your event handler might look like this:

function playVideo(event) {
  const $element = $(event.currentTarget);
  …
}

That is not that much code…

Plain Js

document.addEventListener('click', playVideo, true);

This looks very similar, but as soon as you start clicking anywhere in your page, the 'playVideo` function is called.

That's fine, you need to match your event.target against your selector by hand:

 function playVideo(event) {
   const e = event || window.event;
   const element = e.target || e.srcElement;
   …
   if (element.matches('[data-video-file']')) {
     …
   } else {
     // ignore this click…
   }
 }

At first glance it looks like we are done. But we added a super shiny svg icon to the button and our const element is becoming a <svg> or <use> and our click handler will ignore it.

Remember: jQuery gives you the real target, maybe a <svg>, and the currentTarget which is the element we delegated the click handler on.

So… We need to make one or more steps further into the DOM and have a look if our selector is a parent of our target.

Without supporting IE11 and Edge 14, you could use the closest function, to look for matching parents within the DOM - but we are in dependency free ie11 support land and here we go:

 function matchPath(element, selector) {
   if (element) {
     if (element.matches(selector)) {
       return true;
     }
     return matchPath(element.parentElement, selector);
   }
   return false;
 }

matchPath executed in console

This function runs until it finds your DOM-root or a matching element or both.

Tipp: The tree structure of a DOM is a nice playground for self-calling functions - this technique is called recursion.

Pretty cool - and than you remember, that you need the element you delegate the click on: jQuery's .currentTarget:

function matchPath(element, selector) {
  if (element) {
    if (element.matches(selector)) {
      return element;
    }
    return matchPath(element.parentElement, selector);
  }
  return null;
}

Great - now we have jQuery's brilliancy cloned and the click handler looks like this:

function playVideo(event) {
  const e = event || window.event;
  const element = matchPath(e.target || e.srcElement, videoSelector);
  const wrapper = !!element ? element.parentElement : null;
  let video;
  
  if (element) {…

Just a quick look at IE11 and you will notice a problem with .matches(). Thanks to MDN there is a polyfill you need to include, but no need to alter your code.

Summary

I tried to show some of the work jQuery is doing for you, which enables you to use rich features on an easy way.

Without jQuery you need Event Normalization, matches() Polyfill and more browser testing, but your code will download and executed faster with a smaller memory footprint.

As I said at the beginning, using a library can be a mind-enabler, that gets you using techniques and understanding you might have missed otherwise.