1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-07-01 04:12:14 +02:00

🔤 events

This commit is contained in:
2025-03-05 14:39:28 +01:00
parent 1c5f0dab5e
commit 2a3b6dc5cd
2 changed files with 180 additions and 77 deletions

View File

@ -17,8 +17,14 @@ export function mnemonic(){
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>]))") " — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>]))")
), ),
el("li").append( el("li").append(
el("code", "dispatchEvent(<event>[, <options>])(element, detail)"), el("code", "dispatchEvent(<event>, <element>)([<detail>])"),
" — just ", el("code", "<element>.dispatchEvent(new CustomEvent(<event>, { detail, ...<options> }))") " — just ", el("code", "<element>.dispatchEvent(new Event(<event>))"), " or ",
el("code", "<element>.dispatchEvent(new CustomEvent(<event>, { detail: <detail> }))")
),
el("li").append(
el("code", "dispatchEvent(<event>[, <options>])(<element>[, <detail>])"),
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>] ))"), " or ",
el("code", "<element>.dispatchEvent(new CustomEvent(<event>, { detail: <detail> }))")
), ),
); );
} }

View File

@ -1,7 +1,7 @@
import { T, t } from "./utils/index.js"; import { T, t } from "./utils/index.js";
export const info= { export const info= {
title: t`Events and Addons`, title: t`Events and Addons`,
description: t`Using not only events in UI declaratively.`, description: t`Using events and addons for declarative UI interactions.`,
}; };
import { el } from "deka-dom-el"; import { el } from "deka-dom-el";
@ -15,7 +15,7 @@ const fileURL= url=> new URL(url, import.meta.url);
const references= { const references= {
/** element.addEventListener() */ /** element.addEventListener() */
mdn_listen: { mdn_listen: {
title: t`MDN documentation page for elemetn.addEventListener`, title: t`MDN documentation page for element.addEventListener`,
href: "https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener", href: "https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener",
}, },
/** AbortSignal+element.addEventListener */ /** AbortSignal+element.addEventListener */
@ -45,108 +45,205 @@ const references= {
export function page({ pkg, info }){ export function page({ pkg, info }){
const page_id= info.id; const page_id= info.id;
return el(simplePage, { info, pkg }).append( return el(simplePage, { info, pkg }).append(
el("h2", t`Listenning to the native DOM events and other Addons`), el("h2", t`Declarative Event Handling and Addons`),
el("p").append(...T` el("p").append(...T`
We quickly introduce helper to listening to the native DOM events. And library syntax/pattern so-called Events are at the core of interactive web applications. DDE provides a clean, declarative approach to
${el("em", t`Addon`)} to incorporate not only this in UI templates declaratively. handling DOM events and extends this pattern with a powerful Addon system to incorporate additional
functionalities into your UI templates.
`), `),
el("div", { className: "callout" }).append(
el("h4", t`Why DDE's Event System and Addons Matters`),
el("ul").append(
el("li", t`Integrate event handling directly in element declarations`),
el("li", t`Leverage lifecycle events for better component design`),
el("li", t`Clean up listeners automatically with abort signals`),
el("li", t`Extend elements with custom behaviors using Addons`),
el("li", t`Maintain clean, readable code with consistent patterns`)
)
),
el(code, { src: fileURL("./components/examples/events/intro.js"), page_id }), el(code, { src: fileURL("./components/examples/events/intro.js"), page_id }),
el(h3, t`Events and listenners`), el(h3, t`Events and Listeners: Two Approaches`),
el("p").append(...T` el("p").append(...T`
In JavaScript you can listen to the native DOM events of the given element by using In JavaScript you can listen to native DOM events using
${el("a", references.mdn_listen).append( el("code", "element.addEventListener(type, listener, options)") )}. ${el("a", references.mdn_listen).append(el("code", "element.addEventListener(type, listener, options)"))}.
The library provides an alternative (${el("code", "on")}) accepting the differen order of the arguments: DDE provides an alternative approach with arguments ordered differently to better fit its declarative style:
`), `),
el("div", { className: "illustration" }).append(
el("div", { className: "tabs" }).append(
el("div", { className: "tab" }).append(
el("h5", t`Native DOM API`),
el(code, { content: `element.addEventListener('click', callback, options);`, page_id })
),
el("div", { className: "tab" }).append(
el("h5", t`DDE Approach`),
el(code, { content: `on('click', callback, options)(element);`, page_id })
)
)
),
el(example, { src: fileURL("./components/examples/events/compare.js"), page_id }), el(example, { src: fileURL("./components/examples/events/compare.js"), page_id }),
el("p").append(...T` el("p").append(...T`
…this is actually one of the two differences. The another one is that ${el("code", "on")} accepts only The main benefit of DDE's approach is that it works as an Addon, making it easy to integrate
object as the ${el("code", "options")} (but it is still optional). directly into element declarations.
`), `),
el("p", { className: "notice" }).append(...T`
The other difference is that there is ${el("strong", "no")} ${el("code", "off")} function. You can remove el(h3, t`Removing Event Listeners`),
listener declaratively using ${el("a", { textContent: "AbortSignal", ...references.mdn_abortListener })}: el("div", { className: "note" }).append(
`),
el(example, { src: fileURL("./components/examples/events/abortSignal.js"), page_id }),
el("div", { className: "notice" }).append(
el("p", t`So, there are (typically) three ways to handle events. You can use:`),
el("ul").append(
el("li").append( el("code", `el("button", { textContent: "click me", "=onclick": "console.log(event)" })`)),
el("li").append( el("code", `el("button", { textContent: "click me", onclick: console.log })`)),
el("li").append( el("code", `el("button", { textContent: "click me" }, on("click", console.log))`))
),
el("p").append(...T` el("p").append(...T`
In the first example we force to use HTML attribute (it corresponds to Unlike the native addEventListener/removeEventListener pattern, DDE uses the ${el("a", {
${el("code", `<button onclick="console.log(event)">click me</button>`)}). ${el("em", t`Side note: textContent: "AbortSignal", ...references.mdn_abortListener })} for declarative approach for removal:
this can be useful in case of SSR.`)} To study difference, you can read a nice summary here:
${el("a", { textContent: "GIST @WebReflection/web_events.md", ...references.web_events })}.
`) `)
), ),
el(example, { src: fileURL("./components/examples/events/abortSignal.js"), page_id }),
el(h3, t`Addons`), el(h3, t`Three Ways to Handle Events`),
el("div", { className: "tabs" }).append(
el("div", { className: "tab", "data-tab": "html-attr" }).append(
el("h4", t`HTML Attribute Style`),
el(code, { content: `el("button", {
textContent: "click me",
"=onclick": "console.log(event)"
})`, page_id }),
el("p").append(...T`
Forces usage as an HTML attribute. Corresponds to
${el("code", `<button onclick="console.log(event)">click me</button>`)}. This can be particularly
useful for SSR scenarios.
`)
),
el("div", { className: "tab", "data-tab": "property" }).append(
el("h4", t`Property Assignment`),
el(code, { content: `el("button", {
textContent: "click me",
onclick: console.log
})`, page_id }),
el("p", t`Assigns the event handler directly to the element's property.`)
),
el("div", { className: "tab", "data-tab": "addon" }).append(
el("h4", t`Addon Approach`),
el(code, { content: `el("button", {
textContent: "click me"
}, on("click", console.log))`, page_id }),
el("p", t`Uses the addon pattern, see above.`)
)
),
el("p").append(...T` el("p").append(...T`
From practical point of view, ${el("em", t`Addons`)} are just functions that accept any HTML element as For a deeper comparison of these approaches, see
their first parameter. You can see that the ${el("code", "on(…)")} fullfills this requirement. ${el("a", { textContent: "WebReflection's detailed analysis", ...references.web_events })}.
`), `),
el(h3, t`Understanding Addons`),
el("p").append(...T` el("p").append(...T`
You can use Addons as ≥3rd argument of ${el("code", "el")} function. This way is possible to extends your Addons are a powerful pattern in DDE that extends beyond just event handling.
templates by additional (3rd party) functionalities. But for now mainly, you can add events listeners: An Addon is any function that accepts an HTML element as its first parameter.
`),
el("div", { className: "callout" }).append(
el("h4", t`What Can Addons Do?`),
el("ul").append(
el("li", t`Add event listeners to elements`),
el("li", t`Set up lifecycle behaviors`),
el("li", t`Integrate third-party libraries`),
el("li", t`Create reusable element behaviors`),
el("li", t`Capture element references`)
)
),
el("p").append(...T`
You can use Addons as ≥3rd argument of the ${el("code", "el")} function, making it possible to
extend your templates with additional functionality:
`), `),
el(example, { src: fileURL("./components/examples/events/templateWithListeners.js"), page_id }), el(example, { src: fileURL("./components/examples/events/templateWithListeners.js"), page_id }),
el("p").append(...T` el("p").append(...T`
As the example shows, you can also provide types in JSDoc+TypeScript by using global type As the example shows, you can provide types in JSDoc+TypeScript using the global type
${el("code", "ddeElementAddon")}. Also notice, you can use Addons to get element reference. ${el("code", "ddeElementAddon")}. Notice how Addons can also be used to get element references.
`), `),
el(h3, t`Life-cycle events`),
el(h3, t`Lifecycle Events`),
el("p").append(...T` el("p").append(...T`
Addons are called immediately when the element is created, even it is not connected to live DOM yet. Addons are called immediately when an element is created, even before it's connected to the live DOM.
Therefore, you can understand the Addon to be “oncreate event. You can think of an Addon as an "oncreate" event handler.
`), `),
el("p").append(...T` el("p").append(...T`
The library provide three additional live-cycle events corresponding to how they are named in a case of DDE provides three additional lifecycle events that correspond to custom element lifecycle callbacks:
custom elements: ${el("code", "on.connected")}, ${el("code", "on.disconnected")} and ${el("code",
"on.attributeChanged")}.
`), `),
el(example, { src: fileURL("./components/examples/events/live-cycle.js"), page_id }), el("div", { className: "function-table" }).append(
el("p").append(...T` el("dl").append(
For Custom elements, we will later introduce a way to replace ${el("code", "*Callback")} syntax with el("dt", t`on.connected(callback)`),
${el("code", "dde:*")} events. The ${el("code", "on.*")} functions then listen to the appropriate el("dd", t`Fires when the element is added to the DOM`),
Custom Elements events (see ${el("a", { textContent: t`Custom element lifecycle callbacks | MDN`,
...references.mdn_customElement })}). el("dt", t`on.disconnected(callback)`),
`), el("dd", t`Fires when the element is removed from the DOM`),
el("p").append(...T`
But, in case of regular elemnets the ${el("a", references.mdn_mutation).append(el("code", el("dt", t`on.attributeChanged(callback, attributeName)`),
"MutationObserver"), " | MDN")} is internaly used to track these events. Therefore, there are some el("dd", t`Fires when the specified attribute changes`)
drawbacks: )
`),
el("ul").append(
el("li").append(...T`
To proper listener registration, you need to use ${el("code", "on.*")} not \`on("dde:*", …)\`!
`),
el("li").append(...T`
Use sparingly! Internally, library must loop of all registered events and fires event properly.
${el("strong", t`It is good practice to use the fact that if an element is removed, its children are
also removed!`)} In this spirit, we will introduce later the ${el("strong", t`host`)} syntax to
register, clean up procedures when the component is removed from the app.
`),
), ),
el("p").append(...T` el(example, { src: fileURL("./components/examples/events/live-cycle.js"), page_id }),
To provide intuitive behaviour, similar also to how the life-cycle events works in other
frameworks/libraries, deka-dom-el library ensures that ${el("code", "on.connected")} and el("div", { className: "note" }).append(
${el("code", "on.disconnected")} are called only once and only when the element, is (dis)connected to el("p").append(...T`
live DOM. The solution is inspired by ${el("a", { textContent: "Vue", ...references.vue_fix })}. For using For regular elements (non-custom elements), DDE uses
native behaviour re-(dis)connecting element, use: ${el("a", references.mdn_mutation).append(el("code", "MutationObserver"), " | MDN")}
`), internally to track lifecycle events.
el("ul").append( `)
el("li").append(...T`custom ${el("code", "MutationObserver")} or logic in (dis)${el("code",
"connectedCallback")} or…`),
el("li").append(...T`re-add ${el("code", "on.connected")} or ${el("code", "on.disconnected")} listeners.`)
), ),
el(h3, t`Final notes`), el("div", { className: "warning" }).append(
el("p", t`The library also provides a method to dispatch the events.`), el("ul").append(
el("li").append(...T`
Always use ${el("code", "on.*")} functions, not ${el("code", "on('dde:*', ...)")}, for proper registration
`),
el("li").append(...T`
Use lifecycle events sparingly, as they require internal tracking
`),
el("li").append(...T`
Leverage parent-child relationships: when a parent is removed, all children are also removed
`),
el("li").append(...T`
…see section later in documentation regarding hosts elements
`),
el("li").append(...T`
DDE ensures that connected/disconnected events fire only once for better predictability
`)
)
),
el(h3, t`Dispatching Custom Events`),
el(example, { src: fileURL("./components/examples/events/compareDispatch.js"), page_id }), el(example, { src: fileURL("./components/examples/events/compareDispatch.js"), page_id }),
el("p").append(...T`
This makes it easy to implement component communication through events,
following standard web platform patterns. The curried approach allows for easy reuse
of event dispatchers throughout your application.
`),
el(h3, t`Best Practices`),
el("ol").append(
el("li").append(...T`
${el("strong", "Clean up listeners")}: Use AbortSignal to prevent memory leaks
`),
el("li").append(...T`
${el("strong", "Leverage lifecycle events")}: For component setup and teardown
`),
el("li").append(...T`
${el("strong", "Delegate when possible")}: Add listeners to container elements when handling many similar elements
`),
el("li").append(...T`
${el("strong", "Maintain consistency")}: Choose one event binding approach and stick with it
`)
),
el("div", { className: "troubleshooting" }).append(
el("h4", t`Common Event Pitfalls`),
el("dl").append(
el("dt", t`Event listeners not working`),
el("dd", t`Ensure element is in the DOM before expecting events to fire`),
el("dt", t`Memory leaks`),
el("dd", t`Use AbortController to clean up listeners when elements are removed`),
el("dt", t`Lifecycle events firing unexpectedly`),
el("dd", t`Remember that on.connected and on.disconnected events only fire once per connection state`)
)
),
el(mnemonic) el(mnemonic)
); );