Event Delegation

Written by Hamilton Cline
Last updated on April 3, 2019, 12:40 pm

Delegating Events


There's a movement on the internet against using jQuery. I don't necessarily cotton to that movement, but I can understand it. Some things jQuery does are worth using it for. And some things vanilla javascript can do now mean you don't really need jQuery for most projects, or you're already using a javascript framework for your project that supplants jQuery.

For arguments sake, let's say that for right now, you don't want to or don't need to use jQuery. That's cool. One of the things jQuery does really well is event delegation. That concept is not built into vanilla javascript, and designing it is no simple task. But for kicks and giggles, let's design event delegation.

Normally events work like this in jQuery.

$(".some-element") .on("click", e=>{ /* do something */ });

A problem occurs if that element doesn't exist yet. If you're dynamically generating content, then when that code runs nothing will exist to receive the event. So you can delegate an event to a parent element like this. You can even add multiple event handlers.

$(document) .on("click mousedown", ".some-element", e=>{ /* do something */ });

Now, it turns out, making something that does that in vanilla javascript is not super straight forward. So here we go.

Our own Query

Let's start by making our own selector tool. Something small like jQuery's $, but let's not use the $ symbol, since browsers also use that.

const isDom = d => d instanceof HTMLElement || d instanceof HTMLDocument; const q = (s,sc=document) => !s || !sc || !isDom(sc) ? [] : isDom(s) ? [s] : Array.isArray(s) ? s : sc.querySelectorAll(s);

The first function here checks to see if a parameter is some sort of html element. The q function will let us simplify our code in the future, and will also consolidate certain concepts of document querying. Our q function will always return an array, either an empty one, or one filled with html elements.

Multiple Event Handler

Let's get to the task of making multiple event handling now.

const qon = elementSelector => { const d = q(elementSelector); return (eventString,fn) => eventString.trim().split(/\s+/) .forEach(e => d.forEach(o => o.addEventListener(e,fn))); }

We're going to curry a function, instead of creating chained objects. We pass in a selector, and that's queried, and then a new function is passed back. This function trims the event string, and then splits it using space characters. That array of strings is looped, and each is used to create a different event listener on the selected element.

How does one use this?

qon(".some-element") ("click mousedown", e=>{ /* some code */ }); // Or const someElementOn = qon(".some-element"); someElementOn("mousedown", e=>{ /* some code */ }); someElementOn("click", e=>{ /* some code */ });

Not bad, very similar to the jQuery implementation. Ok, so we can handle multiple events now. The next step is to make something that can handle the idea of delegating an event to a parent element.

But first, it's important to note that not all browsers handle the idea of event paths the exact same way, and some of them don't do it at all. Super frustrating. One of the reasons that jQuery existed in the first place. So if anyone ever tells you there's no need for jQuery, as of this writing, they're wrong.

Path Handling

const getPath = e => e.path || (e.composedPath && e.composedPath()); const inPath = (ev,to) => getPath(ev).some(o => o == to);

These two functions will give us the ability to get the parent path of our element that was evented, and then see if our element is inside the path of the parent.

Event Delegation

const qdelegate = elementSelector => { const d = qon(elementSelector); return (eventString,elementSelector,fn) => d(eventString, e => q(elementSelector) .forEach(to => inPath(e,to)?fn.call(to,e,to):0)); }

Ok... ummmm... So... Well things have gotten a bit complicated. We're currying functions in here. We're calling curried functions inside our curried functions. We're making atomic functions. We're looping loops inside our loops. At a certain level you can break this down further if you want, but for right now, just accept that we need to check to see if the delegate was clicked, to see if the element we want was clicked, to see if the element we want was inside the delegate, and then to call our callback with the scope of our wanted element. It's a lot.

So how do we use this?

qdelegate(document) ("click mousedown", ".some-element", e=>{ /* some code */ }); // Or const delegated = qdelegate("body"); delegated("mousedown mouseup", ".some-element", e=>{ /* some code */ }); delegated("click", ".other-element", e=>{ /* some code */ });

Here's all of it together.

All Together

const isDom = d => d instanceof HTMLElement || d instanceof HTMLDocument; const getPath = e => e.path || (e.composedPath && e.composedPath()); const inPath = (e,to) => getPath(e).some(o=>o==to); const q = (s,sc=document) => !s || !sc || !isDom(sc) ? [] : isDom(s) ? [s] : Array.isArray(s) ? s : sc.querySelectorAll(s); const qon = sc => { const d = q(sc); const f = (es,fn)=>{ es.trim().split(/\s+/).forEach(e=> d.forEach(o=> o.addEventListener(e,fn))); return f; } return f; } const qdelegate = sc => { const d = qon(sc); const f = (es,sl,fn) => { d(es,ev=>q(sl).forEach(to=> inPath(e,to)?fn.call(to,e,to):0)); return f; } return f; }

These are simplified in text, but also allow each instance of our function to return back a new curried instance of the secondary function, allowing us to chain function calls, much like jQuery but as calls, instead of as methods.

To be 100% clear. Is this necessary? Not if you have jQuery already. Is jQuery necessary? Most likely no, depending on your needs. Is jQuery cool? Yup, I love it. If I only need this, do I need jQuery? Well now that you have this, the answer might be no. This bit of code can replace a large chunk of the reason I have stayed with jQuery for so long. So if you have jQuery, keep using it, it's great. If you don't have it or don't want it, this code can help you decouple.

Written by Hamilton Cline
Last updated on April 3, 2019, 12:40 pm