mirror of
				https://github.com/jaandrle/deka-dom-el
				synced 2025-11-03 22:59:16 +01:00 
			
		
		
		
	🔤 events
This commit is contained in:
		@@ -17,8 +17,14 @@ export function mnemonic(){
 | 
			
		||||
			" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>]))")
 | 
			
		||||
		),
 | 
			
		||||
		el("li").append(
 | 
			
		||||
			el("code", "dispatchEvent(<event>[, <options>])(element, detail)"),
 | 
			
		||||
			" — just ", el("code", "<element>.dispatchEvent(new CustomEvent(<event>, { detail, ...<options> }))")
 | 
			
		||||
			el("code", "dispatchEvent(<event>, <element>)([<detail>])"),
 | 
			
		||||
			" — 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> }))")
 | 
			
		||||
		),
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { T, t } from "./utils/index.js";
 | 
			
		||||
export const info= {
 | 
			
		||||
	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";
 | 
			
		||||
@@ -15,7 +15,7 @@ const fileURL= url=> new URL(url, import.meta.url);
 | 
			
		||||
const references= {
 | 
			
		||||
	/** element.addEventListener() */
 | 
			
		||||
	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",
 | 
			
		||||
	},
 | 
			
		||||
	/** AbortSignal+element.addEventListener */
 | 
			
		||||
@@ -45,108 +45,205 @@ const references= {
 | 
			
		||||
export function page({ pkg, info }){
 | 
			
		||||
	const page_id= info.id;
 | 
			
		||||
	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`
 | 
			
		||||
			We quickly introduce helper to listening to the native DOM events. And library syntax/pattern so-called
 | 
			
		||||
			${el("em", t`Addon`)} to incorporate not only this in UI templates declaratively.
 | 
			
		||||
			Events are at the core of interactive web applications. DDE provides a clean, declarative approach to
 | 
			
		||||
			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(h3, t`Events and listenners`),
 | 
			
		||||
		el(h3, t`Events and Listeners: Two Approaches`),
 | 
			
		||||
		el("p").append(...T`
 | 
			
		||||
			In JavaScript you can listen to the native DOM events of the given element by using
 | 
			
		||||
			${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:
 | 
			
		||||
			In JavaScript you can listen to native DOM events using
 | 
			
		||||
			${el("a", references.mdn_listen).append(el("code", "element.addEventListener(type, listener, options)"))}.
 | 
			
		||||
			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("p").append(...T`
 | 
			
		||||
			…this is actually one of the two differences. The another one is that ${el("code", "on")} accepts only
 | 
			
		||||
			object as the ${el("code", "options")} (but it is still optional).
 | 
			
		||||
			The main benefit of DDE's approach is that it works as an Addon, making it easy to integrate
 | 
			
		||||
			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
 | 
			
		||||
			listener declaratively using ${el("a", { textContent: "AbortSignal", ...references.mdn_abortListener })}:
 | 
			
		||||
		`),
 | 
			
		||||
		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(h3, t`Removing Event Listeners`),
 | 
			
		||||
		el("div", { className: "note" }).append(
 | 
			
		||||
			el("p").append(...T`
 | 
			
		||||
				In the first example we force to use HTML attribute (it corresponds to
 | 
			
		||||
				${el("code", `<button onclick="console.log(event)">click me</button>`)}). ${el("em", t`Side note:
 | 
			
		||||
				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 })}.
 | 
			
		||||
				Unlike the native addEventListener/removeEventListener pattern, DDE uses the ${el("a", {
 | 
			
		||||
				textContent: "AbortSignal", ...references.mdn_abortListener })} for declarative approach for removal:
 | 
			
		||||
			`)
 | 
			
		||||
		),
 | 
			
		||||
		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`
 | 
			
		||||
			From practical point of view, ${el("em", t`Addons`)} are just functions that accept any HTML element as
 | 
			
		||||
			their first parameter. You can see that the ${el("code", "on(…)")} fullfills this requirement.
 | 
			
		||||
			For a deeper comparison of these approaches, see
 | 
			
		||||
			${el("a", { textContent: "WebReflection's detailed analysis", ...references.web_events })}.
 | 
			
		||||
		`),
 | 
			
		||||
 | 
			
		||||
		el(h3, t`Understanding Addons`),
 | 
			
		||||
		el("p").append(...T`
 | 
			
		||||
			You can use Addons as ≥3rd argument of ${el("code", "el")} function. This way is possible to extends your
 | 
			
		||||
			templates by additional (3rd party) functionalities. But for now mainly, you can add events listeners:
 | 
			
		||||
			Addons are a powerful pattern in DDE that extends beyond just event handling.
 | 
			
		||||
			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("p").append(...T`
 | 
			
		||||
			As the example shows, you can also provide types in JSDoc+TypeScript by using global type
 | 
			
		||||
			${el("code", "ddeElementAddon")}. Also notice, you can use Addons to get element reference.
 | 
			
		||||
			As the example shows, you can provide types in JSDoc+TypeScript using the global type
 | 
			
		||||
			${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`
 | 
			
		||||
			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.
 | 
			
		||||
			Addons are called immediately when an element is created, even before it's connected to the live DOM.
 | 
			
		||||
			You can think of an Addon as an "oncreate" event handler.
 | 
			
		||||
		`),
 | 
			
		||||
		el("p").append(...T`
 | 
			
		||||
			The library provide three additional live-cycle events corresponding to how they are named in a case of
 | 
			
		||||
			custom elements: ${el("code", "on.connected")}, ${el("code", "on.disconnected")} and ${el("code",
 | 
			
		||||
			"on.attributeChanged")}.
 | 
			
		||||
			DDE provides three additional lifecycle events that correspond to custom element lifecycle callbacks:
 | 
			
		||||
		`),
 | 
			
		||||
		el(example, { src: fileURL("./components/examples/events/live-cycle.js"), page_id }),
 | 
			
		||||
		el("p").append(...T`
 | 
			
		||||
			For Custom elements, we will later introduce a way to replace ${el("code", "*Callback")} syntax with
 | 
			
		||||
			${el("code", "dde:*")} events. The ${el("code", "on.*")} functions then listen to the appropriate
 | 
			
		||||
			Custom Elements events (see ${el("a", { textContent: t`Custom element lifecycle callbacks | MDN`,
 | 
			
		||||
			...references.mdn_customElement })}).
 | 
			
		||||
		`),
 | 
			
		||||
		el("p").append(...T`
 | 
			
		||||
			But, in case of regular elemnets the ${el("a", references.mdn_mutation).append(el("code",
 | 
			
		||||
			"MutationObserver"), " | MDN")} is internaly used to track these events. Therefore, there are some
 | 
			
		||||
			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("div", { className: "function-table" }).append(
 | 
			
		||||
			el("dl").append(
 | 
			
		||||
				el("dt", t`on.connected(callback)`),
 | 
			
		||||
				el("dd", t`Fires when the element is added to the DOM`),
 | 
			
		||||
 | 
			
		||||
				el("dt", t`on.disconnected(callback)`),
 | 
			
		||||
				el("dd", t`Fires when the element is removed from the DOM`),
 | 
			
		||||
 | 
			
		||||
				el("dt", t`on.attributeChanged(callback, attributeName)`),
 | 
			
		||||
				el("dd", t`Fires when the specified attribute changes`)
 | 
			
		||||
			)
 | 
			
		||||
		),
 | 
			
		||||
		el("p").append(...T`
 | 
			
		||||
			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("code", "on.disconnected")} are called only once and only when the element, is (dis)connected to
 | 
			
		||||
			live DOM. The solution is inspired by ${el("a", { textContent: "Vue", ...references.vue_fix })}. For using
 | 
			
		||||
			native behaviour re-(dis)connecting element, use:
 | 
			
		||||
		`),
 | 
			
		||||
		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(example, { src: fileURL("./components/examples/events/live-cycle.js"), page_id }),
 | 
			
		||||
 | 
			
		||||
		el("div", { className: "note" }).append(
 | 
			
		||||
			el("p").append(...T`
 | 
			
		||||
				For regular elements (non-custom elements), DDE uses
 | 
			
		||||
				${el("a", references.mdn_mutation).append(el("code", "MutationObserver"), " | MDN")}
 | 
			
		||||
				internally to track lifecycle events.
 | 
			
		||||
			`)
 | 
			
		||||
		),
 | 
			
		||||
 | 
			
		||||
		el(h3, t`Final notes`),
 | 
			
		||||
		el("p", t`The library also provides a method to dispatch the events.`),
 | 
			
		||||
		el("div", { className: "warning" }).append(
 | 
			
		||||
			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("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)
 | 
			
		||||
	);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user