This gist summarizes the handleEvent(event)
pattern features, something standard, something described by me many times, written in my tiny book, tweeted about, and yet it's something most Web developers ignore.
┌---------------------------------┐
var handler = { | any object that inherits or |
| directly implements this method |
| can be used as event listener |
┌---------┴---------------------------------┘
▼ ┌----------------------------┐
handleEvent({ | the event is a regular one |
type, ◀--------------------┤ with type, preventDefault()|
| and all other event things |
└----------------------------┘
┌----------------------------┐
| like every other event, it |
currentTarget ◀------------┤ contains the node used to |
| add the event listener |
└----------------------------┘
}) { ┌----------------------------┐
| the method is called via |
this === handler; ◀--------┤ handler.handleEvent(event) |
| so the context is handler |
└----------------------------┘
} ┌-------------------------------------------┐
}: | the node will be the currentTarget, |
| which could be different from the target, |
| 'cause the target is the clicked element, |
| as example, while currentTarget is always |
| where the listener was attached to. |
┌---------┴-------------------------------------------┘
▼
node.addEventListener('click', handler);
┌---------------------------------------------┐
| like it is with exact same functions, |
| it doesn't matter if you add multiple time |
| exact same handler, as this will be ignored |
└--------------------------------┬------------┘
▼
node.addEventListener('click', handler);
The nitty-gritty of this pattern is that:
- you don't need arrow functions or
.bind(...)
- it works since year 2000 in every browser (20+ years rock solid)
- it's much better in terms of memory usage, compared to bound methods or trapped arrows
- it prevents the classic arrow or
.bind(...)
footgun: you can always remove a listener viatype
and the handler - it's usable in all my FE libraries: you just pass
onclick=${this}
, as example, and that's it
var handler = {
handleEvent({type, currentTarget}) {
console.log(type, this === handler);
currentTarget.removeEventListener(type, this);
}
};
document.addEventListener('click', handler);
// try to add it twice or more, only one click happens
document.addEventListener('click', handler);
// you can always remove the listener too
// document.removeEventListener('click', handler);
When hooked functions are used as listeners, the library needs to remove the previous listener and add the updated one.
This can be expensive and slow, as it touches the DOM per each update. However, libraries like lighterhtml, as example, won't touch the DOM if the listener is the same as it was before, so that hooks can be assigned right away to a JS object, instead of passing through DOM opertaions.
The following counter example is live in CodePen.
import {render, html} from 'lighterhtml';
import {define, useRef, useState} from 'hooked-elements';
const useClickHandler = ([count, update]) => {
const {current} = useRef({
handleEvent({currentTarget}) {
this[currentTarget.dataset.action]();
}
});
// attach anything to this object (it's fast and cheap)
// instead of adding/removing listeners each time (it's slow)
current.value = count;
current.dec = () => update(count - 1);
current.inc = () => update(count + 1);
return current;
};
define("my-counter", element => {
const handler = useClickHandler(useState(0));
render(element, html`
<button class="large btn" data-action=dec onclick=${handler}>-</button>
<span class="large span">${handler.value}</span>
<button class="large btn" data-action=inc onclick=${handler}>+</button>
`);
});
Following a React based example with useEffect
too.
import React, { useRef, useEffect, useState } from "react";
export default function Box() {
const [focused, setFocused] = useState(false);
const { current: clickRef } = useRef({ handleEvent });
clickRef.setFocused = setFocused;
useEffect(() => {
document.addEventListener("click", clickRef);
return () => {
document.removeEventListener("click", clickRef);
};
}, []);
return (
<div
style={{
backgroundColor: focused ? "green" : "red",
height: "50px",
width: "50px"
}}
ref={clickRef}
/>
);
}
function handleEvent({ target }) {
const isFocused = this.current.contains(target);
this.setFocused(isFocused);
}
The hookedElements equivalent would be written as such:
import {define, useRef, useEffect, useState} from 'hooked-elements';
define(".box", element => {
const [focused, setFocused] = useState(false);
const { current: clickRef } = useRef({ handleEvent });
useEffect(() => {
document.addEventListener("click", clickRef);
return () => {
document.removeEventListener("click", clickRef);
};
}, []);
clickRef.current = element;
clickRef.setFocused = setFocused;
element.style.backgroundColor = focused ? "green" : "red";
});
function handleEvent({ target }) {
const isFocused = this.current.contains(target);
this.setFocused(isFocused);
}
FYI: MDN used to document the
EventListener
interface but that page is now gone.handleEvent()
is only mentioned in passing inEventTarget.addEventListener()