Skip to content

Instantly share code, notes, and snippets.

@Robitx
Forked from WebReflection/handle-event-doodle.md
Created August 8, 2021 19:14
Show Gist options
  • Save Robitx/28cbdbd88dfea25fe91d08b59a9b2409 to your computer and use it in GitHub Desktop.
Save Robitx/28cbdbd88dfea25fe91d08b59a9b2409 to your computer and use it in GitHub Desktop.
The `handleEvent` ASCII doodle

About This Gist

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.

The handleEvent ASCII Doodle

                  ┌---------------------------------┐
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 can 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 Features

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 via type and the handler
  • it's usable in all my FE libraries: you just pass onclick=${this}, as example, and that's it

Live Test

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);

handleEvent & Hooks

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>
  `);
});

handleEvent & React

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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment