`deka-dom-el` — Events and Addons

Using not only events in UI declaratively.

Listenning to the native DOM events and other Addons

We quickly introduce helper to listening to the native DOM events. And library syntax/pattern so-called Addon to incorporate not only this in UI templates declaratively.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm.js import { on, dispatchEvent } from "deka-dom-el"; /** @type {ddeElementAddon} */

# Events and listenners

In JavaScript you can listen to the native DOM events of the given element by using element.addEventListener(type, listener, options). The library provides an alternative (on) accepting the differen order of the arguments:

import { el, on } from "./esm-with-signals.js"; const log= mark=> console.log.bind(console, mark); const button= el("button", "Test click"); button.addEventListener("click", log("`addEventListener`"), { once: true }); on("click", log("`on`"), { once: true })(button); document.body.append( button );

…this is actually one of the two differences. The another one is that on accepts only object as the options (but it is still optional).

The other difference is that there is no off function. You can remove listener declaratively using AbortSignal:

import { el, on } from "./esm-with-signals.js"; const log= mark=> console.log.bind(console, mark); const abort_controller= new AbortController(); const { signal }= abort_controller; const button= el("button", "Test click"); button.addEventListener("click", log("`addEventListener`"), { signal }); on("click", log("`on`"), { signal })(button); document.body.append( button, " ", el("button", { textContent: "Off", onclick: ()=> abort_controller.abort() }) );

So, there are (typically) three ways to handle events. You can use:

  • el("button", { textContent: "click me", "=onclick": "console.log(event)" })
  • el("button", { textContent: "click me", onclick: console.log })
  • el("button", { textContent: "click me" }, on("click", console.log))

In the first example we force to use HTML attribute (it corresponds to <button onclick="console.log(event)">click me</button>). Side note: this can be useful in case of SSR. To study difference, you can read a nice summary here: GIST @WebReflection/web_events.md.

# Addons

From practical point of view, Addons are just functions that accept any HTML element as their first parameter. You can see that the on(…) fullfills this requirement.

You can use Addons as ≥3rd argument of el function. This way is possible to extends your templates by additional (3rd party) functionalities. But for now mainly, you can add events listeners:

import { el, on } from "./esm-with-signals.js"; const abort_controller= new AbortController(); const { signal }= abort_controller; /** @type {ddeElementAddon<HTMLButtonElement>} */ const onclickOff= on("click", ()=> abort_controller.abort(), { signal }); /** @type {(ref?: HTMLOutputElement)=> HTMLOutputElement | null} */ const ref= (store=> ref=> ref ? (store= ref) : store)(null); document.body.append( el("button", "Test `on`", on("click", console.log, { signal }), on("click", update, { signal }), on("mouseup", update, { signal })), " ", el("button", "Off", onclickOff), el("output", { style: { display: "block", whiteSpace: "pre" } }, ref) ); /** @param {MouseEvent} event @this {HTMLButtonElement} */ function update(event){ ref().append( event.type, "\n" ); }

As the example shows, you can also provide types in JSDoc+TypeScript by using global type ddeElementAddon. Also notice, you can use Addons to get element reference.

# Life-cycle events

Addons are called immediately when the element is created, even it is not connected to live DOM yet. Therefore, you can understand the Addon to be “oncreate” event.

The library provide three additional live-cycle events corresponding to how they are named in a case of custom elements: on.connected, on.disconnected and on.attributeChanged.

import { el, on } from "./esm-with-signals.js"; const paragraph= el("p", "See live-cycle events in console.", el=> log({ type: "dde:created", detail: el }), on.connected(log), on.disconnected(log), on.attributeChanged(log)); document.body.append( paragraph, el("button", "Update attribute", on("click", ()=> paragraph.setAttribute("test", Math.random().toString()))), " ", el("button", "Remove", on("click", ()=> paragraph.remove())) ); /** @param {Partial<CustomEvent>} event */ function log({ type, detail }){ console.log({ _this: this, type, detail }); }

For Custom elements, we will later introduce a way to replace *Callback syntax with dde:* events. The on.* functions then listen to the appropriate Custom Elements events (see Custom element lifecycle callbacks | MDN).

But, in case of regular elemnets the MutationObserver | MDN is internaly used to track these events. Therefore, there are some drawbacks:

To provide intuitive behaviour, similar also to how the life-cycle events works in other frameworks/libraries, deka-dom-el library ensures that on.connected and on.disconnected are called only once and only when the element, is (dis)connected to live DOM. The solution is inspired by Vue. For using native behaviour re-(dis)connecting element, use:

# Final notes

The library also provides a method to dispatch the events.

import { el, on, dispatchEvent } from "./esm-with-signals.js"; document.body.append( el("p", "Listenning to `test` event.", on("test", console.log)).append( el("br"), el("button", "native", on("click", native)), " ", el("button", "dde", on("click", dde)), " ", el("button", "dde with options", on("click", ddeOptions)) ) ); function native(){ this.dispatchEvent( new CustomEvent("test", { bubbles: true, detail: "hi" } ) ); } function dde(){ dispatchEvent("test")(this.parentElement, "hi"); } function ddeOptions(){ dispatchEvent("test", { bubbles: true })(this, "hi"); }

# Mnemonic

  • on(<event>, <listener>[, <options>])(<element>) — just <element>.addEventListener(<event>, <listener>[, <options>])
  • on.<live-cycle>(<event>, <listener>[, <options>])(<element>) — corresponds to custom elemnets callbacks <live-cycle>Callback(...){...}. To connect to custom element see following page, else it is simulated by MutationObserver.
  • dispatchEvent(<event>[, <options>])(element) — just <element>.dispatchEvent(new Event(<event>[, <options>]))
  • dispatchEvent(<event>[, <options>])(element, detail) — just <element>.dispatchEvent(new CustomEvent(<event>, { detail, ...<options> }))