1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-04-01 11:45:53 +02:00

🔤 🐛 v0.9.1-alpha (#30)

* :tap: removed on.attributeChanged and static observedAttributes

*  import optimalization

*  scope.signal

* 🔤 🐛

*  🐛 registerReactivity and types

* 🔤

* 

* 🔤

* 🐛 Node in enviroment

*  todos

* 

*  🔤

*  lint

*  memo

* 🔤 🐛 memo

*  🔤 todomvc

* 🐛 types

* 🔤 p08 signal factory

* 🔤  types

*  🔤 lint

* 🔤

* 🔤

* 🔤

* 🔤

* 📺
This commit is contained in:
Jan Andrle 2025-03-12 18:37:42 +01:00 committed by GitHub
parent e1f321004d
commit 25d475ec04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 4899 additions and 2182 deletions

View File

@ -63,6 +63,7 @@ Creating reactive elements, components, and Web Components using the native
- ☑️ **Lightweight** — ~10-15kB minified (original goal 10kB) with zero/minimal dependencies
- ✅ **Declarative & functional approach** for clean, maintainable code
- ✅ **Signals and events** for reactive UI
- ✅ **Memoization for performance** — optimize rendering with intelligent caching
- ✅ **Optional build-in signals** with support for custom reactive implementations
- ✅ **Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom)
- ✅ **TypeScript support** (work in progress)
@ -122,3 +123,4 @@ Signals are the reactive backbone of Deka DOM Elements:
- [potch/signals](https://github.com/potch/signals) - A small reactive signals library
- [jaandrle/dollar_dom_component](https://github.com/jaandrle/dollar_dom_component) -
Functional DOM components without JSX/virtual DOM
- [mxjp/rvx: A signal based frontend framework](https://github.com/mxjp/rvx)

View File

@ -4,11 +4,13 @@ const files= [ "index", "index-with-signals" ];
$.api("")
.command("main", "Build main files", { default: true })
.action(async function main(){
.option("--no-types", "Also generate d.ts files", false)
.action(async function main({ types }){
const regular = await build({
files,
filesOut,
minify: "no",
types,
});
const min = await build({
files,
@ -18,6 +20,7 @@ $.api("")
return out.slice(0, idx)+".min"+out.slice(idx);
},
minify: "full",
types,
});
return $.exit(regular + min);
})

View File

@ -3,7 +3,7 @@ const css= echo.css`
.info{ color: gray; }
`;
export async function build({ files, filesOut, minify= "partial", iife= true }){
export function build({ files, filesOut, minify= "partial", iife= true, types= true }){
for(const file_root of files){
const file= file_root+".js";
echo(`Processing ${file} (minified: ${minify})`);
@ -11,20 +11,22 @@ export async function build({ files, filesOut, minify= "partial", iife= true }){
const esbuild_output= buildEsbuild({ file, out, minify });
echoVariant(esbuild_output.stderr.split("\n")[1].trim());
const file_dts= file_root+".d.ts";
const file_dts_out= filesOut(file_dts);
echoVariant(file_dts_out, true);
buildDts({
bundle: out,
entry: file_dts,
});
echoVariant(file_dts_out);
if(types){
const file_dts= file_root+".d.ts";
const file_dts_out= filesOut(file_dts);
echoVariant(file_dts_out, true);
buildDts({
bundle: out,
entry: file_dts,
});
echoVariant(file_dts_out);
}
if(iife) toIIFE(file, file_root);
if(iife) toIIFE(file, file_root, types);
}
return 0;
async function toIIFE(file, file_root){
function toIIFE(file, file_root, types){
const fileMark= "iife";
const name= "DDE";
const out= filesOut(file_root+".js", fileMark);
@ -36,6 +38,7 @@ export async function build({ files, filesOut, minify= "partial", iife= true }){
const dde_output= buildEsbuild({ file, out, minify, params });
echoVariant(`${out} (${name})`)
if(!types) return dde_output;
const file_dts= file_root+".d.ts";
const file_dts_out= filesOut(file_dts, fileMark);
echoVariant(file_dts_out, true);

View File

@ -1,5 +1,11 @@
#!/usr/bin/env bash
set -eou pipefail
npx editorconfig-checker -format gcc
# if $1=vim -no-color
one=${1:-''}
additional=''
[ "$one" = 'vim' ] && additional='-no-color'
npx editorconfig-checker -format gcc ${additional}
[ "$one" = 'vim' ] && additional='--reporter unix'
npx jshint index.js src ${additional}
[ "$one" = 'vim' ] && exit 0
npx size-limit
npx jshint index.js src

View File

@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean;
};
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal {
_: Symbol;
/**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li)));
* ```
* */
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
}
export const signal: signal;
@ -80,10 +81,11 @@ export type CustomElementTagNameMap = {
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global {
type ddeComponentAttributes = Record<any, any> | undefined;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
type ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>;
}
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = {
/**
@ -145,6 +147,7 @@ export namespace el {
host?: "this" | "parentElement";
}, is_open?: boolean): Comment;
}
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
export function el<A extends {
textContent: ddeStringable;
@ -157,7 +160,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
/** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
/**
@ -166,9 +168,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
* @param body Body of the custom element
* */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
@ -186,7 +188,7 @@ export interface On {
export const on: On;
export type Scope = {
scope: Node | Function | Object;
host: ddeElementAddon<any>;
host: Host<SupportedElement>;
custom_element: false | HTMLElement;
prevent: boolean;
};
@ -202,7 +204,11 @@ export const scope: {
* It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`.
* */
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
host: Host<SupportedElement>;
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
signal: AbortSignal;
state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
@ -214,7 +220,6 @@ export const scope: {
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
/**
* This is used primarly for server side rendering. To be sure that all async operations
* are finished before the page is sent to the client.
@ -235,6 +240,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
* ```
* */
export function queue(promise?: Promise<unknown>): Promise<unknown>;
/**
* Memoization utility for caching DOM elements to improve performance.
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
*
* @param key - Unique identifier for the element (usually an ID or unique value)
* @param generator - Function that creates the element
* @returns The cached element if the key exists, otherwise the result of the generator function
*
* @example
* ```ts
* // Within S.el for list rendering
* S.el(itemsSignal, (items, memo) =>
* el("ul").append(
* ...items.map(item =>
* memo(item.id, () => el(ItemComponent, item))
* )
* )
* )
* ```
*/
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
/**
* Memo namespace containing utility functions for memoization.
*/
export namespace memo {
/**
* Checks if an object is a memo scope.
* @param obj - The object to check
* @returns True if the object is a memo scope
*/
export function isScope(obj: any): boolean;
/**
* Creates a memoized function with optional cleanup support.
*
* @param fun - The function to memoize
* @param options - Configuration options
* @param options.signal - AbortSignal for cleanup
* @param options.onlyLast - When true, only keeps the cache from the most recent call
* @returns A memoized version of the function with a .clear() method
*
* @example
* ```ts
* const renderItems = memo.scope(function(items) {
* return items.map(item =>
* memo(item.id, () => el("div", item.name))
* );
* }, {
* signal: controller.signal,
* onlyLast: true
* });
* ```
*/
export function scope<F extends Function>(fun: F, options?: {
signal?: AbortSignal;
onlyLast?: boolean;
}): F & {
clear: () => void;
};
}
/* TypeScript MEH */
declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;

View File

@ -32,8 +32,8 @@ function onAbort(signal2, listener) {
};
}
function observedAttributes(instance, observedAttribute2) {
const { observedAttributes: observedAttributes3 = [] } = instance.constructor;
return observedAttributes3.reduce(function(out, name) {
const { observedAttributes: observedAttributes2 = [] } = instance.constructor;
return observedAttributes2.reduce(function(out, name) {
out[kebabToCamel(name)] = observedAttribute2(instance, name);
return out;
}, {});
@ -91,6 +91,7 @@ var enviroment = {
setDeleteAttr,
ssr: "",
D: globalThis.document,
N: globalThis.Node,
F: globalThis.DocumentFragment,
H: globalThis.HTMLElement,
S: globalThis.SVGElement,
@ -111,6 +112,215 @@ var evc = "dde:connected";
var evd = "dde:disconnected";
var eva = "dde:attributeChanged";
// src/events-observer.js
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get() {
return () => {
};
}
});
function connectionsChangesObserverConstructor() {
const store = /* @__PURE__ */ new Map();
let is_observing = false;
const observerListener = (stop2) => function(mutations) {
for (const mutation of mutations) {
if (mutation.type !== "childList") continue;
if (observerAdded(mutation.addedNodes, true)) {
stop2();
continue;
}
if (observerRemoved(mutation.removedNodes, true))
stop2();
}
};
const observer = new enviroment.M(observerListener(stop));
return {
/**
* Creates an observer for a specific element
* @param {Element} element - Element to observe
* @returns {Function} Cleanup function
*/
observe(element) {
const o = new enviroment.M(observerListener(() => {
}));
o.observe(element, { childList: true, subtree: true });
return () => o.disconnect();
},
/**
* Register a connection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for connection event
*/
onConnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.connected.has(listener)) return;
listeners.connected.add(listener);
listeners.length_c += 1;
},
/**
* Unregister a connection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offConnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
if (!ls.connected.has(listener)) return;
ls.connected.delete(listener);
ls.length_c -= 1;
cleanWhenOff(element, ls);
},
/**
* Register a disconnection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for disconnection event
*/
onDisconnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.disconnected.has(listener)) return;
listeners.disconnected.add(listener);
listeners.length_d += 1;
},
/**
* Unregister a disconnection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offDisconnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
ls.disconnected.delete(listener);
ls.length_d -= 1;
cleanWhenOff(element, ls);
}
};
function cleanWhenOff(element, ls) {
if (ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
function getElementStore(element) {
if (store.has(element)) return store.get(element);
const out = {
connected: /* @__PURE__ */ new WeakSet(),
length_c: 0,
disconnected: /* @__PURE__ */ new WeakSet(),
length_d: 0
};
store.set(element, out);
return out;
}
function start() {
if (is_observing) return;
is_observing = true;
observer.observe(enviroment.D.body, { childList: true, subtree: true });
}
function stop() {
if (!is_observing || store.size) return;
is_observing = false;
observer.disconnect();
}
function requestIdle() {
return new Promise(function(resolve) {
(requestIdleCallback || requestAnimationFrame)(resolve);
});
}
async function collectChildren(element) {
if (store.size > 30)
await requestIdle();
const out = [];
if (!isInstance(element, enviroment.N)) return out;
for (const el of store.keys()) {
if (el === element || !isInstance(el, enviroment.N)) continue;
if (element.contains(el))
out.push(el);
}
return out;
}
function observerAdded(addedNodes, is_root) {
let out = false;
for (const element of addedNodes) {
if (is_root) collectChildren(element).then(observerAdded);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_c) continue;
element.dispatchEvent(new Event(evc));
ls.connected = /* @__PURE__ */ new WeakSet();
ls.length_c = 0;
if (!ls.length_d) store.delete(element);
out = true;
}
return out;
}
function observerRemoved(removedNodes, is_root) {
let out = false;
for (const element of removedNodes) {
if (is_root) collectChildren(element).then(observerRemoved);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_d) continue;
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
out = true;
}
return out;
}
function dispatchRemove(element) {
return () => {
if (element.isConnected) return;
element.dispatchEvent(new Event(evd));
store.delete(element);
};
}
}
// src/events.js
function dispatchEvent(name, options, host) {
if (typeof options === "function") {
host = options;
options = null;
}
if (!options) options = {};
return function dispatch(element, ...d) {
if (host) {
d.unshift(element);
element = typeof host === "function" ? host() : host;
}
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
return element.dispatchEvent(event);
};
}
function on(event, listener, options) {
return function registerElement(element) {
element.addEventListener(event, listener, options);
return element;
};
}
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
on.connected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evc, listener, options);
if (element[keyLTE]) return element;
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
if (c) c_ch_o.onConnected(element, listener);
return element;
};
};
on.disconnected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evd, listener, options);
if (element[keyLTE]) return element;
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
if (c) c_ch_o.onDisconnected(element, listener);
return element;
};
};
// src/dom.js
function queue(promise) {
return enviroment.q(promise);
@ -122,6 +332,7 @@ var scopes = [{
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
prevent: true
}];
var store_abort = /* @__PURE__ */ new WeakMap();
var scope = {
/**
* Gets the current scope
@ -138,6 +349,17 @@ var scope = {
return this.current.host;
},
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
get signal() {
const { host } = this;
if (store_abort.has(host)) return store_abort.get(host);
const a = new AbortController();
store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a.signal;
},
/**
* Prevents default behavior in the current scope
* @returns {Object} Current scope context
*/
@ -380,172 +602,8 @@ function setDelete(obj, key, val) {
return Reflect.deleteProperty(obj, key);
}
// src/events-observer.js
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get() {
return () => {
};
}
});
function connectionsChangesObserverConstructor() {
const store = /* @__PURE__ */ new Map();
let is_observing = false;
const observerListener = (stop2) => function(mutations) {
for (const mutation of mutations) {
if (mutation.type !== "childList") continue;
if (observerAdded(mutation.addedNodes, true)) {
stop2();
continue;
}
if (observerRemoved(mutation.removedNodes, true))
stop2();
}
};
const observer = new enviroment.M(observerListener(stop));
return {
/**
* Creates an observer for a specific element
* @param {Element} element - Element to observe
* @returns {Function} Cleanup function
*/
observe(element) {
const o = new enviroment.M(observerListener(() => {
}));
o.observe(element, { childList: true, subtree: true });
return () => o.disconnect();
},
/**
* Register a connection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for connection event
*/
onConnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.connected.has(listener)) return;
listeners.connected.add(listener);
listeners.length_c += 1;
},
/**
* Unregister a connection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offConnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
if (!ls.connected.has(listener)) return;
ls.connected.delete(listener);
ls.length_c -= 1;
cleanWhenOff(element, ls);
},
/**
* Register a disconnection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for disconnection event
*/
onDisconnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.disconnected.has(listener)) return;
listeners.disconnected.add(listener);
listeners.length_d += 1;
},
/**
* Unregister a disconnection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offDisconnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
ls.disconnected.delete(listener);
ls.length_d -= 1;
cleanWhenOff(element, ls);
}
};
function cleanWhenOff(element, ls) {
if (ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
function getElementStore(element) {
if (store.has(element)) return store.get(element);
const out = {
connected: /* @__PURE__ */ new WeakSet(),
length_c: 0,
disconnected: /* @__PURE__ */ new WeakSet(),
length_d: 0
};
store.set(element, out);
return out;
}
function start() {
if (is_observing) return;
is_observing = true;
observer.observe(enviroment.D.body, { childList: true, subtree: true });
}
function stop() {
if (!is_observing || store.size) return;
is_observing = false;
observer.disconnect();
}
function requestIdle() {
return new Promise(function(resolve) {
(requestIdleCallback || requestAnimationFrame)(resolve);
});
}
async function collectChildren(element) {
if (store.size > 30)
await requestIdle();
const out = [];
if (!isInstance(element, Node)) return out;
for (const el of store.keys()) {
if (el === element || !isInstance(el, Node)) continue;
if (element.contains(el))
out.push(el);
}
return out;
}
function observerAdded(addedNodes, is_root) {
let out = false;
for (const element of addedNodes) {
if (is_root) collectChildren(element).then(observerAdded);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_c) continue;
element.dispatchEvent(new Event(evc));
ls.connected = /* @__PURE__ */ new WeakSet();
ls.length_c = 0;
if (!ls.length_d) store.delete(element);
out = true;
}
return out;
}
function observerRemoved(removedNodes, is_root) {
let out = false;
for (const element of removedNodes) {
if (is_root) collectChildren(element).then(observerRemoved);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_d) continue;
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
out = true;
}
return out;
}
function dispatchRemove(element) {
return () => {
if (element.isConnected) return;
element.dispatchEvent(new Event(evd));
store.delete(element);
};
}
}
// src/customElement.js
function customElementRender(target, render, props = observedAttributes2) {
function customElementRender(target, render, props = {}) {
const custom_element = target.host || target;
scope.push({
scope: custom_element,
@ -586,81 +644,40 @@ function wrapMethod(obj, method, apply) {
obj[method] = new Proxy(obj[method] || (() => {
}), { apply });
}
function observedAttributes2(instance) {
return observedAttributes(instance, (i, n) => i.getAttribute(n));
}
// src/events.js
function dispatchEvent(name, options, host) {
if (typeof options === "function") {
host = options;
options = null;
}
if (!options) options = {};
return function dispatch(element, ...d) {
if (host) {
d.unshift(element);
element = typeof host === "function" ? host() : host;
}
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
return element.dispatchEvent(event);
};
// src/memo.js
var memoMark = "__dde_memo";
var memo_scope = [];
function memo(key, generator) {
if (!memo_scope.length) return generator(key);
const k = typeof key === "object" ? JSON.stringify(key) : key;
const [{ cache, after }] = memo_scope;
return after(k, hasOwn(cache, k) ? cache[k] : generator(key));
}
function on(event, listener, options) {
return function registerElement(element) {
element.addEventListener(event, listener, options);
return element;
};
}
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
on.connected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evc, listener, options);
if (element[keyLTE]) return element;
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
if (c) c_ch_o.onConnected(element, listener);
return element;
};
memo.isScope = function(obj) {
return obj[memoMark];
};
on.disconnected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evd, listener, options);
if (element[keyLTE]) return element;
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
if (c) c_ch_o.onDisconnected(element, listener);
return element;
};
};
var store_abort = /* @__PURE__ */ new WeakMap();
on.disconnectedAsAbort = function(host) {
if (store_abort.has(host)) return store_abort.get(host);
const a = new AbortController();
store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a.signal;
};
var els_attribute_store = /* @__PURE__ */ new WeakSet();
on.attributeChanged = function(listener, options) {
if (typeof options !== "object")
options = {};
return function registerElement(element) {
element.addEventListener(eva, listener, options);
if (element[keyLTE] || els_attribute_store.has(element))
return element;
if (!enviroment.M) return element;
const observer = new enviroment.M(function(mutations) {
for (const { attributeName, target } of mutations)
target.dispatchEvent(
new CustomEvent(eva, { detail: [attributeName, target.getAttribute(attributeName)] })
);
memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
let cache = oCreate();
function memoScope2(...args) {
if (signal2 && signal2.aborted)
return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate();
memo_scope.unshift({
cache,
after(key, val) {
return cache_local[key] = val;
}
});
const c = onAbort(options.signal, () => observer.disconnect());
if (c) observer.observe(element, { attributes: true });
return element;
};
const out = fun.apply(this, args);
memo_scope.shift();
cache = cache_local;
return out;
}
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope2.clear);
return memoScope2;
};
// src/signals-lib/helpers.js
@ -776,25 +793,18 @@ signal.clear = function(...signals2) {
}
};
var key_reactive = "__dde_reactive";
function cache(store = oCreate()) {
return (key, fun) => hasOwn(store, key) ? store[key] : store[key] = fun();
}
signal.el = function(s, map) {
map = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start = createElement.mark({ type: "reactive", source: new Defined().compact }, true);
const mark_end = mark_start.end;
const out = enviroment.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current } = scope;
let cache_shared = oCreate();
const reRenderReactiveElement = (v) => {
if (!mark_start.parentNode || !mark_end.parentNode)
return removeSignalListener(s, reRenderReactiveElement);
const memo = cache(cache_shared);
cache_shared = oCreate();
scope.push(current);
let els = map(v, function useCache(key, fun) {
return cache_shared[key] = memo(key, fun);
});
let els = map(v);
scope.pop();
if (!Array.isArray(els))
els = [els];
@ -814,7 +824,7 @@ signal.el = function(s, map) {
current.host(on.disconnected(
() => (
/*! Clears cached elements for reactive element `S.el` */
cache_shared = {}
map.clear()
)
));
return out;
@ -846,7 +856,7 @@ var key_attributes = "__dde_attributes";
signal.observedAttributes = function(element) {
const store = element[key_attributes] = {};
const attrs = observedAttributes(element, observedAttribute(store));
on.attributeChanged(function attributeChangeToSignal({ detail }) {
on(eva, function attributeChangeToSignal({ detail }) {
/*! This maps attributes to signals (`S.observedAttributes`).
Investigate `__dde_attributes` key of the element. */
const [name, value] = detail;
@ -997,7 +1007,7 @@ export {
elementAttribute,
isSignal,
lifecyclesToEvents,
observedAttributes2 as observedAttributes,
memo,
on,
queue,
registerReactivity,

View File

@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean;
};
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal {
_: Symbol;
/**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li)));
* ```
* */
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
}
export const signal: signal;
@ -80,10 +81,11 @@ export type CustomElementTagNameMap = {
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global {
type ddeComponentAttributes = Record<any, any> | undefined;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
type ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>;
}
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = {
/**
@ -145,6 +147,7 @@ export namespace el {
host?: "this" | "parentElement";
}, is_open?: boolean): Comment;
}
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
export function el<A extends {
textContent: ddeStringable;
@ -157,7 +160,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
/** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
/**
@ -166,9 +168,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
* @param body Body of the custom element
* */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
@ -186,7 +188,7 @@ export interface On {
export const on: On;
export type Scope = {
scope: Node | Function | Object;
host: ddeElementAddon<any>;
host: Host<SupportedElement>;
custom_element: false | HTMLElement;
prevent: boolean;
};
@ -202,7 +204,11 @@ export const scope: {
* It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`.
* */
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
host: Host<SupportedElement>;
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
signal: AbortSignal;
state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
@ -214,7 +220,6 @@ export const scope: {
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
/**
* This is used primarly for server side rendering. To be sure that all async operations
* are finished before the page is sent to the client.
@ -235,6 +240,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
* ```
* */
export function queue(promise?: Promise<unknown>): Promise<unknown>;
/**
* Memoization utility for caching DOM elements to improve performance.
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
*
* @param key - Unique identifier for the element (usually an ID or unique value)
* @param generator - Function that creates the element
* @returns The cached element if the key exists, otherwise the result of the generator function
*
* @example
* ```ts
* // Within S.el for list rendering
* S.el(itemsSignal, (items, memo) =>
* el("ul").append(
* ...items.map(item =>
* memo(item.id, () => el(ItemComponent, item))
* )
* )
* )
* ```
*/
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
/**
* Memo namespace containing utility functions for memoization.
*/
export namespace memo {
/**
* Checks if an object is a memo scope.
* @param obj - The object to check
* @returns True if the object is a memo scope
*/
export function isScope(obj: any): boolean;
/**
* Creates a memoized function with optional cleanup support.
*
* @param fun - The function to memoize
* @param options - Configuration options
* @param options.signal - AbortSignal for cleanup
* @param options.onlyLast - When true, only keeps the cache from the most recent call
* @returns A memoized version of the function with a .clear() method
*
* @example
* ```ts
* const renderItems = memo.scope(function(items) {
* return items.map(item =>
* memo(item.id, () => el("div", item.name))
* );
* }, {
* signal: controller.signal,
* onlyLast: true
* });
* ```
*/
export function scope<F extends Function>(fun: F, options?: {
signal?: AbortSignal;
onlyLast?: boolean;
}): F & {
clear: () => void;
};
}
/* TypeScript MEH */
declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;

File diff suppressed because one or more lines are too long

80
dist/esm.d.ts vendored
View File

@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean;
};
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal {
_: Symbol;
/**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li)));
* ```
* */
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
}
declare const signal: signal;
@ -79,10 +80,11 @@ export type CustomElementTagNameMap = {
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global {
type ddeComponentAttributes = Record<any, any> | undefined;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
type ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>;
}
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = {
/**
@ -144,6 +146,7 @@ export namespace el {
host?: "this" | "parentElement";
}, is_open?: boolean): Comment;
}
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
export function el<A extends {
textContent: ddeStringable;
@ -156,7 +159,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
/** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
/**
@ -165,9 +167,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
* @param body Body of the custom element
* */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
@ -185,7 +187,7 @@ export interface On {
export const on: On;
export type Scope = {
scope: Node | Function | Object;
host: ddeElementAddon<any>;
host: Host<SupportedElement>;
custom_element: false | HTMLElement;
prevent: boolean;
};
@ -201,7 +203,11 @@ export const scope: {
* It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`.
* */
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
host: Host<SupportedElement>;
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
signal: AbortSignal;
state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
@ -213,7 +219,6 @@ export const scope: {
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
/**
* This is used primarly for server side rendering. To be sure that all async operations
* are finished before the page is sent to the client.
@ -234,6 +239,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
* ```
* */
export function queue(promise?: Promise<unknown>): Promise<unknown>;
/**
* Memoization utility for caching DOM elements to improve performance.
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
*
* @param key - Unique identifier for the element (usually an ID or unique value)
* @param generator - Function that creates the element
* @returns The cached element if the key exists, otherwise the result of the generator function
*
* @example
* ```ts
* // Within S.el for list rendering
* S.el(itemsSignal, (items, memo) =>
* el("ul").append(
* ...items.map(item =>
* memo(item.id, () => el(ItemComponent, item))
* )
* )
* )
* ```
*/
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
/**
* Memo namespace containing utility functions for memoization.
*/
export namespace memo {
/**
* Checks if an object is a memo scope.
* @param obj - The object to check
* @returns True if the object is a memo scope
*/
export function isScope(obj: any): boolean;
/**
* Creates a memoized function with optional cleanup support.
*
* @param fun - The function to memoize
* @param options - Configuration options
* @param options.signal - AbortSignal for cleanup
* @param options.onlyLast - When true, only keeps the cache from the most recent call
* @returns A memoized version of the function with a .clear() method
*
* @example
* ```ts
* const renderItems = memo.scope(function(items) {
* return items.map(item =>
* memo(item.id, () => el("div", item.name))
* );
* }, {
* signal: controller.signal,
* onlyLast: true
* });
* ```
*/
export function scope<F extends Function>(fun: F, options?: {
signal?: AbortSignal;
onlyLast?: boolean;
}): F & {
clear: () => void;
};
}
/* TypeScript MEH */
declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;

505
dist/esm.js vendored
View File

@ -1,4 +1,5 @@
// src/helpers.js
var hasOwn = (...a) => Object.prototype.hasOwnProperty.call(...a);
function isUndef(value) {
return typeof value === "undefined";
}
@ -8,6 +9,9 @@ function isInstance(obj, cls) {
function isProtoFrom(obj, cls) {
return Object.prototype.isPrototypeOf.call(cls, obj);
}
function oCreate(proto = null, p = {}) {
return Object.create(proto, p);
}
function oAssign(...o) {
return Object.assign(...o);
}
@ -21,16 +25,6 @@ function onAbort(signal, listener) {
signal.removeEventListener("abort", listener);
};
}
function observedAttributes(instance, observedAttribute) {
const { observedAttributes: observedAttributes3 = [] } = instance.constructor;
return observedAttributes3.reduce(function(out, name) {
out[kebabToCamel(name)] = observedAttribute(instance, name);
return out;
}, {});
}
function kebabToCamel(name) {
return name.replace(/-./g, (x) => x[1].toUpperCase());
}
// src/signals-lib/common.js
var signals_global = {
@ -68,6 +62,7 @@ var enviroment = {
setDeleteAttr,
ssr: "",
D: globalThis.document,
N: globalThis.Node,
F: globalThis.DocumentFragment,
H: globalThis.HTMLElement,
S: globalThis.SVGElement,
@ -88,6 +83,215 @@ var evc = "dde:connected";
var evd = "dde:disconnected";
var eva = "dde:attributeChanged";
// src/events-observer.js
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get() {
return () => {
};
}
});
function connectionsChangesObserverConstructor() {
const store = /* @__PURE__ */ new Map();
let is_observing = false;
const observerListener = (stop2) => function(mutations) {
for (const mutation of mutations) {
if (mutation.type !== "childList") continue;
if (observerAdded(mutation.addedNodes, true)) {
stop2();
continue;
}
if (observerRemoved(mutation.removedNodes, true))
stop2();
}
};
const observer = new enviroment.M(observerListener(stop));
return {
/**
* Creates an observer for a specific element
* @param {Element} element - Element to observe
* @returns {Function} Cleanup function
*/
observe(element) {
const o = new enviroment.M(observerListener(() => {
}));
o.observe(element, { childList: true, subtree: true });
return () => o.disconnect();
},
/**
* Register a connection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for connection event
*/
onConnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.connected.has(listener)) return;
listeners.connected.add(listener);
listeners.length_c += 1;
},
/**
* Unregister a connection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offConnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
if (!ls.connected.has(listener)) return;
ls.connected.delete(listener);
ls.length_c -= 1;
cleanWhenOff(element, ls);
},
/**
* Register a disconnection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for disconnection event
*/
onDisconnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.disconnected.has(listener)) return;
listeners.disconnected.add(listener);
listeners.length_d += 1;
},
/**
* Unregister a disconnection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offDisconnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
ls.disconnected.delete(listener);
ls.length_d -= 1;
cleanWhenOff(element, ls);
}
};
function cleanWhenOff(element, ls) {
if (ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
function getElementStore(element) {
if (store.has(element)) return store.get(element);
const out = {
connected: /* @__PURE__ */ new WeakSet(),
length_c: 0,
disconnected: /* @__PURE__ */ new WeakSet(),
length_d: 0
};
store.set(element, out);
return out;
}
function start() {
if (is_observing) return;
is_observing = true;
observer.observe(enviroment.D.body, { childList: true, subtree: true });
}
function stop() {
if (!is_observing || store.size) return;
is_observing = false;
observer.disconnect();
}
function requestIdle() {
return new Promise(function(resolve) {
(requestIdleCallback || requestAnimationFrame)(resolve);
});
}
async function collectChildren(element) {
if (store.size > 30)
await requestIdle();
const out = [];
if (!isInstance(element, enviroment.N)) return out;
for (const el of store.keys()) {
if (el === element || !isInstance(el, enviroment.N)) continue;
if (element.contains(el))
out.push(el);
}
return out;
}
function observerAdded(addedNodes, is_root) {
let out = false;
for (const element of addedNodes) {
if (is_root) collectChildren(element).then(observerAdded);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_c) continue;
element.dispatchEvent(new Event(evc));
ls.connected = /* @__PURE__ */ new WeakSet();
ls.length_c = 0;
if (!ls.length_d) store.delete(element);
out = true;
}
return out;
}
function observerRemoved(removedNodes, is_root) {
let out = false;
for (const element of removedNodes) {
if (is_root) collectChildren(element).then(observerRemoved);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_d) continue;
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
out = true;
}
return out;
}
function dispatchRemove(element) {
return () => {
if (element.isConnected) return;
element.dispatchEvent(new Event(evd));
store.delete(element);
};
}
}
// src/events.js
function dispatchEvent(name, options, host) {
if (typeof options === "function") {
host = options;
options = null;
}
if (!options) options = {};
return function dispatch(element, ...d) {
if (host) {
d.unshift(element);
element = typeof host === "function" ? host() : host;
}
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
return element.dispatchEvent(event);
};
}
function on(event, listener, options) {
return function registerElement(element) {
element.addEventListener(event, listener, options);
return element;
};
}
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
on.connected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evc, listener, options);
if (element[keyLTE]) return element;
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
if (c) c_ch_o.onConnected(element, listener);
return element;
};
};
on.disconnected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evd, listener, options);
if (element[keyLTE]) return element;
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
if (c) c_ch_o.onDisconnected(element, listener);
return element;
};
};
// src/dom.js
function queue(promise) {
return enviroment.q(promise);
@ -99,6 +303,7 @@ var scopes = [{
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
prevent: true
}];
var store_abort = /* @__PURE__ */ new WeakMap();
var scope = {
/**
* Gets the current scope
@ -115,6 +320,17 @@ var scope = {
return this.current.host;
},
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
get signal() {
const { host } = this;
if (store_abort.has(host)) return store_abort.get(host);
const a = new AbortController();
store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a.signal;
},
/**
* Prevents default behavior in the current scope
* @returns {Object} Current scope context
*/
@ -357,172 +573,8 @@ function setDelete(obj, key, val) {
return Reflect.deleteProperty(obj, key);
}
// src/events-observer.js
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get() {
return () => {
};
}
});
function connectionsChangesObserverConstructor() {
const store = /* @__PURE__ */ new Map();
let is_observing = false;
const observerListener = (stop2) => function(mutations) {
for (const mutation of mutations) {
if (mutation.type !== "childList") continue;
if (observerAdded(mutation.addedNodes, true)) {
stop2();
continue;
}
if (observerRemoved(mutation.removedNodes, true))
stop2();
}
};
const observer = new enviroment.M(observerListener(stop));
return {
/**
* Creates an observer for a specific element
* @param {Element} element - Element to observe
* @returns {Function} Cleanup function
*/
observe(element) {
const o = new enviroment.M(observerListener(() => {
}));
o.observe(element, { childList: true, subtree: true });
return () => o.disconnect();
},
/**
* Register a connection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for connection event
*/
onConnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.connected.has(listener)) return;
listeners.connected.add(listener);
listeners.length_c += 1;
},
/**
* Unregister a connection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offConnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
if (!ls.connected.has(listener)) return;
ls.connected.delete(listener);
ls.length_c -= 1;
cleanWhenOff(element, ls);
},
/**
* Register a disconnection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for disconnection event
*/
onDisconnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.disconnected.has(listener)) return;
listeners.disconnected.add(listener);
listeners.length_d += 1;
},
/**
* Unregister a disconnection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offDisconnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
ls.disconnected.delete(listener);
ls.length_d -= 1;
cleanWhenOff(element, ls);
}
};
function cleanWhenOff(element, ls) {
if (ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
function getElementStore(element) {
if (store.has(element)) return store.get(element);
const out = {
connected: /* @__PURE__ */ new WeakSet(),
length_c: 0,
disconnected: /* @__PURE__ */ new WeakSet(),
length_d: 0
};
store.set(element, out);
return out;
}
function start() {
if (is_observing) return;
is_observing = true;
observer.observe(enviroment.D.body, { childList: true, subtree: true });
}
function stop() {
if (!is_observing || store.size) return;
is_observing = false;
observer.disconnect();
}
function requestIdle() {
return new Promise(function(resolve) {
(requestIdleCallback || requestAnimationFrame)(resolve);
});
}
async function collectChildren(element) {
if (store.size > 30)
await requestIdle();
const out = [];
if (!isInstance(element, Node)) return out;
for (const el of store.keys()) {
if (el === element || !isInstance(el, Node)) continue;
if (element.contains(el))
out.push(el);
}
return out;
}
function observerAdded(addedNodes, is_root) {
let out = false;
for (const element of addedNodes) {
if (is_root) collectChildren(element).then(observerAdded);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_c) continue;
element.dispatchEvent(new Event(evc));
ls.connected = /* @__PURE__ */ new WeakSet();
ls.length_c = 0;
if (!ls.length_d) store.delete(element);
out = true;
}
return out;
}
function observerRemoved(removedNodes, is_root) {
let out = false;
for (const element of removedNodes) {
if (is_root) collectChildren(element).then(observerRemoved);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_d) continue;
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
out = true;
}
return out;
}
function dispatchRemove(element) {
return () => {
if (element.isConnected) return;
element.dispatchEvent(new Event(evd));
store.delete(element);
};
}
}
// src/customElement.js
function customElementRender(target, render, props = observedAttributes2) {
function customElementRender(target, render, props = {}) {
const custom_element = target.host || target;
scope.push({
scope: custom_element,
@ -563,81 +615,40 @@ function wrapMethod(obj, method, apply) {
obj[method] = new Proxy(obj[method] || (() => {
}), { apply });
}
function observedAttributes2(instance) {
return observedAttributes(instance, (i, n) => i.getAttribute(n));
}
// src/events.js
function dispatchEvent(name, options, host) {
if (typeof options === "function") {
host = options;
options = null;
}
if (!options) options = {};
return function dispatch(element, ...d) {
if (host) {
d.unshift(element);
element = typeof host === "function" ? host() : host;
}
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
return element.dispatchEvent(event);
};
// src/memo.js
var memoMark = "__dde_memo";
var memo_scope = [];
function memo(key, generator) {
if (!memo_scope.length) return generator(key);
const k = typeof key === "object" ? JSON.stringify(key) : key;
const [{ cache, after }] = memo_scope;
return after(k, hasOwn(cache, k) ? cache[k] : generator(key));
}
function on(event, listener, options) {
return function registerElement(element) {
element.addEventListener(event, listener, options);
return element;
};
}
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
on.connected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evc, listener, options);
if (element[keyLTE]) return element;
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
if (c) c_ch_o.onConnected(element, listener);
return element;
};
memo.isScope = function(obj) {
return obj[memoMark];
};
on.disconnected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evd, listener, options);
if (element[keyLTE]) return element;
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
if (c) c_ch_o.onDisconnected(element, listener);
return element;
};
};
var store_abort = /* @__PURE__ */ new WeakMap();
on.disconnectedAsAbort = function(host) {
if (store_abort.has(host)) return store_abort.get(host);
const a = new AbortController();
store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a.signal;
};
var els_attribute_store = /* @__PURE__ */ new WeakSet();
on.attributeChanged = function(listener, options) {
if (typeof options !== "object")
options = {};
return function registerElement(element) {
element.addEventListener(eva, listener, options);
if (element[keyLTE] || els_attribute_store.has(element))
return element;
if (!enviroment.M) return element;
const observer = new enviroment.M(function(mutations) {
for (const { attributeName, target } of mutations)
target.dispatchEvent(
new CustomEvent(eva, { detail: [attributeName, target.getAttribute(attributeName)] })
);
memo.scope = function memoScope(fun, { signal, onlyLast } = {}) {
let cache = oCreate();
function memoScope2(...args) {
if (signal && signal.aborted)
return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate();
memo_scope.unshift({
cache,
after(key, val) {
return cache_local[key] = val;
}
});
const c = onAbort(options.signal, () => observer.disconnect());
if (c) observer.observe(element, { attributes: true });
return element;
};
const out = fun.apply(this, args);
memo_scope.shift();
cache = cache_local;
return out;
}
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal) signal.addEventListener("abort", memoScope2.clear);
return memoScope2;
};
export {
assign,
@ -653,7 +664,7 @@ export {
createElementNS as elNS,
elementAttribute,
lifecyclesToEvents,
observedAttributes2 as observedAttributes,
memo,
on,
queue,
registerReactivity,

80
dist/esm.min.d.ts vendored
View File

@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean;
};
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal {
_: Symbol;
/**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li)));
* ```
* */
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
}
declare const signal: signal;
@ -79,10 +80,11 @@ export type CustomElementTagNameMap = {
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global {
type ddeComponentAttributes = Record<any, any> | undefined;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
type ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>;
}
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = {
/**
@ -144,6 +146,7 @@ export namespace el {
host?: "this" | "parentElement";
}, is_open?: boolean): Comment;
}
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
export function el<A extends {
textContent: ddeStringable;
@ -156,7 +159,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
/** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
/**
@ -165,9 +167,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
* @param body Body of the custom element
* */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
@ -185,7 +187,7 @@ export interface On {
export const on: On;
export type Scope = {
scope: Node | Function | Object;
host: ddeElementAddon<any>;
host: Host<SupportedElement>;
custom_element: false | HTMLElement;
prevent: boolean;
};
@ -201,7 +203,11 @@ export const scope: {
* It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`.
* */
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
host: Host<SupportedElement>;
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
signal: AbortSignal;
state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
@ -213,7 +219,6 @@ export const scope: {
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
/**
* This is used primarly for server side rendering. To be sure that all async operations
* are finished before the page is sent to the client.
@ -234,6 +239,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
* ```
* */
export function queue(promise?: Promise<unknown>): Promise<unknown>;
/**
* Memoization utility for caching DOM elements to improve performance.
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
*
* @param key - Unique identifier for the element (usually an ID or unique value)
* @param generator - Function that creates the element
* @returns The cached element if the key exists, otherwise the result of the generator function
*
* @example
* ```ts
* // Within S.el for list rendering
* S.el(itemsSignal, (items, memo) =>
* el("ul").append(
* ...items.map(item =>
* memo(item.id, () => el(ItemComponent, item))
* )
* )
* )
* ```
*/
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
/**
* Memo namespace containing utility functions for memoization.
*/
export namespace memo {
/**
* Checks if an object is a memo scope.
* @param obj - The object to check
* @returns True if the object is a memo scope
*/
export function isScope(obj: any): boolean;
/**
* Creates a memoized function with optional cleanup support.
*
* @param fun - The function to memoize
* @param options - Configuration options
* @param options.signal - AbortSignal for cleanup
* @param options.onlyLast - When true, only keeps the cache from the most recent call
* @returns A memoized version of the function with a .clear() method
*
* @example
* ```ts
* const renderItems = memo.scope(function(items) {
* return items.map(item =>
* memo(item.id, () => el("div", item.name))
* );
* }, {
* signal: controller.signal,
* onlyLast: true
* });
* ```
*/
export function scope<F extends Function>(fun: F, options?: {
signal?: AbortSignal;
onlyLast?: boolean;
}): F & {
clear: () => void;
};
}
/* TypeScript MEH */
declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;

2
dist/esm.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean;
};
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal {
_: Symbol;
/**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li)));
* ```
* */
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
}
export const signal: signal;
@ -80,10 +81,11 @@ export type CustomElementTagNameMap = {
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global {
type ddeComponentAttributes = Record<any, any> | undefined;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
type ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>;
}
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = {
/**
@ -145,6 +147,7 @@ export namespace el {
host?: "this" | "parentElement";
}, is_open?: boolean): Comment;
}
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
export function el<A extends {
textContent: ddeStringable;
@ -157,7 +160,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
/** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
/**
@ -166,9 +168,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
* @param body Body of the custom element
* */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
@ -186,7 +188,7 @@ export interface On {
export const on: On;
export type Scope = {
scope: Node | Function | Object;
host: ddeElementAddon<any>;
host: Host<SupportedElement>;
custom_element: false | HTMLElement;
prevent: boolean;
};
@ -202,7 +204,11 @@ export const scope: {
* It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`.
* */
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
host: Host<SupportedElement>;
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
signal: AbortSignal;
state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
@ -214,7 +220,6 @@ export const scope: {
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
/**
* This is used primarly for server side rendering. To be sure that all async operations
* are finished before the page is sent to the client.
@ -235,6 +240,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
* ```
* */
export function queue(promise?: Promise<unknown>): Promise<unknown>;
/**
* Memoization utility for caching DOM elements to improve performance.
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
*
* @param key - Unique identifier for the element (usually an ID or unique value)
* @param generator - Function that creates the element
* @returns The cached element if the key exists, otherwise the result of the generator function
*
* @example
* ```ts
* // Within S.el for list rendering
* S.el(itemsSignal, (items, memo) =>
* el("ul").append(
* ...items.map(item =>
* memo(item.id, () => el(ItemComponent, item))
* )
* )
* )
* ```
*/
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
/**
* Memo namespace containing utility functions for memoization.
*/
export namespace memo {
/**
* Checks if an object is a memo scope.
* @param obj - The object to check
* @returns True if the object is a memo scope
*/
export function isScope(obj: any): boolean;
/**
* Creates a memoized function with optional cleanup support.
*
* @param fun - The function to memoize
* @param options - Configuration options
* @param options.signal - AbortSignal for cleanup
* @param options.onlyLast - When true, only keeps the cache from the most recent call
* @returns A memoized version of the function with a .clear() method
*
* @example
* ```ts
* const renderItems = memo.scope(function(items) {
* return items.map(item =>
* memo(item.id, () => el("div", item.name))
* );
* }, {
* signal: controller.signal,
* onlyLast: true
* });
* ```
*/
export function scope<F extends Function>(fun: F, options?: {
signal?: AbortSignal;
onlyLast?: boolean;
}): F & {
clear: () => void;
};
}
/* TypeScript MEH */
declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;

View File

@ -35,7 +35,7 @@ var DDE = (() => {
elementAttribute: () => elementAttribute,
isSignal: () => isSignal,
lifecyclesToEvents: () => lifecyclesToEvents,
observedAttributes: () => observedAttributes2,
memo: () => memo,
on: () => on,
queue: () => queue,
registerReactivity: () => registerReactivity,
@ -78,8 +78,8 @@ var DDE = (() => {
};
}
function observedAttributes(instance, observedAttribute2) {
const { observedAttributes: observedAttributes3 = [] } = instance.constructor;
return observedAttributes3.reduce(function(out, name) {
const { observedAttributes: observedAttributes2 = [] } = instance.constructor;
return observedAttributes2.reduce(function(out, name) {
out[kebabToCamel(name)] = observedAttribute2(instance, name);
return out;
}, {});
@ -137,6 +137,7 @@ var DDE = (() => {
setDeleteAttr,
ssr: "",
D: globalThis.document,
N: globalThis.Node,
F: globalThis.DocumentFragment,
H: globalThis.HTMLElement,
S: globalThis.SVGElement,
@ -157,6 +158,215 @@ var DDE = (() => {
var evd = "dde:disconnected";
var eva = "dde:attributeChanged";
// src/events-observer.js
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get() {
return () => {
};
}
});
function connectionsChangesObserverConstructor() {
const store = /* @__PURE__ */ new Map();
let is_observing = false;
const observerListener = (stop2) => function(mutations) {
for (const mutation of mutations) {
if (mutation.type !== "childList") continue;
if (observerAdded(mutation.addedNodes, true)) {
stop2();
continue;
}
if (observerRemoved(mutation.removedNodes, true))
stop2();
}
};
const observer = new enviroment.M(observerListener(stop));
return {
/**
* Creates an observer for a specific element
* @param {Element} element - Element to observe
* @returns {Function} Cleanup function
*/
observe(element) {
const o = new enviroment.M(observerListener(() => {
}));
o.observe(element, { childList: true, subtree: true });
return () => o.disconnect();
},
/**
* Register a connection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for connection event
*/
onConnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.connected.has(listener)) return;
listeners.connected.add(listener);
listeners.length_c += 1;
},
/**
* Unregister a connection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offConnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
if (!ls.connected.has(listener)) return;
ls.connected.delete(listener);
ls.length_c -= 1;
cleanWhenOff(element, ls);
},
/**
* Register a disconnection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for disconnection event
*/
onDisconnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.disconnected.has(listener)) return;
listeners.disconnected.add(listener);
listeners.length_d += 1;
},
/**
* Unregister a disconnection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offDisconnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
ls.disconnected.delete(listener);
ls.length_d -= 1;
cleanWhenOff(element, ls);
}
};
function cleanWhenOff(element, ls) {
if (ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
function getElementStore(element) {
if (store.has(element)) return store.get(element);
const out = {
connected: /* @__PURE__ */ new WeakSet(),
length_c: 0,
disconnected: /* @__PURE__ */ new WeakSet(),
length_d: 0
};
store.set(element, out);
return out;
}
function start() {
if (is_observing) return;
is_observing = true;
observer.observe(enviroment.D.body, { childList: true, subtree: true });
}
function stop() {
if (!is_observing || store.size) return;
is_observing = false;
observer.disconnect();
}
function requestIdle() {
return new Promise(function(resolve) {
(requestIdleCallback || requestAnimationFrame)(resolve);
});
}
async function collectChildren(element) {
if (store.size > 30)
await requestIdle();
const out = [];
if (!isInstance(element, enviroment.N)) return out;
for (const el of store.keys()) {
if (el === element || !isInstance(el, enviroment.N)) continue;
if (element.contains(el))
out.push(el);
}
return out;
}
function observerAdded(addedNodes, is_root) {
let out = false;
for (const element of addedNodes) {
if (is_root) collectChildren(element).then(observerAdded);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_c) continue;
element.dispatchEvent(new Event(evc));
ls.connected = /* @__PURE__ */ new WeakSet();
ls.length_c = 0;
if (!ls.length_d) store.delete(element);
out = true;
}
return out;
}
function observerRemoved(removedNodes, is_root) {
let out = false;
for (const element of removedNodes) {
if (is_root) collectChildren(element).then(observerRemoved);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_d) continue;
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
out = true;
}
return out;
}
function dispatchRemove(element) {
return () => {
if (element.isConnected) return;
element.dispatchEvent(new Event(evd));
store.delete(element);
};
}
}
// src/events.js
function dispatchEvent(name, options, host) {
if (typeof options === "function") {
host = options;
options = null;
}
if (!options) options = {};
return function dispatch(element, ...d) {
if (host) {
d.unshift(element);
element = typeof host === "function" ? host() : host;
}
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
return element.dispatchEvent(event);
};
}
function on(event, listener, options) {
return function registerElement(element) {
element.addEventListener(event, listener, options);
return element;
};
}
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
on.connected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evc, listener, options);
if (element[keyLTE]) return element;
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
if (c) c_ch_o.onConnected(element, listener);
return element;
};
};
on.disconnected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evd, listener, options);
if (element[keyLTE]) return element;
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
if (c) c_ch_o.onDisconnected(element, listener);
return element;
};
};
// src/dom.js
function queue(promise) {
return enviroment.q(promise);
@ -168,6 +378,7 @@ var DDE = (() => {
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
prevent: true
}];
var store_abort = /* @__PURE__ */ new WeakMap();
var scope = {
/**
* Gets the current scope
@ -184,6 +395,17 @@ var DDE = (() => {
return this.current.host;
},
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
get signal() {
const { host } = this;
if (store_abort.has(host)) return store_abort.get(host);
const a = new AbortController();
store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a.signal;
},
/**
* Prevents default behavior in the current scope
* @returns {Object} Current scope context
*/
@ -426,172 +648,8 @@ var DDE = (() => {
return Reflect.deleteProperty(obj, key);
}
// src/events-observer.js
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get() {
return () => {
};
}
});
function connectionsChangesObserverConstructor() {
const store = /* @__PURE__ */ new Map();
let is_observing = false;
const observerListener = (stop2) => function(mutations) {
for (const mutation of mutations) {
if (mutation.type !== "childList") continue;
if (observerAdded(mutation.addedNodes, true)) {
stop2();
continue;
}
if (observerRemoved(mutation.removedNodes, true))
stop2();
}
};
const observer = new enviroment.M(observerListener(stop));
return {
/**
* Creates an observer for a specific element
* @param {Element} element - Element to observe
* @returns {Function} Cleanup function
*/
observe(element) {
const o = new enviroment.M(observerListener(() => {
}));
o.observe(element, { childList: true, subtree: true });
return () => o.disconnect();
},
/**
* Register a connection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for connection event
*/
onConnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.connected.has(listener)) return;
listeners.connected.add(listener);
listeners.length_c += 1;
},
/**
* Unregister a connection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offConnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
if (!ls.connected.has(listener)) return;
ls.connected.delete(listener);
ls.length_c -= 1;
cleanWhenOff(element, ls);
},
/**
* Register a disconnection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for disconnection event
*/
onDisconnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.disconnected.has(listener)) return;
listeners.disconnected.add(listener);
listeners.length_d += 1;
},
/**
* Unregister a disconnection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offDisconnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
ls.disconnected.delete(listener);
ls.length_d -= 1;
cleanWhenOff(element, ls);
}
};
function cleanWhenOff(element, ls) {
if (ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
function getElementStore(element) {
if (store.has(element)) return store.get(element);
const out = {
connected: /* @__PURE__ */ new WeakSet(),
length_c: 0,
disconnected: /* @__PURE__ */ new WeakSet(),
length_d: 0
};
store.set(element, out);
return out;
}
function start() {
if (is_observing) return;
is_observing = true;
observer.observe(enviroment.D.body, { childList: true, subtree: true });
}
function stop() {
if (!is_observing || store.size) return;
is_observing = false;
observer.disconnect();
}
function requestIdle() {
return new Promise(function(resolve) {
(requestIdleCallback || requestAnimationFrame)(resolve);
});
}
async function collectChildren(element) {
if (store.size > 30)
await requestIdle();
const out = [];
if (!isInstance(element, Node)) return out;
for (const el of store.keys()) {
if (el === element || !isInstance(el, Node)) continue;
if (element.contains(el))
out.push(el);
}
return out;
}
function observerAdded(addedNodes, is_root) {
let out = false;
for (const element of addedNodes) {
if (is_root) collectChildren(element).then(observerAdded);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_c) continue;
element.dispatchEvent(new Event(evc));
ls.connected = /* @__PURE__ */ new WeakSet();
ls.length_c = 0;
if (!ls.length_d) store.delete(element);
out = true;
}
return out;
}
function observerRemoved(removedNodes, is_root) {
let out = false;
for (const element of removedNodes) {
if (is_root) collectChildren(element).then(observerRemoved);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_d) continue;
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
out = true;
}
return out;
}
function dispatchRemove(element) {
return () => {
if (element.isConnected) return;
element.dispatchEvent(new Event(evd));
store.delete(element);
};
}
}
// src/customElement.js
function customElementRender(target, render, props = observedAttributes2) {
function customElementRender(target, render, props = {}) {
const custom_element = target.host || target;
scope.push({
scope: custom_element,
@ -632,81 +690,40 @@ var DDE = (() => {
obj[method] = new Proxy(obj[method] || (() => {
}), { apply });
}
function observedAttributes2(instance) {
return observedAttributes(instance, (i, n) => i.getAttribute(n));
}
// src/events.js
function dispatchEvent(name, options, host) {
if (typeof options === "function") {
host = options;
options = null;
}
if (!options) options = {};
return function dispatch(element, ...d) {
if (host) {
d.unshift(element);
element = typeof host === "function" ? host() : host;
}
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
return element.dispatchEvent(event);
};
// src/memo.js
var memoMark = "__dde_memo";
var memo_scope = [];
function memo(key, generator) {
if (!memo_scope.length) return generator(key);
const k = typeof key === "object" ? JSON.stringify(key) : key;
const [{ cache, after }] = memo_scope;
return after(k, hasOwn(cache, k) ? cache[k] : generator(key));
}
function on(event, listener, options) {
return function registerElement(element) {
element.addEventListener(event, listener, options);
return element;
};
}
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
on.connected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evc, listener, options);
if (element[keyLTE]) return element;
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
if (c) c_ch_o.onConnected(element, listener);
return element;
};
memo.isScope = function(obj) {
return obj[memoMark];
};
on.disconnected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evd, listener, options);
if (element[keyLTE]) return element;
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
if (c) c_ch_o.onDisconnected(element, listener);
return element;
};
};
var store_abort = /* @__PURE__ */ new WeakMap();
on.disconnectedAsAbort = function(host) {
if (store_abort.has(host)) return store_abort.get(host);
const a = new AbortController();
store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a.signal;
};
var els_attribute_store = /* @__PURE__ */ new WeakSet();
on.attributeChanged = function(listener, options) {
if (typeof options !== "object")
options = {};
return function registerElement(element) {
element.addEventListener(eva, listener, options);
if (element[keyLTE] || els_attribute_store.has(element))
return element;
if (!enviroment.M) return element;
const observer = new enviroment.M(function(mutations) {
for (const { attributeName, target } of mutations)
target.dispatchEvent(
new CustomEvent(eva, { detail: [attributeName, target.getAttribute(attributeName)] })
);
memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
let cache = oCreate();
function memoScope2(...args) {
if (signal2 && signal2.aborted)
return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate();
memo_scope.unshift({
cache,
after(key, val) {
return cache_local[key] = val;
}
});
const c = onAbort(options.signal, () => observer.disconnect());
if (c) observer.observe(element, { attributes: true });
return element;
};
const out = fun.apply(this, args);
memo_scope.shift();
cache = cache_local;
return out;
}
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope2.clear);
return memoScope2;
};
// src/signals-lib/helpers.js
@ -822,25 +839,18 @@ var DDE = (() => {
}
};
var key_reactive = "__dde_reactive";
function cache(store = oCreate()) {
return (key, fun) => hasOwn(store, key) ? store[key] : store[key] = fun();
}
signal.el = function(s, map) {
map = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start = createElement.mark({ type: "reactive", source: new Defined().compact }, true);
const mark_end = mark_start.end;
const out = enviroment.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current } = scope;
let cache_shared = oCreate();
const reRenderReactiveElement = (v) => {
if (!mark_start.parentNode || !mark_end.parentNode)
return removeSignalListener(s, reRenderReactiveElement);
const memo = cache(cache_shared);
cache_shared = oCreate();
scope.push(current);
let els = map(v, function useCache(key, fun) {
return cache_shared[key] = memo(key, fun);
});
let els = map(v);
scope.pop();
if (!Array.isArray(els))
els = [els];
@ -860,7 +870,7 @@ var DDE = (() => {
current.host(on.disconnected(
() => (
/*! Clears cached elements for reactive element `S.el` */
cache_shared = {}
map.clear()
)
));
return out;
@ -892,7 +902,7 @@ var DDE = (() => {
signal.observedAttributes = function(element) {
const store = element[key_attributes] = {};
const attrs = observedAttributes(element, observedAttribute(store));
on.attributeChanged(function attributeChangeToSignal({ detail }) {
on(eva, function attributeChangeToSignal({ detail }) {
/*! This maps attributes to signals (`S.observedAttributes`).
Investigate `__dde_attributes` key of the element. */
const [name, value] = detail;

View File

@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean;
};
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal {
_: Symbol;
/**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li)));
* ```
* */
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
}
export const signal: signal;
@ -80,10 +81,11 @@ export type CustomElementTagNameMap = {
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global {
type ddeComponentAttributes = Record<any, any> | undefined;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
type ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>;
}
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = {
/**
@ -145,6 +147,7 @@ export namespace el {
host?: "this" | "parentElement";
}, is_open?: boolean): Comment;
}
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
export function el<A extends {
textContent: ddeStringable;
@ -157,7 +160,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
/** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
/**
@ -166,9 +168,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
* @param body Body of the custom element
* */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
@ -186,7 +188,7 @@ export interface On {
export const on: On;
export type Scope = {
scope: Node | Function | Object;
host: ddeElementAddon<any>;
host: Host<SupportedElement>;
custom_element: false | HTMLElement;
prevent: boolean;
};
@ -202,7 +204,11 @@ export const scope: {
* It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`.
* */
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
host: Host<SupportedElement>;
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
signal: AbortSignal;
state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
@ -214,7 +220,6 @@ export const scope: {
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
/**
* This is used primarly for server side rendering. To be sure that all async operations
* are finished before the page is sent to the client.
@ -235,6 +240,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
* ```
* */
export function queue(promise?: Promise<unknown>): Promise<unknown>;
/**
* Memoization utility for caching DOM elements to improve performance.
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
*
* @param key - Unique identifier for the element (usually an ID or unique value)
* @param generator - Function that creates the element
* @returns The cached element if the key exists, otherwise the result of the generator function
*
* @example
* ```ts
* // Within S.el for list rendering
* S.el(itemsSignal, (items, memo) =>
* el("ul").append(
* ...items.map(item =>
* memo(item.id, () => el(ItemComponent, item))
* )
* )
* )
* ```
*/
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
/**
* Memo namespace containing utility functions for memoization.
*/
export namespace memo {
/**
* Checks if an object is a memo scope.
* @param obj - The object to check
* @returns True if the object is a memo scope
*/
export function isScope(obj: any): boolean;
/**
* Creates a memoized function with optional cleanup support.
*
* @param fun - The function to memoize
* @param options - Configuration options
* @param options.signal - AbortSignal for cleanup
* @param options.onlyLast - When true, only keeps the cache from the most recent call
* @returns A memoized version of the function with a .clear() method
*
* @example
* ```ts
* const renderItems = memo.scope(function(items) {
* return items.map(item =>
* memo(item.id, () => el("div", item.name))
* );
* }, {
* signal: controller.signal,
* onlyLast: true
* });
* ```
*/
export function scope<F extends Function>(fun: F, options?: {
signal?: AbortSignal;
onlyLast?: boolean;
}): F & {
clear: () => void;
};
}
/* TypeScript MEH */
declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;

File diff suppressed because one or more lines are too long

80
dist/iife.d.ts vendored
View File

@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean;
};
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal {
_: Symbol;
/**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li)));
* ```
* */
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
}
declare const signal: signal;
@ -79,10 +80,11 @@ export type CustomElementTagNameMap = {
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global {
type ddeComponentAttributes = Record<any, any> | undefined;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
type ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>;
}
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = {
/**
@ -144,6 +146,7 @@ export namespace el {
host?: "this" | "parentElement";
}, is_open?: boolean): Comment;
}
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
export function el<A extends {
textContent: ddeStringable;
@ -156,7 +159,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
/** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
/**
@ -165,9 +167,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
* @param body Body of the custom element
* */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
@ -185,7 +187,7 @@ export interface On {
export const on: On;
export type Scope = {
scope: Node | Function | Object;
host: ddeElementAddon<any>;
host: Host<SupportedElement>;
custom_element: false | HTMLElement;
prevent: boolean;
};
@ -201,7 +203,11 @@ export const scope: {
* It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`.
* */
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
host: Host<SupportedElement>;
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
signal: AbortSignal;
state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
@ -213,7 +219,6 @@ export const scope: {
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
/**
* This is used primarly for server side rendering. To be sure that all async operations
* are finished before the page is sent to the client.
@ -234,6 +239,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
* ```
* */
export function queue(promise?: Promise<unknown>): Promise<unknown>;
/**
* Memoization utility for caching DOM elements to improve performance.
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
*
* @param key - Unique identifier for the element (usually an ID or unique value)
* @param generator - Function that creates the element
* @returns The cached element if the key exists, otherwise the result of the generator function
*
* @example
* ```ts
* // Within S.el for list rendering
* S.el(itemsSignal, (items, memo) =>
* el("ul").append(
* ...items.map(item =>
* memo(item.id, () => el(ItemComponent, item))
* )
* )
* )
* ```
*/
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
/**
* Memo namespace containing utility functions for memoization.
*/
export namespace memo {
/**
* Checks if an object is a memo scope.
* @param obj - The object to check
* @returns True if the object is a memo scope
*/
export function isScope(obj: any): boolean;
/**
* Creates a memoized function with optional cleanup support.
*
* @param fun - The function to memoize
* @param options - Configuration options
* @param options.signal - AbortSignal for cleanup
* @param options.onlyLast - When true, only keeps the cache from the most recent call
* @returns A memoized version of the function with a .clear() method
*
* @example
* ```ts
* const renderItems = memo.scope(function(items) {
* return items.map(item =>
* memo(item.id, () => el("div", item.name))
* );
* }, {
* signal: controller.signal,
* onlyLast: true
* });
* ```
*/
export function scope<F extends Function>(fun: F, options?: {
signal?: AbortSignal;
onlyLast?: boolean;
}): F & {
clear: () => void;
};
}
/* TypeScript MEH */
declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;

505
dist/iife.js vendored
View File

@ -33,7 +33,7 @@ var DDE = (() => {
elNS: () => createElementNS,
elementAttribute: () => elementAttribute,
lifecyclesToEvents: () => lifecyclesToEvents,
observedAttributes: () => observedAttributes2,
memo: () => memo,
on: () => on,
queue: () => queue,
registerReactivity: () => registerReactivity,
@ -42,6 +42,7 @@ var DDE = (() => {
});
// src/helpers.js
var hasOwn = (...a) => Object.prototype.hasOwnProperty.call(...a);
function isUndef(value) {
return typeof value === "undefined";
}
@ -51,6 +52,9 @@ var DDE = (() => {
function isProtoFrom(obj, cls) {
return Object.prototype.isPrototypeOf.call(cls, obj);
}
function oCreate(proto = null, p = {}) {
return Object.create(proto, p);
}
function oAssign(...o) {
return Object.assign(...o);
}
@ -64,16 +68,6 @@ var DDE = (() => {
signal.removeEventListener("abort", listener);
};
}
function observedAttributes(instance, observedAttribute) {
const { observedAttributes: observedAttributes3 = [] } = instance.constructor;
return observedAttributes3.reduce(function(out, name) {
out[kebabToCamel(name)] = observedAttribute(instance, name);
return out;
}, {});
}
function kebabToCamel(name) {
return name.replace(/-./g, (x) => x[1].toUpperCase());
}
// src/signals-lib/common.js
var signals_global = {
@ -111,6 +105,7 @@ var DDE = (() => {
setDeleteAttr,
ssr: "",
D: globalThis.document,
N: globalThis.Node,
F: globalThis.DocumentFragment,
H: globalThis.HTMLElement,
S: globalThis.SVGElement,
@ -131,6 +126,215 @@ var DDE = (() => {
var evd = "dde:disconnected";
var eva = "dde:attributeChanged";
// src/events-observer.js
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get() {
return () => {
};
}
});
function connectionsChangesObserverConstructor() {
const store = /* @__PURE__ */ new Map();
let is_observing = false;
const observerListener = (stop2) => function(mutations) {
for (const mutation of mutations) {
if (mutation.type !== "childList") continue;
if (observerAdded(mutation.addedNodes, true)) {
stop2();
continue;
}
if (observerRemoved(mutation.removedNodes, true))
stop2();
}
};
const observer = new enviroment.M(observerListener(stop));
return {
/**
* Creates an observer for a specific element
* @param {Element} element - Element to observe
* @returns {Function} Cleanup function
*/
observe(element) {
const o = new enviroment.M(observerListener(() => {
}));
o.observe(element, { childList: true, subtree: true });
return () => o.disconnect();
},
/**
* Register a connection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for connection event
*/
onConnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.connected.has(listener)) return;
listeners.connected.add(listener);
listeners.length_c += 1;
},
/**
* Unregister a connection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offConnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
if (!ls.connected.has(listener)) return;
ls.connected.delete(listener);
ls.length_c -= 1;
cleanWhenOff(element, ls);
},
/**
* Register a disconnection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for disconnection event
*/
onDisconnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.disconnected.has(listener)) return;
listeners.disconnected.add(listener);
listeners.length_d += 1;
},
/**
* Unregister a disconnection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offDisconnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
ls.disconnected.delete(listener);
ls.length_d -= 1;
cleanWhenOff(element, ls);
}
};
function cleanWhenOff(element, ls) {
if (ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
function getElementStore(element) {
if (store.has(element)) return store.get(element);
const out = {
connected: /* @__PURE__ */ new WeakSet(),
length_c: 0,
disconnected: /* @__PURE__ */ new WeakSet(),
length_d: 0
};
store.set(element, out);
return out;
}
function start() {
if (is_observing) return;
is_observing = true;
observer.observe(enviroment.D.body, { childList: true, subtree: true });
}
function stop() {
if (!is_observing || store.size) return;
is_observing = false;
observer.disconnect();
}
function requestIdle() {
return new Promise(function(resolve) {
(requestIdleCallback || requestAnimationFrame)(resolve);
});
}
async function collectChildren(element) {
if (store.size > 30)
await requestIdle();
const out = [];
if (!isInstance(element, enviroment.N)) return out;
for (const el of store.keys()) {
if (el === element || !isInstance(el, enviroment.N)) continue;
if (element.contains(el))
out.push(el);
}
return out;
}
function observerAdded(addedNodes, is_root) {
let out = false;
for (const element of addedNodes) {
if (is_root) collectChildren(element).then(observerAdded);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_c) continue;
element.dispatchEvent(new Event(evc));
ls.connected = /* @__PURE__ */ new WeakSet();
ls.length_c = 0;
if (!ls.length_d) store.delete(element);
out = true;
}
return out;
}
function observerRemoved(removedNodes, is_root) {
let out = false;
for (const element of removedNodes) {
if (is_root) collectChildren(element).then(observerRemoved);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_d) continue;
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
out = true;
}
return out;
}
function dispatchRemove(element) {
return () => {
if (element.isConnected) return;
element.dispatchEvent(new Event(evd));
store.delete(element);
};
}
}
// src/events.js
function dispatchEvent(name, options, host) {
if (typeof options === "function") {
host = options;
options = null;
}
if (!options) options = {};
return function dispatch(element, ...d) {
if (host) {
d.unshift(element);
element = typeof host === "function" ? host() : host;
}
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
return element.dispatchEvent(event);
};
}
function on(event, listener, options) {
return function registerElement(element) {
element.addEventListener(event, listener, options);
return element;
};
}
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
on.connected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evc, listener, options);
if (element[keyLTE]) return element;
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
if (c) c_ch_o.onConnected(element, listener);
return element;
};
};
on.disconnected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evd, listener, options);
if (element[keyLTE]) return element;
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
if (c) c_ch_o.onDisconnected(element, listener);
return element;
};
};
// src/dom.js
function queue(promise) {
return enviroment.q(promise);
@ -142,6 +346,7 @@ var DDE = (() => {
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
prevent: true
}];
var store_abort = /* @__PURE__ */ new WeakMap();
var scope = {
/**
* Gets the current scope
@ -158,6 +363,17 @@ var DDE = (() => {
return this.current.host;
},
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
get signal() {
const { host } = this;
if (store_abort.has(host)) return store_abort.get(host);
const a = new AbortController();
store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a.signal;
},
/**
* Prevents default behavior in the current scope
* @returns {Object} Current scope context
*/
@ -400,172 +616,8 @@ var DDE = (() => {
return Reflect.deleteProperty(obj, key);
}
// src/events-observer.js
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get() {
return () => {
};
}
});
function connectionsChangesObserverConstructor() {
const store = /* @__PURE__ */ new Map();
let is_observing = false;
const observerListener = (stop2) => function(mutations) {
for (const mutation of mutations) {
if (mutation.type !== "childList") continue;
if (observerAdded(mutation.addedNodes, true)) {
stop2();
continue;
}
if (observerRemoved(mutation.removedNodes, true))
stop2();
}
};
const observer = new enviroment.M(observerListener(stop));
return {
/**
* Creates an observer for a specific element
* @param {Element} element - Element to observe
* @returns {Function} Cleanup function
*/
observe(element) {
const o = new enviroment.M(observerListener(() => {
}));
o.observe(element, { childList: true, subtree: true });
return () => o.disconnect();
},
/**
* Register a connection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for connection event
*/
onConnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.connected.has(listener)) return;
listeners.connected.add(listener);
listeners.length_c += 1;
},
/**
* Unregister a connection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offConnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
if (!ls.connected.has(listener)) return;
ls.connected.delete(listener);
ls.length_c -= 1;
cleanWhenOff(element, ls);
},
/**
* Register a disconnection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for disconnection event
*/
onDisconnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.disconnected.has(listener)) return;
listeners.disconnected.add(listener);
listeners.length_d += 1;
},
/**
* Unregister a disconnection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offDisconnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
ls.disconnected.delete(listener);
ls.length_d -= 1;
cleanWhenOff(element, ls);
}
};
function cleanWhenOff(element, ls) {
if (ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
function getElementStore(element) {
if (store.has(element)) return store.get(element);
const out = {
connected: /* @__PURE__ */ new WeakSet(),
length_c: 0,
disconnected: /* @__PURE__ */ new WeakSet(),
length_d: 0
};
store.set(element, out);
return out;
}
function start() {
if (is_observing) return;
is_observing = true;
observer.observe(enviroment.D.body, { childList: true, subtree: true });
}
function stop() {
if (!is_observing || store.size) return;
is_observing = false;
observer.disconnect();
}
function requestIdle() {
return new Promise(function(resolve) {
(requestIdleCallback || requestAnimationFrame)(resolve);
});
}
async function collectChildren(element) {
if (store.size > 30)
await requestIdle();
const out = [];
if (!isInstance(element, Node)) return out;
for (const el of store.keys()) {
if (el === element || !isInstance(el, Node)) continue;
if (element.contains(el))
out.push(el);
}
return out;
}
function observerAdded(addedNodes, is_root) {
let out = false;
for (const element of addedNodes) {
if (is_root) collectChildren(element).then(observerAdded);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_c) continue;
element.dispatchEvent(new Event(evc));
ls.connected = /* @__PURE__ */ new WeakSet();
ls.length_c = 0;
if (!ls.length_d) store.delete(element);
out = true;
}
return out;
}
function observerRemoved(removedNodes, is_root) {
let out = false;
for (const element of removedNodes) {
if (is_root) collectChildren(element).then(observerRemoved);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_d) continue;
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
out = true;
}
return out;
}
function dispatchRemove(element) {
return () => {
if (element.isConnected) return;
element.dispatchEvent(new Event(evd));
store.delete(element);
};
}
}
// src/customElement.js
function customElementRender(target, render, props = observedAttributes2) {
function customElementRender(target, render, props = {}) {
const custom_element = target.host || target;
scope.push({
scope: custom_element,
@ -606,81 +658,40 @@ var DDE = (() => {
obj[method] = new Proxy(obj[method] || (() => {
}), { apply });
}
function observedAttributes2(instance) {
return observedAttributes(instance, (i, n) => i.getAttribute(n));
}
// src/events.js
function dispatchEvent(name, options, host) {
if (typeof options === "function") {
host = options;
options = null;
}
if (!options) options = {};
return function dispatch(element, ...d) {
if (host) {
d.unshift(element);
element = typeof host === "function" ? host() : host;
}
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
return element.dispatchEvent(event);
};
// src/memo.js
var memoMark = "__dde_memo";
var memo_scope = [];
function memo(key, generator) {
if (!memo_scope.length) return generator(key);
const k = typeof key === "object" ? JSON.stringify(key) : key;
const [{ cache, after }] = memo_scope;
return after(k, hasOwn(cache, k) ? cache[k] : generator(key));
}
function on(event, listener, options) {
return function registerElement(element) {
element.addEventListener(event, listener, options);
return element;
};
}
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
on.connected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evc, listener, options);
if (element[keyLTE]) return element;
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
if (c) c_ch_o.onConnected(element, listener);
return element;
};
memo.isScope = function(obj) {
return obj[memoMark];
};
on.disconnected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evd, listener, options);
if (element[keyLTE]) return element;
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
if (c) c_ch_o.onDisconnected(element, listener);
return element;
};
};
var store_abort = /* @__PURE__ */ new WeakMap();
on.disconnectedAsAbort = function(host) {
if (store_abort.has(host)) return store_abort.get(host);
const a = new AbortController();
store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a.signal;
};
var els_attribute_store = /* @__PURE__ */ new WeakSet();
on.attributeChanged = function(listener, options) {
if (typeof options !== "object")
options = {};
return function registerElement(element) {
element.addEventListener(eva, listener, options);
if (element[keyLTE] || els_attribute_store.has(element))
return element;
if (!enviroment.M) return element;
const observer = new enviroment.M(function(mutations) {
for (const { attributeName, target } of mutations)
target.dispatchEvent(
new CustomEvent(eva, { detail: [attributeName, target.getAttribute(attributeName)] })
);
memo.scope = function memoScope(fun, { signal, onlyLast } = {}) {
let cache = oCreate();
function memoScope2(...args) {
if (signal && signal.aborted)
return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate();
memo_scope.unshift({
cache,
after(key, val) {
return cache_local[key] = val;
}
});
const c = onAbort(options.signal, () => observer.disconnect());
if (c) observer.observe(element, { attributes: true });
return element;
};
const out = fun.apply(this, args);
memo_scope.shift();
cache = cache_local;
return out;
}
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal) signal.addEventListener("abort", memoScope2.clear);
return memoScope2;
};
return __toCommonJS(index_exports);
})();

80
dist/iife.min.d.ts vendored
View File

@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean;
};
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal {
_: Symbol;
/**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li)));
* ```
* */
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
}
declare const signal: signal;
@ -79,10 +80,11 @@ export type CustomElementTagNameMap = {
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global {
type ddeComponentAttributes = Record<any, any> | undefined;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
type ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>;
}
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = {
/**
@ -144,6 +146,7 @@ export namespace el {
host?: "this" | "parentElement";
}, is_open?: boolean): Comment;
}
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
export function el<A extends {
textContent: ddeStringable;
@ -156,7 +159,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
/** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
/**
@ -165,9 +167,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
* @param body Body of the custom element
* */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
@ -185,7 +187,7 @@ export interface On {
export const on: On;
export type Scope = {
scope: Node | Function | Object;
host: ddeElementAddon<any>;
host: Host<SupportedElement>;
custom_element: false | HTMLElement;
prevent: boolean;
};
@ -201,7 +203,11 @@ export const scope: {
* It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`.
* */
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
host: Host<SupportedElement>;
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
signal: AbortSignal;
state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
@ -213,7 +219,6 @@ export const scope: {
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
/**
* This is used primarly for server side rendering. To be sure that all async operations
* are finished before the page is sent to the client.
@ -234,6 +239,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
* ```
* */
export function queue(promise?: Promise<unknown>): Promise<unknown>;
/**
* Memoization utility for caching DOM elements to improve performance.
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
*
* @param key - Unique identifier for the element (usually an ID or unique value)
* @param generator - Function that creates the element
* @returns The cached element if the key exists, otherwise the result of the generator function
*
* @example
* ```ts
* // Within S.el for list rendering
* S.el(itemsSignal, (items, memo) =>
* el("ul").append(
* ...items.map(item =>
* memo(item.id, () => el(ItemComponent, item))
* )
* )
* )
* ```
*/
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
/**
* Memo namespace containing utility functions for memoization.
*/
export namespace memo {
/**
* Checks if an object is a memo scope.
* @param obj - The object to check
* @returns True if the object is a memo scope
*/
export function isScope(obj: any): boolean;
/**
* Creates a memoized function with optional cleanup support.
*
* @param fun - The function to memoize
* @param options - Configuration options
* @param options.signal - AbortSignal for cleanup
* @param options.onlyLast - When true, only keeps the cache from the most recent call
* @returns A memoized version of the function with a .clear() method
*
* @example
* ```ts
* const renderItems = memo.scope(function(items) {
* return items.map(item =>
* memo(item.id, () => el("div", item.name))
* );
* }, {
* signal: controller.signal,
* onlyLast: true
* });
* ```
*/
export function scope<F extends Function>(fun: F, options?: {
signal?: AbortSignal;
onlyLast?: boolean;
}): F & {
clear: () => void;
};
}
/* TypeScript MEH */
declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;

2
dist/iife.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -189,6 +189,7 @@ import { el } from "deka-dom-el";
* */
export function code({ id, src, content, language= "js", className= host.slice(1), page_id }){
if(src) content= s.cat(src);
content= normalizeIndentation(content);
let dataJS;
if(page_id){
registerClientPart(page_id);
@ -198,6 +199,10 @@ export function code({ id, src, content, language= "js", className= host.slice(1
el("code", { className: "language-"+language, textContent: content.trim() })
);
}
export function pre({ content }){
content= normalizeIndentation(content);
return el("pre").append(el("code", content.trim()));
}
let is_registered= {};
/** @param {string} page_id */
function registerClientPart(page_id){
@ -207,33 +212,6 @@ function registerClientPart(page_id){
document.head.append(
// Use a newer version of Shiki with better performance
el("script", { src: "https://cdn.jsdelivr.net/npm/shiki@0.14.3/dist/index.unpkg.iife.js", defer: true }),
// Make sure we can match Flems styling in dark/light mode
el("style", `
/* Ensure CodeMirror and Shiki use the same font */
.CodeMirror *, .shiki * {
font-family: var(--font-mono) !important;
}
/* Style Shiki's output to match our theme */
.shiki {
background-color: var(--shiki-color-background) !important;
color: var(--shiki-color-text) !important;
padding: 1rem;
border-radius: var(--border-radius);
tab-size: 2;
}
/* Ensure Shiki code tokens use our CSS variables */
.shiki .keyword { color: var(--shiki-token-keyword) !important; }
.shiki .constant { color: var(--shiki-token-constant) !important; }
.shiki .string { color: var(--shiki-token-string) !important; }
.shiki .comment { color: var(--shiki-token-comment) !important; }
.shiki .function { color: var(--shiki-token-function) !important; }
.shiki .operator, .shiki .punctuation { color: var(--shiki-token-punctuation) !important; }
.shiki .parameter { color: var(--shiki-token-parameter) !important; }
.shiki .variable { color: var(--shiki-token-variable) !important; }
.shiki .property { color: var(--shiki-token-property) !important; }
`),
);
registerClientFile(
@ -245,3 +223,9 @@ function registerClientPart(page_id){
is_registered[page_id]= true;
}
/** @param {string} src */
function normalizeIndentation(src){
const lines= src.split("\n");
const min_indent= Math.min(...lines.map(line=> line.search(/\S/)).filter(i=> i >= 0));
return lines.map(line=> line.slice(min_indent)).join("\n");
}

View File

@ -96,6 +96,18 @@ html[data-theme="light"] .cm-s-material .cm-error { color: #f44336 !important; }
max-width: 100% !important;
}
}
${host}[data-variant=big]{
height: 100vh;
main {
flex-flow: column nowrap;
flex-grow: 1;
}
main > * {
width: 100%;
max-width: 100% !important;
}
}
`;
const dde_content= s.cat(new URL("../../dist/esm-with-signals.js", import.meta.url)).toString();
@ -108,15 +120,16 @@ import { relative } from "node:path";
* @param {object} attrs
* @param {URL} attrs.src Example code file path
* @param {"js"|"ts"|"html"|"css"} [attrs.language="js"] Language of the code
* @param {"normal"|"big"} [attrs.variant="normal"] Size of the example
* @param {string} attrs.page_id ID of the page
* */
export function example({ src, language= "js", page_id }){
export function example({ src, language= "js", variant= "normal", page_id }){
registerClientPart(page_id);
const content= s.cat(src).toString()
.replaceAll(/ from "deka-dom-el(\/signals)?";/g, ' from "./esm-with-signals.js";');
const id= "code-example-"+generateCodeId(src);
return el().append(
el(code, { id, content, language, className: example.name }),
el(code, { id, content, language, className: example.name }, el=> el.dataset.variant= variant),
elCode({ id, content, extension: "."+language })
);
}

View File

@ -8,7 +8,8 @@ export class HTMLCustomElement extends HTMLElement{
connectedCallback(){
customElementRender(
this.attachShadow({ mode: "open" }),
ddeComponent
ddeComponent,
this
);
}
set attr(value){ this.setAttribute("attr", value); }

View File

@ -2,7 +2,6 @@
import {
customElementRender,
customElementWithDDE,
observedAttributes,
} from "deka-dom-el";
/** @type {ddePublicElementTagNameMap} */
import { S } from "deka-dom-el/signals";

View File

@ -9,7 +9,7 @@ export class HTMLCustomElement extends HTMLElement{
// nice place to render custom element
}
attributeChangedCallback(name, oldValue, newValue){
// listen to attribute changes (see `observedAttributes`)
// listen to attribute changes (see `S.observedAttributes`)
}
disconnectedCallback(){
// nice place to clean up

View File

@ -1,7 +1,6 @@
import {
customElementRender,
customElementWithDDE,
observedAttributes,
el, on, scope,
} from "deka-dom-el";
import { S } from "deka-dom-el/signals";
@ -9,7 +8,6 @@ export class HTMLCustomElement extends HTMLElement{
static tagName= "custom-element";
static observedAttributes= [ "attr" ];
connectedCallback(){
console.log(observedAttributes(this));
customElementRender(
this.attachShadow({ mode: "open" }),
ddeComponent,

View File

@ -1,4 +1,4 @@
// Example of reactive element marker
<!--<dde:mark type=\"reactive\" source=\"...\">-->
<!--<dde:mark type="reactive" source="...">-->
<!-- content that updates when signal changes -->
<!--</dde:mark>-->

View File

@ -24,7 +24,11 @@ document.body.append(
);
import { chainableAppend } from "deka-dom-el";
/** @param {keyof HTMLElementTagNameMap} tag */
/**
* @template {keyof HTMLElementTagNameMap} TAG
* @param {TAG} tag
* @returns {ddeHTMLElementTagNameMap[TAG] extends HTMLElement ? ddeHTMLElementTagNameMap[TAG] : ddeHTMLElement}
* */
const createElement= tag=> chainableAppend(document.createElement(tag));
document.body.append(
createElement("p").append(

View File

@ -8,12 +8,12 @@ button.disabled = true;
const button2 = Object.assign(
document.createElement('button'),
{
textContent: "Click me",
className: "primary",
disabled: true
textContent: "Click me",
className: "primary",
disabled: true
}
);
// Add to DOM
document.body.appendChild(button);
document.body.appendChild(button2);
document.body.append(button);
document.body.append(button2);

View File

@ -2,14 +2,14 @@
const div = document.createElement('div');
const h1 = document.createElement('h1');
h1.textContent = 'Title';
div.appendChild(h1);
div.append(h1);
const p = document.createElement('p');
p.textContent = 'Paragraph';
div.appendChild(p);
div.append(p);
// appendChild doesn't return parent
// append doesn't return parent
// so chaining is not possible
// Add to DOM
document.body.appendChild(div);
document.body.append(div);

View File

@ -0,0 +1,19 @@
import { el, on, dispatchEvent, scope } from "deka-dom-el";
document.body.append(
el(component),
);
function component(){
const { host }= scope;
const dispatchExample= dispatchEvent(
"example",
{ bubbles: true },
host
);
return el("div").append(
el("p", "Dispatch events from outside of the component."),
el("button", { textContent: "Dispatch", type: "button" },
on("click", dispatchExample))
);
}

View File

@ -1,9 +1,9 @@
import { el, on } from "deka-dom-el";
const paragraph= el("p", "See live-cycle events in console.",
const paragraph= el("p", "See lifecycle events in console.",
el=> log({ type: "dde:created", detail: el }),
on.connected(log),
on.disconnected(log),
on.attributeChanged(log));
);
document.body.append(
paragraph,

View File

@ -6,9 +6,8 @@ let count = 0;
button.addEventListener('click', () => {
count++;
document.querySelector('p').textContent =
'Clicked ' + count + ' times';
'Clicked ' + count + ' times';
if (count > 10) {
button.disabled = true;
}
if (count > 10)
button.disabled = true;
});

View File

@ -1,4 +1,4 @@
import { el, on } from "deka-dom-el";
import { el } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
// A HelloWorld component using the 3PS pattern
@ -27,4 +27,4 @@ function HelloWorld({ emoji = "🚀" }) {
// Use the component in your app
document.body.append(
el(HelloWorld, { emoji: "🎉" })
);
);

View File

@ -15,15 +15,15 @@ function HelloWorldComponent({ initial }){
return el().append(
el("p", {
textContent: S(() => `Hello World ${emoji().repeat(clicks())}`),
textContent: S(() => `Hello World ${emoji.get().repeat(clicks.get())}`),
className: "example",
ariaLive: "polite", //OR ariaset: { live: "polite" },
dataset: { example: "Example" }, //OR dataExample: "Example",
}),
el("button",
{ textContent: "Fire", type: "button" },
on("click", ()=> clicks(clicks() + 1)),
on("keyup", ()=> clicks(clicks() - 2)),
on("click", ()=> clicks.set(clicks.get() + 1)),
on("keyup", ()=> clicks.set(clicks.get() - 2)),
),
el("select", null, onChange).append(
el(OptionComponent, "🎉", isSelected),//OR { textContent: "🎉" }

View File

@ -0,0 +1,2 @@
// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js
import { memo } from "deka-dom-el";

View File

@ -0,0 +1,115 @@
// Example of how memoization improves performance with list rendering
import { el, on, memo } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
// A utility to log element creation
function logCreation(name) {
console.log(`Creating ${name} element`);
return name;
}
// Create a signal with our items
const itemsSignal = S([
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
{ id: 3, name: "Item 3" }
], {
add() {
const { length }= this.value;
this.value.push({
id: length + 1,
name: `Item ${length + 1}`
});
},
force(){},
});
// Without memoization - creates new elements on every render
function withoutMemo() {
return el("div").append(
el("h3", "Without Memoization (check console for element creation)"),
el("p", "Elements are recreated on every render"),
S.el(itemsSignal, items =>
el("ul").append(
...items.map(item =>
el("li").append(
el("span", logCreation(item.name))
)
)
)
),
);
}
// With memoization - reuses elements when possible
function withMemo() {
return el("div").append(
el("h3", "With Memoization (check console for element creation)"),
el("p", "Elements are reused when the key (item.id) stays the same"),
S.el(itemsSignal, items =>
el("ul").append(
...items.map(item =>
// Use item.id as a stable key for memoization
memo(item.id, () =>
el("li").append(
el("span", logCreation(item.name))
)
)
)
)
),
);
}
// Using memo.scope for a custom memoized function
const renderMemoList = memo.scope(function(items) {
return el("ul").append(
...items.map(item =>
memo(item.id, () =>
el("li").append(
el("span", logCreation(`Custom memo: ${item.name}`))
)
)
)
);
});
function withCustomMemo() {
return el("div").append(
el("h3", "With Custom Memo Function"),
el("p", "Using memo.scope to create a memoized rendering function"),
S.el(itemsSignal, items =>
renderMemoList(items)
),
el("button", "Clear Cache",
on("click", () => {
renderMemoList.clear();
S.action(itemsSignal, "force");
}
)
)
);
}
// Demo component showing the difference
export function MemoDemo() {
return el("div", { style: "padding: 1em; border: 1px solid #ccc;" }).append(
el("h2", "Memoization Demo"),
el("p", "See in the console when elements are created."),
el("p").append(`
Notice that without memoization, elements are recreated on every render. With memoization,
only new elements are created.
`),
el("button", "Add Item",
on("click", () => S.action(itemsSignal, "add"))
),
el("div", { style: "display: flex; gap: 2em; margin-top: 1em;" }).append(
withoutMemo(),
withMemo(),
withCustomMemo()
)
);
}
document.body.append(el(MemoDemo));

View File

@ -0,0 +1,386 @@
import { dispatchEvent, el, memo, on, scope } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
/**
* Main TodoMVC application component
*
* Creates and manages the TodoMVC application with the following features:
* - Todo items management (add, edit, delete)
* - Filtering by status (all, active, completed)
* - Client-side routing via URL hash
* - Persistent storage with localStorage
*
* @returns {HTMLElement} The root TodoMVC application element
*/
function Todos(){
const pageS = routerSignal(S);
const todosS = todosSignal();
/** Derived signal that filters todos based on current route */
const filteredTodosS = S(()=> {
const todos = todosS.get();
const filter = pageS.get();
return todos.filter(todo => {
if (filter === "active") return !todo.completed;
if (filter === "completed") return todo.completed;
return true; // "all"
});
});
// Setup hash change listener
window.addEventListener("hashchange", () => {
const hash = location.hash.replace("#", "") || "all";
S.action(pageS, "set", /** @type {"all"|"active"|"completed"} */(hash));
});
/** @type {ddeElementAddon<HTMLInputElement>} */
const onToggleAll = on("change", event => {
const checked = /** @type {HTMLInputElement} */ (event.target).checked;
S.action(todosS, "completeAll", checked);
});
/** @type {ddeElementAddon<HTMLFormElement>} */
const onSubmitNewTodo = on("submit", event => {
event.preventDefault();
const input = /** @type {HTMLInputElement} */(
/** @type {HTMLFormElement} */(event.target).elements.namedItem("newTodo")
);
const title = input.value.trim();
if (title) {
S.action(todosS, "add", title);
input.value = "";
}
});
const onClearCompleted = on("click", () => S.action(todosS, "clearCompleted"));
const onDelete = on("todo:delete", ev =>
S.action(todosS, "delete", /** @type {{ detail: Todo["id"] }} */(ev).detail));
const onEdit = on("todo:edit", ev =>
S.action(todosS, "edit", /** @type {{ detail: Partial<Todo> & { id: Todo["id"] } }} */(ev).detail));
return el("section", { className: "todoapp" }).append(
el("header", { className: "header" }).append(
el("h1", "todos"),
el("form", null, onSubmitNewTodo).append(
el("input", {
className: "new-todo",
name: "newTodo",
placeholder: "What needs to be done?",
autocomplete: "off",
autofocus: true
})
)
),
S.el(todosS, todos => todos.length
? el("main", { className: "main" }).append(
el("input", {
id: "toggle-all",
className: "toggle-all",
type: "checkbox"
}, onToggleAll),
el("label", { htmlFor: "toggle-all", title: "Mark all as complete" }),
el("ul", { className: "todo-list" }).append(
S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
)
)
)
: el()
),
S.el(todosS, todos => memo(todos.length, length=> length
? el("footer", { className: "footer" }).append(
el("span", { className: "todo-count" }).append(
S.el(S(() => todosS.get().filter(todo => !todo.completed).length),
length=> el("strong").append(
length + " ",
length === 1 ? "item left" : "items left"
)
)
),
el("ul", { className: "filters" }).append(
el("li").append(
el("a", {
textContent: "All",
className: S(()=> pageS.get() === "all" ? "selected" : ""),
href: "#"
}),
),
el("li").append(
el("a", {
textContent: "Active",
className: S(()=> pageS.get() === "active" ? "selected" : ""),
href: "#active"
}),
),
el("li").append(
el("a", {
textContent: "Completed",
className: S(()=> pageS.get() === "completed" ? "selected" : ""),
href: "#completed"
}),
)
),
S.el(S(() => todosS.get().some(todo => todo.completed)),
hasTodosCompleted=> hasTodosCompleted
? el("button", { textContent: "Clear completed", className: "clear-completed" }, onClearCompleted)
: el()
)
)
: el()
))
);
}
/**
* Todo item data structure
* @typedef {{ title: string, id: string, completed: boolean }} Todo
*/
/**
* Component for rendering an individual todo item
*
* Features:
* - Display todo with completed state
* - Toggle completion status
* - Delete todo
* - Edit todo with double-click
* - Cancel edit with Escape key
*
* @param {Todo} todo - The todo item data
* @fires {void} todo:delete - todo deletion event
* @fires {Partial<Todo>} todo:edit - todo edits event
*/
function TodoItem({ id, title, completed }) {
const { host }= scope;
const isEditing = S(false);
const isCompleted = S(completed);
/** @type {(id: string) => void} Dispatch function for deleting todo */
const dispatchDelete= dispatchEvent("todo:delete", host);
/** @type {(data: {id: string, [key: string]: any}) => void} Dispatch function for editing todo */
const dispatchEdit = dispatchEvent("todo:edit", host);
/** @type {ddeElementAddon<HTMLInputElement>} */
const onToggleCompleted = on("change", (ev) => {
const completed= /** @type {HTMLInputElement} */(ev.target).checked;
isCompleted.set(completed);
dispatchEdit({ id, completed });
});
/** @type {ddeElementAddon<HTMLButtonElement>} */
const onDelete = on("click", () => dispatchDelete(id));
/** @type {ddeElementAddon<HTMLLabelElement>} */
const onStartEdit = on("dblclick", () => isEditing.set(true));
/** @type {ddeElementAddon<HTMLInputElement>} */
const onBlurEdit = on("blur", event => {
const value = /** @type {HTMLInputElement} */(event.target).value.trim();
if (value) {
dispatchEdit({ id, title: value });
} else {
dispatchDelete(id);
}
isEditing.set(false);
});
/** @type {ddeElementAddon<HTMLFormElement>} */
const onSubmitEdit = on("submit", event => {
event.preventDefault();
const input = /** @type {HTMLFormElement} */(event.target).elements.namedItem("edit");
const value = /** @type {HTMLInputElement} */(input).value.trim();
if (value) {
dispatchEdit({ id, title: value });
} else {
dispatchDelete(id);
}
isEditing.set(false);
});
/**
* Event handler for keyboard events in edit mode
* @type {ddeElementAddon<HTMLInputElement>}
*/
const onKeyDown = on("keydown", event => {
if (event.key !== "Escape") return;
isEditing.set(false);
});
return el("li", { classList: { completed: isCompleted, editing: isEditing } }).append(
el("div", { className: "view" }).append(
el("input", {
className: "toggle",
type: "checkbox",
checked: completed
}, onToggleCompleted),
el("label", { textContent: title }, onStartEdit),
el("button", { className: "destroy" }, onDelete)
),
S.el(isEditing, editing => editing
? el("form", null, onSubmitEdit).append(
el("input", {
className: "edit",
name: "edit",
value: title,
"data-id": id
}, onBlurEdit, onKeyDown, addFocus)
)
: el()
)
);
}
// Set up the document head
document.head.append(
el("title", "TodoMVC: dd<el>"),
el("meta", { name: "description", content: "A TodoMVC implementation using dd<el>." }),
el("link", {
rel: "stylesheet",
href: "https://cdn.jsdelivr.net/npm/todomvc-common@1.0.5/base.css"
}),
el("link", {
rel: "stylesheet",
href: "https://cdn.jsdelivr.net/npm/todomvc-app-css@2.4.2/index.css"
})
);
// Set up the document body
document.body.append(
el(Todos),
el("footer", { className: "info" }).append(
el("p", "Double-click to edit a todo"),
el("p").append(
"Created with ",
el("a", { textContent: "deka-dom-el", href: "https://github.com/jaandrle/deka-dom-el" })
),
el("p").append(
"Part of ",
el("a", { textContent: "TodoMVC", href: "http://todomvc.com" })
)
)
);
/**
* Utility function to set focus on an input element
* Uses requestAnimationFrame to ensure the element is rendered
* before trying to focus it
*
* @param {HTMLInputElement} editInput - The input element to focus
* @returns {number} The requestAnimationFrame ID
*/
function addFocus(editInput){
return requestAnimationFrame(()=> {
editInput.focus();
editInput.selectionStart = editInput.selectionEnd = editInput.value.length;
});
}
/**
* Creates a signal for managing todos with persistence
*
* Features:
* - Loads todos from localStorage on initialization
* - Automatically saves todos to localStorage on changes
* - Provides actions for adding, editing, deleting todos
*/
function todosSignal(){
const store_key = "dde-todos";
// Try to load todos from localStorage
let savedTodos = [];
try {
const stored = localStorage.getItem(store_key);
if (stored) {
savedTodos = JSON.parse(stored);
}
} catch (_) {}
const out= S(/** @type {Todo[]} */(savedTodos || []), {
/**
* Add a new todo
* @param {string} value - The title of the new todo
*/
add(value){
this.value.push({
completed: false,
title: value,
id: uuid(),
});
},
/**
* Edit an existing todo
* @param {{ id: string, [key: string]: any }} data - Object containing id and fields to update
*/
edit({ id, ...update }){
const index = this.value.findIndex(t => t.id === id);
if (index === -1) return this.stopPropagation();
Object.assign(this.value[index], update);
},
/**
* Delete a todo by id
* @param {string} id - The id of the todo to delete
*/
delete(id){
const index = this.value.findIndex(t => t.id === id);
if (index === -1) return this.stopPropagation();
this.value.splice(index, 1);
},
/**
* Remove all completed todos
*/
clearCompleted() {
this.value = this.value.filter(todo => !todo.completed);
},
completeAll(state= true) {
this.value.forEach(todo => todo.completed = state);
},
/**
* Handle cleanup when signal is cleared
*/
[S.symbols.onclear](){
}
});
/**
* Save todos to localStorage whenever the signal changes
* @param {Todo[]} value - Current todos array
*/
S.on(out, /** @param {Todo[]} value */ function saveTodos(value) {
try {
localStorage.setItem(store_key, JSON.stringify(value));
} catch (e) {
console.error("Failed to save todos to localStorage", e);
}
});
return out;
}
/**
* Creates a signal for managing route state
*
* @param {typeof S} signal - The signal constructor
*/
function routerSignal(signal){
const initial = location.hash.replace("#", "") || "all";
return signal(initial, {
/**
* Set the current route
* @param {"all"|"active"|"completed"} hash - The route to set
*/
set(hash){
location.hash = hash;
this.value = hash;
}
});
}
/**
* Generates a RFC4122 version 4 compliant UUID
* Used to create unique identifiers for todo items
*
* @returns {string} A randomly generated UUID
*/
function uuid() {
let uuid = "";
for (let i = 0; i < 32; i++) {
let random = (Math.random() * 16) | 0;
if (i === 8 || i === 12 || i === 16 || i === 20)
uuid += "-";
uuid += (i === 12 ? 4 : i === 16 ? (random & 3) | 8 : random).toString(16);
}
return uuid;
}

View File

@ -14,7 +14,7 @@ function component(){
const textContent= S("Click to change text.");
const onclickChange= on("click", function redispatch(){
textContent("Text changed! "+(new Date()).toString())
textContent.set("Text changed! "+(new Date()).toString())
});
return el("p", textContent, onclickChange);
}

View File

@ -1,14 +0,0 @@
import { scope } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
function customSignalLogic() {
// Create an isolated scope for a specific operation
scope.push(); // Start new scope
// These signals are in the new scope
const isolatedCount = S(0);
const isolatedDerived = S(() => isolatedCount.get() * 2);
// Clean up by returning to previous scope
scope.pop();
}

View File

@ -16,7 +16,9 @@ function Counter() {
// THE HOST IS PROBABLY DIFFERENT THAN
// YOU EXPECT AND SIGNAL MAY BE
// UNEXPECTEDLY REMOVED!!!
host().querySelector("button").disabled = count.get() >= 10;
S.on(count, (count)=>
host().querySelector("button").disabled = count >= 10
);
};
setTimeout(()=> {
// ok, BUT consider extract to separate function

View File

@ -1,6 +1,5 @@
// Handling async data in SSR
import { JSDOM } from "jsdom";
import { S } from "deka-dom-el/signals";
import { register, queue } from "deka-dom-el/jsdom";
async function renderWithAsyncData() {
@ -8,23 +7,7 @@ async function renderWithAsyncData() {
const { el } = await register(dom);
// Create a component that fetches data
function AsyncComponent() {
const title= S("-");
const description= S("-");
// Use the queue to track the async operation
queue(fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => {
title.set(data.title);
description.set(data.description);
}));
return el("div", { className: "async-content" }).append(
el("h2", title),
el("p", description)
);
}
const { AsyncComponent } = await import("./components/AsyncComponent.js");
// Render the page
dom.window.document.body.append(
@ -41,3 +24,24 @@ async function renderWithAsyncData() {
}
renderWithAsyncData();
// file: components/AsyncComponent.js
import { el } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
function AsyncComponent() {
const title= S("-");
const description= S("-");
// Use the queue to track the async operation
queue(fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => {
title.set(data.title);
description.set(data.description);
}));
return el("div", { className: "async-content" }).append(
el("h2", title),
el("p", description)
);
}

View File

@ -11,6 +11,7 @@ async function renderPage() {
const { el } = await register(dom);
// Create a simple header component
// can be separated into a separate file and use `import { el } from "deka-dom-el"`
function Header({ title }) {
return el("header").append(
el("h1", title),

View File

@ -17,6 +17,7 @@ async function renderPage() {
const { el } = await register(dom);
// 4. Dynamically import page components
// use `import { el } from "deka-dom-el"`
const { Header } = await import("./components/Header.js");
const { Content } = await import("./components/Content.js");

View File

@ -1,6 +1,6 @@
// Basic jsdom integration example
import { JSDOM } from "jsdom";
import { register, unregister, queue } from "deka-dom-el/jsdom.js";
import { register, unregister, queue } from "deka-dom-el/jsdom";
// Create a jsdom instance
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");

View File

@ -12,7 +12,7 @@ async function buildSite() {
];
// Create output directory
mkdirSync("./dist", { recursive: true });
mkdirSync("./dist/docs", { recursive: true });
// Build each page
for (const page of pages) {
@ -23,6 +23,7 @@ async function buildSite() {
const { el } = await register(dom);
// Import the page component
// use `import { el } from "deka-dom-el"`
const { default: PageComponent } = await import(page.component);
// Render the page with its metadata
@ -35,7 +36,7 @@ async function buildSite() {
// Write the HTML to a file
const html = dom.serialize();
writeFileSync(`./dist/${page.id}.html`, html);
writeFileSync(`./dist/docs/${page.id}.html`, html);
console.log(`Built page: ${page.id}.html`);
}

View File

@ -11,10 +11,6 @@ export function mnemonic(){
el("code", "customElementWithDDE(<custom-element>)"),
" — register <custom-element> to DDE library, see also `lifecyclesToEvents`, can be also used as decorator",
),
el("li").append(
el("code", "observedAttributes(<custom-element>)"),
" — returns record of observed attributes (keys uses camelCase)",
),
el("li").append(
el("code", "S.observedAttributes(<custom-element>)"),
" — returns record of observed attributes (keys uses camelCase and values are signals)",
@ -32,4 +28,4 @@ export function mnemonic(){
" — simulate slots for “dde”/functional components",
),
);
}
}

View File

@ -16,15 +16,16 @@ export function mnemonic(){
el("code", "dispatchEvent(<event>[, <options>])(element)"),
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>]))")
),
el("li").append(
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> }))")
),
el("li").append(
el("code", "dispatchEvent(<event>[, <options>], <host>)([<detail>])"),
" — just ", el("code", "<host>().dispatchEvent(new Event(<event>[, <options>]))"), " or ",
el("code", "<host>().dispatchEvent(new CustomEvent(<event>, { detail: <detail> }[, <options>] ))"),
" (see scopes section of docs)"
),
);
}

View File

@ -0,0 +1,15 @@
import { el } from "deka-dom-el";
import { mnemonicUl } from "../mnemonicUl.html.js";
export function mnemonic(){
return mnemonicUl().append(
el("li").append(
el("code", "memo.scope(<function>, <argument(s)>)"),
" — Scope for memo",
),
el("li").append(
el("code", "memo(<key>, <generator>)"),
" — returns value from memo and/or generates it (and caches it)",
),
);
}

View File

@ -14,6 +14,10 @@ export function mnemonic(){
el("li").append(
el("code", "scope.host(...<addons>)"),
" — use addons to current component",
),
el("li").append(
el("code", "scope.signal"),
" — get AbortSignal that triggers when the element disconnects",
)
);
}

View File

@ -0,0 +1,39 @@
import { styles } from "../ssr.js";
styles.css`
/* Scroll to top button */
.scroll-top-button {
position: fixed;
bottom: 2rem;
left: 2rem;
display: flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 3rem;
border-radius: 50%;
background-color: var(--primary);
color: var(--button-text);
font-size: 1.5rem;
text-decoration: none;
box-shadow: var(--shadow);
transition: background-color 0.2s ease, transform 0.2s ease;
z-index: 1000;
}
.scroll-top-button:hover {
background-color: var(--primary-dark);
transform: translateY(-4px);
text-decoration: none;
}
@media (max-width: 768px) {
.scroll-top-button {
bottom: 0.5rem;
left: unset;
right: .5rem;
width: 2.5rem;
height: 2.5rem;
}
}
`;

View File

@ -0,0 +1,14 @@
import { el } from "deka-dom-el";
export function scrollTop() {
return el("a", {
href: "#",
className: "scroll-top-button",
ariaLabel: "Scroll to top",
textContent: "↑",
onclick: (e) => {
e.preventDefault();
window.scrollTo({ top: 0, behavior: "smooth" });
}
})
}

View File

@ -16,7 +16,6 @@ styles.css`
SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
--body-max-width: 40rem;
--sidebar-width: 20rem;
--header-height: 4rem;
--border-radius: 0.375rem;
@ -73,6 +72,7 @@ styles.css`
html {
scroll-behavior: smooth;
tab-size: var(--tab-size, 2rem);
}
/* Accessibility improvements */
@ -215,8 +215,8 @@ body {
}
@media (min-width: 768px) {
body {
grid-template-rows: var(--header-height) 1fr;
grid-template-columns: var(--sidebar-width) 1fr;
grid-template-rows: auto 1fr;
grid-template-columns: auto 1fr;
grid-template-areas:
"header header"
"sidebar content";
@ -250,7 +250,7 @@ h2 {
/* Section headings with better visual hierarchy */
body > main h3, body > main h4 {
scroll-margin-top: calc(var(--header-height) + 1rem);
scroll-margin-top: 1rem;
}
/* Make clickable heading links for better navigation */

View File

@ -36,9 +36,9 @@ export function page({ pkg, info }){
el("h4", t`What Makes dd<el> Special`),
el("ul").append(
el("li", t`No build step required — use directly in the browser`),
el("li", t`Lightweight core (~1015kB minified) with zero dependencies`),
el("li", t`Lightweight core (~1015kB minified) without unnecessary dependencies (0 at now 😇)`),
el("li", t`Natural DOM API — work with real DOM nodes, not abstractions`),
el("li", t`Built-in reactivity with powerful signals system`),
el("li", t`Built-in reactivity with simplified but powerful signals system`),
el("li", t`Clean code organization with the 3PS pattern`)
)
),
@ -67,7 +67,7 @@ export function page({ pkg, info }){
`),
el("ol").append(
el("li").append(...T`
${el("strong", "Create State")}: Define your application's reactive data using signals
${el("strong", "Create State")}: Define your applications reactive data using signals
`),
el("li").append(...T`
${el("strong", "Bind to Elements")}: Define how UI elements react to state changes
@ -87,30 +87,44 @@ export function page({ pkg, info }){
el("div", { className: "note" }).append(
el("p").append(...T`
The 3PS pattern becomes especially powerful when combined with components, allowing you to create
reusable pieces of UI with encapsulated state and behavior. You'll learn more about this in the
reusable pieces of UI with encapsulated state and behavior. Youll learn more about this in the
following sections.
`)
),
el(h3, t`How to Use This Documentation`),
el("p").append(...T`
This guide will take you through dd<el>'s features step by step:
This guide will take you through dd<el>s features step by step:
`),
el("ol").append(
el("li").append(...T`${el("strong", "Elements")} — Creating and manipulating DOM elements`),
el("li").append(...T`${el("strong", "Events")} — Handling user interactions and lifecycle events`),
el("li").append(...T`${el("strong", "Signals")} — Adding reactivity to your UI`),
el("li").append(...T`${el("strong", "Scopes")} — Managing component lifecycles`),
el("li").append(...T`${el("strong", "Custom Elements")} — Building web components`),
el("li").append(...T`${el("strong", "Debugging")} — Tools to help you build and fix your apps`),
el("li").append(...T`${el("strong", "Extensions")} — Integrating third-party functionalities`),
el("li").append(...T`${el("strong", "Ireland Components")}
Creating interactive demos with server-side pre-rendering`),
el("li").append(...T`${el("strong", "SSR")} — Server-side rendering with dd<el>`)
el("ol", { start: 2 }).append(
el("li").append(...T`${el("a", { href: "p02-elements.html" }).append(el("strong", "Elements"))} — Creating
and manipulating DOM elements`),
el("li").append(...T`${el("a", { href: "p03-events.html" }).append(el("strong", "Events and Addons"))} —
Handling user interactions and lifecycle events`),
el("li").append(...T`${el("a", { href: "p04-signals.html" }).append(el("strong", "Signals"))} — Adding
reactivity to your UI`),
el("li").append(...T`${el("a", { href: "p05-scopes.html" }).append(el("strong", "Scopes"))} — Managing
component lifecycles`),
el("li").append(...T`${el("a", { href: "p06-customElement.html" }).append(el("strong", "Web Components"))} —
Building native custom elements`),
el("li").append(...T`${el("a", { href: "p07-debugging.html" }).append(el("strong", "Debugging"))} — Tools to
help you build and fix your apps`),
el("li").append(...T`${el("a", { href: "p08-extensions.html" }).append(el("strong", "Extensions"))} —
Integrating third-party functionalities`),
el("li").append(...T`${el("a", { href: "p09-optimization.html" })
.append(el("strong", "Performance Optimization"))} Techniques for optimizing your applications`),
el("li").append(...T`${el("a", { href: "p10-todomvc.html" }).append(el("strong", "TodoMVC"))} — A real-world
application implementation`),
el("li").append(...T`${el("a", { href: "p11-ssr.html" }).append(el("strong", "SSR"))} — Server-side
rendering with dd<el>`),
el("li").append(...T`${el("a", { href: "p12-ireland.html" }).append(el("strong", "Ireland Components"))} —
Interactive demos with server-side pre-rendering`),
el("li").append(...T`${el("a", { href: "p13-appendix.html" }).append(el("strong", "Appendix & Summary"))} —
Comprehensive reference and best practices`),
),
el("p").append(...T`
Each section builds on the previous ones, so we recommend following them in order.
Let's get started with the basics of creating elements!
Lets get started with the basics of creating elements!
`),
);
}

View File

@ -105,13 +105,8 @@ ${host_nav} a .nav-number {
}
@media (max-width: 767px) {
${host_nav} {
padding: 0.75rem;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
border-bottom: 1px solid var(--border);
border-right: none;
flex-flow: row wrap;
justify-content: center;
}
@ -121,14 +116,7 @@ ${host_nav} a .nav-number {
white-space: nowrap;
}
${host_nav} a .nav-number {
width: auto;
margin-right: 0.25rem;
}
${host_nav} a:first-child {
margin-bottom: 0;
margin-right: 0.5rem;
min-width: 100%;
justify-content: center;
}

View File

@ -3,6 +3,10 @@ import { el, simulateSlots } from "deka-dom-el";
import { header } from "./head.html.js";
import { prevNext } from "../components/pageUtils.html.js";
import { ireland } from "../components/ireland.html.js";
import "../components/scrollTop.css.js";
/** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url);
/** @param {Pick<import("../types.d.ts").PageAttrs, "pkg" | "info">} attrs */
export function simplePage({ pkg, info }){
@ -26,6 +30,9 @@ export function simplePage({ pkg, info }){
// Navigation between pages
el(prevNext, info)
)
),
// Scroll to top button
el(ireland, { src: fileURL("../components/scrollTop.js.js"), exportName: "scrollTop" })
));
}

View File

@ -54,13 +54,13 @@ export function page({ pkg, info }){
dd<el> provides a simple yet powerful approach to element creation that is declarative, chainable,
and maintains a clean syntax close to HTML structure.
`),
el("div", { class: "callout" }).append(
el("div", { className: "callout" }).append(
el("h4", t`dd<el> Elements: Key Benefits`),
el("ul").append(
el("li", t`Declarative element creation with intuitive property assignment`),
el("li", t`Chainable methods for natural DOM tree construction`),
el("li", t`Simplified component patterns for code reuse`),
el("li", t`Normalized property/attribute handling across browsers`),
el("li", t`Normalized declarative property/attribute handling across browsers`),
el("li", t`Smart element return values for cleaner code flow`)
)
),
@ -71,10 +71,10 @@ export function page({ pkg, info }){
el("p").append(...T`
In standard JavaScript, you create DOM elements using the
${el("a", references.mdn_create).append(el("code", "document.createElement()"))} method
and then set properties individually or with Object.assign():
and then set properties individually or with ${el("code", "Object.assign()")}:
`),
el("div", { class: "illustration" }).append(
el("div", { class: "comparison" }).append(
el("div", { className: "illustration" }).append(
el("div", { className: "comparison" }).append(
el("div").append(
el("h5", t`Native DOM API`),
el(code, { src: fileURL("./components/examples/elements/native-dom-create.js"), page_id })
@ -86,41 +86,45 @@ export function page({ pkg, info }){
)
),
el("p").append(...T`
The ${el("code", "el")} function provides a simple wrapper around ${el("code", "document.createElement")}
The ${el("code", "el")} function provides a simple wrapper around ${el("code", "document.createElement")}
with enhanced property assignment.
`),
el(example, { src: fileURL("./components/examples/elements/dekaCreateElement.js"), page_id }),
el(h3, t`Advanced Property Assignment`),
el("p").append(...T`
The ${el("code", "assign")} function is the heart of dd<el>'s element property handling. It is internally
The ${el("code", "assign")} function is the heart of dd<el>s element property handling. It is internally
used to assign properties using the ${el("code", "el")} function. ${el("code", "assign")} provides
intelligent assignment of both properties (IDL) and attributes:
intelligent assignment of both ${el("a", { textContent: "properties (IDL)", ...references.mdn_idl })}
and attributes:
`),
el("div", { class: "function-table" }).append(
el("div", { className: "function-table" }).append(
el("dl").append(
el("dt", t`Property vs Attribute Priority`),
el("dd", t`Prefers IDL properties, falls back to setAttribute() when no writable property exists`),
el("dt", t`Data and ARIA Attributes`),
el("dd", t`Both dataset.* and data-* syntaxes supported (same for ARIA)`),
el("dd").append(...T`Both ${el("code", "dataset")}.* and ${el("code", "data-")}* syntaxes supported
(same for ${el("em", "ARIA")})`),
el("dt", t`Style Handling`),
el("dd", t`Accepts string or object notation for style property`),
el("dd").append(...T`Accepts string or object notation for ${el("code", "style")} property`),
el("dt", t`Class Management`),
el("dd", t`Works with className, class, or classList object for toggling classes`),
el("dd").append(...T`Works with ${el("code", "className")}, ${el("code", "class")}, or ${el("code",
"classList")} object for toggling classes`),
el("dt", t`Force Modes`),
el("dd", t`Use = prefix to force attribute mode, . prefix to force property mode`),
el("dd").append(...T`Use ${el("code", "=")} prefix to force attribute mode, ${el("code", ".")} prefix to
force property mode`),
el("dt", t`Attribute Removal`),
el("dd", t`Pass undefined to remove a property or attribute`)
el("dd").append(...T`Pass ${el("code", "undefined")} to remove a property or attribute`)
)
),
el(example, { src: fileURL("./components/examples/elements/dekaAssign.js"), page_id }),
el("div", { class: "note" }).append(
el("div", { className: "note" }).append(
el("p").append(...T`
You can explore standard HTML element properties in the MDN documentation for
${el("a", { textContent: "HTMLElement", ...references.mdn_el })} (base class)
@ -131,16 +135,16 @@ export function page({ pkg, info }){
el(h3, t`Building DOM Trees with Chainable Methods`),
el("p").append(...T`
One of the most powerful features of dd<el> is its approach to building element trees.
Unlike the native DOM API which doesn't return the parent after appendChild(), dd<el>'s
append() always returns the parent element:
Unlike the native DOM API which doesnt return the parent after ${el("code", "append()")}, dd<el>s
${el("code", "append()")} always returns the parent element:
`),
el("div", { class: "illustration" }).append(
el("div", { class: "comparison" }).append(
el("div", { class: "bad-practice" }).append(
el("div", { className: "illustration" }).append(
el("div", { className: "comparison" }).append(
el("div", { className: "bad-practice" }).append(
el("h5", t`❌ Native DOM API`),
el(code, { src: fileURL("./components/examples/elements/native-dom-tree.js"), page_id })
),
el("div", { class: "good-practice" }).append(
el("div", { className: "good-practice" }).append(
el("h5", t`✅ dd<el> Approach`),
el(code, { src: fileURL("./components/examples/elements/dde-dom-tree.js"), page_id })
)
@ -148,7 +152,7 @@ export function page({ pkg, info }){
),
el("p").append(...T`
This chainable pattern is much cleaner and easier to follow, especially for deeply nested elements.
It also makes it simple to add multiple children to a parent element in a single fluent expression.
It also makes it simple to add multiple children to a parent element in a single fluent expression.
`),
el(example, { src: fileURL("./components/examples/elements/dekaAppend.js"), page_id }),
@ -162,12 +166,17 @@ export function page({ pkg, info }){
Component functions receive the properties object as their first argument, just like regular elements.
This makes it easy to pass data down to components and create reusable UI fragments.
`),
el("div", { class: "tip" }).append(
el("div", { className: "tip" }).append(
el("p").append(...T`
It's helpful to use naming conventions similar to native DOM elements for your components.
This allows you to use ${el("a", { textContent: "destructuring assignment", ...references.mdn_destruct })}
and keeps your code consistent with the DOM API.
`)
Its helpful to use naming conventions similar to native DOM elements for your components.
This allows you to keeps your code consistent with the DOM API.
`),
el("p").append(...T`
Use ${el("a", { textContent: "destructuring assignment", ...references.mdn_destruct })}
to extract the properties from the ${el("code", "props")} and pass them to the component element:
${el("code", "function component({ className }){ return el(\"p\", { className }); }")} for make
templates cleaner.
`),
),
el(h3, t`Working with SVG and Other Namespaces`),
@ -194,7 +203,7 @@ export function page({ pkg, info }){
from the props object for cleaner component code.
`),
el("li").append(...T`
${el("strong", "Leverage chainable methods for better performance:")} Use chainable methods like
${el("strong", "Leverage chainable methods for better performance:")} Use chainable methods
${el("code", ".append()")} to build complex DOM trees for better performance and cleaner code.
`),
),

View File

@ -36,11 +36,6 @@ const references= {
mdn_mutation: {
href: "https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver",
},
/** Readding the element to the DOM fix by Vue */
vue_fix: {
title: t`Vue and Web Components, lifecycle implementation readding the element to the DOM`,
href: "https://vuejs.org/guide/extras/web-components.html#lifecycle",
}
};
/** @param {import("./types.d.ts").PageAttrs} attrs */
export function page({ pkg, info }){
@ -48,11 +43,11 @@ export function page({ pkg, info }){
return el(simplePage, { info, pkg }).append(
el("p").append(...T`
Events are at the core of interactive web applications. dd<el> provides a clean, declarative approach to
handling DOM events and extends this pattern with a powerful Addon system to incorporate additional
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 dd<el>'s Event System and Addons Matters`),
el("h4", t`Why dd<el>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`),
@ -68,23 +63,23 @@ export function page({ pkg, info }){
el("p").append(...T`
In JavaScript you can listen to native DOM events using
${el("a", references.mdn_listen).append(el("code", "element.addEventListener(type, listener, options)"))}.
dd<el> provides an alternative approach with arguments ordered differently to better fit its declarative
dd<el> 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(code, { content: `element.addEventListener("click", callback, options);`, page_id })
),
el("div", { className: "tab" }).append(
el("h5", t`dd<el> Approach`),
el(code, { content: `on('click', callback, options)(element);`, page_id })
el(code, { content: `on("click", callback, options)(element);`, page_id })
)
)
),
el("p").append(...T`
The main benefit of dd<el>'s approach is that it works as an Addon, making it easy to integrate
The main benefit of dd<el>s approach is that it works as an Addon (see below), making it easy to integrate
directly into element declarations.
`),
el(example, { src: fileURL("./components/examples/events/compare.js"), page_id }),
@ -97,10 +92,14 @@ export function page({ pkg, info }){
`)
),
el(example, { src: fileURL("./components/examples/events/abortSignal.js"), page_id }),
el("p").append(...T`
This is the same for signals (see next section) and works well with scopes and library extendability (
see scopes and extensions section).
`),
el(h3, t`Three Ways to Handle Events`),
el("div", { className: "tabs" }).append(
el("div", { className: "tab", "data-tab": "html-attr" }).append(
el("div", { className: "tab", dataTab: "html-attr" }).append(
el("h4", t`HTML Attribute Style`),
el(code, { src: fileURL("./components/examples/events/attribute-event.js"), page_id }),
el("p").append(...T`
@ -109,25 +108,25 @@ export function page({ pkg, info }){
useful for SSR scenarios.
`)
),
el("div", { className: "tab", "data-tab": "property" }).append(
el("div", { className: "tab", dataTab: "property" }).append(
el("h4", t`Property Assignment`),
el(code, { src: fileURL("./components/examples/events/property-event.js"), page_id }),
el("p", t`Assigns the event handler directly to the element's property.`)
el("p", t`Assigns the event handler directly to the elements property.`)
),
el("div", { className: "tab", "data-tab": "addon" }).append(
el("div", { className: "tab", dataTab: "addon" }).append(
el("h4", t`Addon Approach`),
el(code, { src: fileURL("./components/examples/events/chain-event.js"), page_id }),
el("p", t`Uses the addon pattern, see above.`)
el("p", t`Uses the addon pattern (so adds the event listener to the element), see above.`)
)
),
el("p").append(...T`
For a deeper comparison of these approaches, see
${el("a", { textContent: "WebReflection's detailed analysis", ...references.web_events })}.
${el("a", { textContent: "WebReflections detailed analysis", ...references.web_events })}.
`),
el(h3, t`Understanding Addons`),
el("p").append(...T`
Addons are a powerful pattern in dd<el> that extends beyond just event handling.
Addons are a powerful pattern in dd<el> 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(
@ -152,11 +151,12 @@ export function page({ pkg, info }){
el(h3, t`Lifecycle Events`),
el("p").append(...T`
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.
Addons are called immediately when an element is created, even before its connected to the live DOM.
You can think of an Addon as an "oncreate" event handler.
`),
el("p").append(...T`
dd<el> provides three additional lifecycle events that correspond to custom element lifecycle callbacks:
dd<el> provides two additional lifecycle events that correspond to ${el("a", { textContent:
"custom element", ...references.mdn_customElements })} lifecycle callbacks:
`),
el("div", { className: "function-table" }).append(
el("dl").append(
@ -165,31 +165,30 @@ export function page({ pkg, info }){
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(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), dd<el> uses
${el("a", references.mdn_mutation).append(el("code", "MutationObserver"), " | MDN")}
internally to track lifecycle events.
For regular elements (non-custom elements), dd<el> uses ${el("a",
references.mdn_mutation).append(el("code", "MutationObserver"), " | MDN")} internally to track
lifecycle 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
Always use ${el("code", "on.*")} functions as library must ensure proper (MutationObserver)
registration, not ${el("code", "on('dde:*', ...)")}, even the native event system is used with event
names prefixed with ${el("code", "dde:")}.
`),
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
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
@ -202,11 +201,11 @@ export function page({ pkg, info }){
el(h3, t`Dispatching Custom Events`),
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.
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(example, { src: fileURL("./components/examples/events/compareDispatch.js"), page_id }),
el(code, { src: fileURL("./components/examples/events/dispatch.js"), page_id }),
el(h3, t`Best Practices`),
el("ol").append(
@ -217,7 +216,8 @@ export function page({ pkg, info }){
${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("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

View File

@ -48,7 +48,7 @@ export function page({ pkg, info }){
Signals provide a simple yet powerful way to create reactive applications with dd<el>. They handle the
fundamental challenge of keeping your UI in sync with changing data in a declarative, efficient way.
`),
el("div", { class: "callout" }).append(
el("div", { className: "callout" }).append(
el("h4", t`What Makes Signals Special?`),
el("ul").append(
el("li", t`Fine-grained reactivity without complex state management`),
@ -65,18 +65,18 @@ export function page({ pkg, info }){
Signals organize your code into three distinct parts, following the
${el("a", { textContent: t`3PS principle`, href: "./#h-3ps" })}:
`),
el("div", { class: "signal-diagram" }).append(
el("div", { class: "signal-part" }).append(
el("div", { className: "signal-diagram" }).append(
el("div", { className: "signal-part" }).append(
el("h4", t`PART 1: Create Signal`),
el(code, { content: "const count = S(0);", page_id }),
el("p", t`Define a reactive value that can be observed and changed`)
el("p", t`Define a reactive value that can be observed and changed`)
),
el("div", { class: "signal-part" }).append(
el("div", { className: "signal-part" }).append(
el("h4", t`PART 2: React to Changes`),
el(code, { content: "S.on(count, value => updateUI(value));", page_id }),
el("p", t`Subscribe to signal changes with callbacks or effects`)
),
el("div", { class: "signal-part" }).append(
el("div", { className: "signal-part" }).append(
el("h4", t`PART 3: Update Signal`),
el(code, { content: "count.set(count.get() + 1);", page_id }),
el("p", t`Modify the signal value, which automatically triggers updates`)
@ -84,26 +84,26 @@ export function page({ pkg, info }){
),
el(example, { src: fileURL("./components/examples/signals/signals.js"), page_id }),
el("div", { class: "note" }).append(
el("div", { className: "note" }).append(
el("p").append(...T`
Signals implement the ${el("a", { textContent: t`Publishsubscribe pattern`, ...references.wiki_pubsub })},
a form of ${el("a", { textContent: t`Event-driven programming`, ...references.wiki_event_driven })}.
This architecture allows different parts of your application to stay synchronized through a shared signal,
without direct dependencies on each other. Compare for example with ${el("a", { textContent:
t`fpubsub library`, ...references.fpubsub })}.
Signals implement the ${el("a", { textContent: t`Publishsubscribe pattern`, ...references.wiki_pubsub
})}, a form of ${el("a", { textContent: t`Event-driven programming`, ...references.wiki_event_driven
})}. This architecture allows different parts of your application to stay synchronized through
a shared signal, without direct dependencies on each other. Compare for example with ${el("a", {
textContent: t`fpubsub library`, ...references.fpubsub })}.
`)
),
el(h3, t`Signal Essentials: Core API`),
el("div", { class: "function-table" }).append(
el("div", { className: "function-table" }).append(
el("dl").append(
el("dt", t`Creating a Signal`),
el("dt", t`Creating a Signal`),
el("dd", t`S(initialValue) → creates a signal with the given value`),
el("dt", t`Reading a Signal`),
el("dt", t`Reading a Signal`),
el("dd", t`signal.get() → returns the current value`),
el("dt", t`Writing to a Signal`),
el("dt", t`Writing to a Signal`),
el("dd", t`signal.set(newValue) → updates the value and notifies subscribers`),
el("dt", t`Subscribing to Changes`),
@ -115,51 +115,53 @@ export function page({ pkg, info }){
)
),
el("p").append(...T`
Signals can be created with any type of value, but they work best with
${el("a", { textContent: t`primitive types`, ...references.mdn_primitive })} like strings, numbers, and booleans.
For complex data types like objects and arrays, you'll want to use Actions (covered below).
Signals can be created with any type of value, but they work best with ${el("a", { textContent:
t`primitive types`, ...references.mdn_primitive })} like strings, numbers, and booleans. For complex
data types like objects and arrays, youll want to use Actions (covered below).
`),
el(h3, t`Derived Signals: Computed Values`),
el("p").append(...T`
Computed values (also called derived signals) automatically update when their dependencies change.
Create them by passing a function to S():
Create them by passing ${el("strong", "a function")} to ${el("code", "S()")}:
`),
el(example, { src: fileURL("./components/examples/signals/derived.js"), page_id }),
el("p").append(...T`
Derived signals are read-only - you can't call .set() on them. Their value is always computed
from their dependencies. They're perfect for transforming or combining data from other signals.
Derived signals are read-only - you cant call ${el("code", ".set()")} on them. Their value is always
computed from their dependencies. Theyre perfect for transforming or combining data from other signals.
`),
el(example, { src: fileURL("./components/examples/signals/computations-abort.js"), page_id }),
el(h3, t`Signal Actions: For Complex State`),
el("p").append(...T`
When working with objects, arrays, or other complex data structures, Signal Actions provide
a structured way to modify state while maintaining reactivity.
When working with objects, arrays, or other complex data structures. Signal Actions provide
a structured way to modify state while maintaining reactivity.
`),
el("div", { class: "illustration" }).append(
el("div", { className: "illustration" }).append(
el("h4", t`Actions vs. Direct Mutation`),
el("div", { class: "comparison" }).append(
el("div", { class: "good-practice" }).append(
el("div", { className: "comparison" }).append(
el("div", { className: "good-practice" }).append(
el("h5", t`✅ With Actions`),
el(code, { content: `const todos = S([], {
add(text) {
this.value.push(text);
// Subscribers notified automatically
}
});
// Use the action
S.action(todos, "add", "New todo");`, page_id })
el(code, { content: `
const todos = S([], {
add(text) {
this.value.push(text);
// Subscribers notified automatically
}
});
// Use the action
S.action(todos, "add", "New todo");
`, page_id })
),
el("div", { class: "bad-practice" }).append(
el("div", { className: "bad-practice" }).append(
el("h5", t`❌ Without Actions`),
el(code, { content: `
const todos = S([]);
// Directly mutating the array
const items = todos.get();
items.push("New todo");
// This WON'T trigger updates!`, page_id }))
const todos = S([]);
// Directly mutating the array
const items = todos.get();
items.push("New todo");
// This WONT trigger updates!
`, page_id }))
),
),
el("p").append(...T`
@ -182,16 +184,19 @@ items.push("New todo");
el("li", t`Act similar to reducers in other state management libraries`)
),
el("p").append(...T`
Here's a more complete example of a todo list using signal actions:
Heres a more complete example of a todo list using signal actions:
`),
el(example, { src: fileURL("./components/examples/signals/actions-todos.js"), page_id }),
el("div", { class: "tip" }).append(
el("div", { className: "tip" }).append(
el("p").append(...T`
${el("strong", "Special Action Methods")}: Signal actions can implement special lifecycle hooks:
`),
el("ul").append(
el("li", t`[S.symbols.onclear]() - Called when the signal is cleared. Use it to clean up resources.`),
el("li").append(...T`
${el("code", "[S.symbols.onclear]()")} - Called when the signal is cleared. Use it to clean up
resources.
`),
)
),
@ -200,39 +205,43 @@ items.push("New todo");
Signals really shine when connected to your UI. dd<el> provides several ways to bind signals to DOM elements:
`),
el("div", { class: "tabs" }).append(
el("div", { class: "tab", "data-tab": "attributes" }).append(
el("div", { className: "tabs" }).append(
el("div", { className: "tab", dataTab: "attributes" }).append(
el("h4", t`Reactive Attributes`),
el("p", t`Bind signal values directly to element attributes, properties, or styles:`),
el(code, { content: `// Create a signal
const color = S("blue");
el(code, { content: `
// Create a signal
const color = S("blue");
// Bind it to an element's style
el("div", {
style: {
color, // Updates when signal changes
fontWeight: S(() => color.get() === "red" ? "bold" : "normal")
}
}, "This text changes color");
// Bind it to an elements style
el("div", {
style: {
color, // Updates when signal changes
fontWeight: S(() => color.get() === "red" ? "bold" : "normal")
}
}, "This text changes color");
// Later:
color.set("red"); // UI updates automatically`, page_id })
// Later:
color.set("red"); // UI updates automatically
`, page_id }),
),
el("div", { class: "tab", "data-tab": "elements" }).append(
el("div", { className: "tab", dataTab: "elements" }).append(
el("h4", t`Reactive Elements`),
el("p", t`Dynamically create or update elements based on signal values:`),
el(code, { content: `// Create an array signal
const items = S(["Apple", "Banana", "Cherry"]);
el(code, { content: `
// Create an array signal
const items = S(["Apple", "Banana", "Cherry"]);
// Create a dynamic list that updates when items change
el("ul").append(
S.el(items, items =>
items.map(item => el("li", item))
)
);
// Create a dynamic list that updates when items change
el("ul").append(
S.el(items, items =>
items.map(item => el("li", item))
)
);
// Later:
S.action(items, "push", "Dragonfruit"); // List updates automatically`, page_id })
// Later:
S.action(items, "push", "Dragonfruit"); // List updates automatically
`, page_id }),
)
),
@ -254,23 +263,24 @@ S.action(items, "push", "Dragonfruit"); // List updates automatically`, page_id
`),
el("ol").append(
el("li").append(...T`
${el("strong", "Keep signals small and focused")}: Use many small signals rather than a few large ones
${el("strong", "Keep signals small and focused")}: Use many small signals rather than a few large ones
`),
el("li").append(...T`
${el("strong", "Use derived signals for computations")}: Don't recompute values in multiple places
${el("strong", "Use derived signals for computations")}: Dont recompute values in multiple places
`),
el("li").append(...T`
${el("strong", "Clean up signal subscriptions")}: Use AbortController or scope.host() to prevent memory leaks
${el("strong", "Clean up signal subscriptions")}: Use AbortController (scope.host()) to prevent memory
leaks
`),
el("li").append(...T`
${el("strong", "Use actions for complex state")}: Don't directly mutate objects or arrays in signals
${el("strong", "Use actions for complex state")}: Dont directly mutate objects or arrays in signals
`),
el("li").append(...T`
${el("strong", "Avoid infinite loops")}: Be careful when one signal updates another in a subscription
${el("strong", "Avoid infinite loops")}: Be careful when one signal updates another in a subscription
`)
),
el("div", { class: "troubleshooting" }).append(
el("div", { className: "troubleshooting" }).append(
el("h4", t`Common Signal Pitfalls`),
el("dl").append(
el("dt", t`UI not updating when array/object changes`),

View File

@ -10,7 +10,7 @@ import { simplePage } from "./layout/simplePage.html.js";
import { example } from "./components/example.html.js";
import { h3 } from "./components/pageUtils.html.js";
import { mnemonic } from "./components/mnemonic/scopes-init.js";
import { code } from "./components/code.html.js";
import { code, pre } from "./components/code.html.js";
/** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url);
const references= {
@ -31,7 +31,7 @@ export function page({ pkg, info }){
return el(simplePage, { info, pkg }).append(
el("p").append(...T`
For state-less components we can use functions as UI components (see Elements page). But in real life,
we may need to handle the component's life-cycle and provide JavaScript the way to properly use
we may need to handle the components life-cycle and provide JavaScript the way to properly use
the ${el("a", { textContent: t`Garbage collection`, ...references.garbage_collection })}.
`),
el(code, { src: fileURL("./components/examples/scopes/intro.js"), page_id }),
@ -40,56 +40,56 @@ export function page({ pkg, info }){
el(h3, t`Understanding Host Elements and Scopes`),
el("p").append(...T`
The ${el("strong", "host")} is the name for the element representing the component. This is typically the
element returned by a function. To get a reference, you can use ${el("code", "scope.host()")}. To apply addons,
element returned by a function. To get a reference, you can use ${el("code", "scope.host()")}. To apply addons,
just use ${el("code", "scope.host(...<addons>)")}.
`),
el("p").append(...T`
Scopes are primarily needed when signals are used in DOM templates (with ${el("code", "el")}, ${el("code",
"assign")}, or ${el("code", "S.el")}). They provide a way for automatically removing signal listeners
"assign")}, or ${el("code", "S.el")}). They provide a way for automatically removing signal listeners
and cleaning up unused signals when components are removed from the DOM.
`),
el("div", { className: "illustration" }).append(
el("h4", t`Component Anatomy`),
el("pre").append(el("code", `
// 1. Component scope created
el(MyComponent);
el(pre, { content: `
// 1. Component scope created
el(MyComponent);
function MyComponent() {
  // 2. access the host element
  const { host } = scope;
function MyComponent() {
// 2. access the host element
const { host } = scope;
  // 3. Add behavior to host
  host(
  on.click(handleClick)
  );
// 3. Add behavior to host
host(
on.click(handleClick)
);
  // 4. Return the host element
  return el("div", {
  className: "my-component"
  }).append(
  el("h2", "Title"),
  el("p", "Content")
  );
}
`.trim()))
// 4. Return the host element
return el("div", {
className: "my-component"
}).append(
el("h2", "Title"),
el("p", "Content")
);
}
` })
),
el("div", { className: "function-table" }).append(
el("h4", t`scope.host()`),
el("dl").append(
el("dt", t`When called with no arguments`),
el("dd", t`Returns a reference to the host element (the root element of your component)`),
el("dd", t`Returns a reference to the host element (the root element of your component)`),
el("dt", t`When called with addons/callbacks`),
el("dd", t`Applies the addons to the host element and returns the host element`)
el("dd", t`Applies the addons to the host element (and returns the host element)`)
)
),
el(example, { src: fileURL("./components/examples/scopes/scopes-and-hosts.js"), page_id }),
el("div", { className: "tip" }).append(
el("p").append(...T`
${el("strong", "Best Practice:")} Always capture the host reference at the beginning of your component
function using ${el("code", "const { host } = scope")} to avoid scope-related issues, especially with
asynchronous code.
${el("strong", "Best Practice:")} Always capture the host reference (or other scope related values) at
the beginning of your component function using ${el("code", "const { host } = scope")} to avoid
scope-related issues, especially with ${el("em", "asynchronous code")}.
`),
el("p").append(...T`
If you are interested in the implementation details, see Class-Based Components section.
@ -112,31 +112,32 @@ function MyComponent() {
`),
el("div", { className: "illustration" }).append(
el("h4", t`Lifecycle Flow`),
el("pre").append(el("code", `
1. Component created scope established
2. Component add<el> to DOM connected event
3. Component interactions happen
4. Component removed from DOM disconnected event
5. Automatic cleanup of:
  - Event listeners
  - Signal subscriptions
  - Custom cleanup code
`))
el(pre, { content: `
1. Component created scope established
2. Component added to DOM connected event
3. Component interactions happen
4. Component removed from DOM disconnected event
5. Automatic cleanup of:
- Event listeners (browser)
- Signal subscriptions (dd<el> and browser)
- Custom cleanup code (dd<el> and user)
` })
),
el(example, { src: fileURL("./components/examples/scopes/cleaning.js"), page_id }),
el("div", { className: "note" }).append(
el("p").append(...T`
In this example, when you click "Remove", the component is removed from the DOM, and all its associated
resources are automatically cleaned up, including the signal subscription that updates the text content.
This happens because the library internally registers a disconnected event handler on the host element.
resources are automatically cleaned up, including ${el("em",
"the signal subscription that updates the text content")}. This happens because the library
internally registers a disconnected event handler on the host element.
`)
),
el(h3, t`Declarative vs Imperative Components`),
el("p").append(...T`
The library DOM API and signals work best when used declaratively. It means you split your app's logic
into three parts as introduced in ${el("a", { textContent: "Signals", ...references.signals })}.
The library DOM API and signals work best when used declaratively. It means you split your apps logic
into three parts as introduced in ${el("a", { textContent: "Signals (3PS)", ...references.signals })}.
`),
el("div", { className: "note" }).append(
el("p").append(...T`
@ -145,17 +146,17 @@ function MyComponent() {
`)
),
el("div", { className: "tabs" }).append(
el("div", { className: "tab", "data-tab": "declarative" }).append(
el("div", { className: "tab", dataTab: "declarative" }).append(
el("h4", t`✅ Declarative Approach`),
el("p", t`Define what your UI should look like based on state:`),
el(code, { src: fileURL("./components/examples/scopes/declarative.js"), page_id })
),
el("div", { className: "tab", "data-tab": "imperative" }).append(
el("div", { className: "tab", dataTab: "imperative" }).append(
el("h4", t`⚠️ Imperative Approach`),
el("p", t`Manually update the DOM in response to events:`),
el(code, { src: fileURL("./components/examples/scopes/imperative.js"), page_id })
),
el("div", { className: "tab", "data-tab": "mixed" }).append(
el("div", { className: "tab", dataTab: "mixed" }).append(
el("h4", t`❌ Mixed Approach`),
el("p", t`This approach should be avoided:`),
el(code, { src: fileURL("./components/examples/scopes/mixed.js"), page_id })
@ -171,7 +172,8 @@ function MyComponent() {
${el("strong", "Define signals as constants:")} ${el("code", "const counter = S(0);")}
`),
el("li").append(...T`
${el("strong", "Prefer declarative patterns:")} Use signals to drive UI updates rather than manual DOM manipulation
${el("strong", "Prefer declarative patterns:")} Use signals to drive UI updates rather than manual DOM
manipulation
`),
el("li").append(...T`
${el("strong", "Keep components focused:")} Each component should do one thing well
@ -195,7 +197,7 @@ function MyComponent() {
el("dd", t`Use arrow functions or .bind() to preserve context`),
el("dt", t`Mixing declarative and imperative styles`),
el("dd", t`Choose one approach and be consistent throughout a component`)
el("dd", t`Choose one approach and be consistent throughout a component(s)`)
)
),

View File

@ -10,7 +10,7 @@ import { simplePage } from "./layout/simplePage.html.js";
import { example } from "./components/example.html.js";
import { h3 } from "./components/pageUtils.html.js";
import { mnemonic } from "./components/mnemonic/customElement-init.js";
import { code } from "./components/code.html.js";
import { code, pre } from "./components/code.html.js";
/** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url);
const references= {
@ -49,6 +49,11 @@ const references= {
title: t`Everything you need to know about Shadow DOM (github repo praveenpuglia/shadow-dom-in-depth)`,
href: "https://github.com/praveenpuglia/shadow-dom-in-depth",
},
/** Decorators */
decorators: {
title: t`JavaScript Decorators: An In-depth Guide`,
href: "https://www.sitepoint.com/javascript-decorators-what-they-are/",
}
};
/** @param {import("./types.d.ts").PageAttrs} attrs */
export function page({ pkg, info }){
@ -56,7 +61,7 @@ export function page({ pkg, info }){
return el(simplePage, { info, pkg }).append(
el("p").append(...T`
dd<el> pairs powerfully with ${el("a", references.mdn_web_components).append(el("strong", t`Web
Components`))} to create reusable, encapsulated custom elements with all the benefits of dd<el>'s
Components`))} to create reusable, encapsulated custom elements with all the benefits of dd<el>s
declarative DOM construction and reactivity system.
`),
el("div", { className: "callout" }).append(
@ -66,14 +71,13 @@ export function page({ pkg, info }){
el("li", t`Reactive attribute updates through signals`),
el("li", t`Simplified event handling with the same events API`),
el("li", t`Clean component lifecycle management`),
el("li", t`Improved code organization with scopes`)
)
),
),
el(code, { src: fileURL("./components/examples/customElement/intro.js"), page_id }),
el(h3, t`Getting Started: Web Components Basics`),
el("p").append(...T`
Web Components are a set of standard browser APIs that let you create custom HTML elements with
Web Components are a set of standard browser APIs that let you create custom HTML elements with
encapsulated functionality. They consist of three main technologies:
`),
el("ul").append(
@ -81,14 +85,15 @@ export function page({ pkg, info }){
${el("strong", "Custom Elements:")} Create your own HTML tags with JS-defined behavior
`),
el("li").append(...T`
${el("strong", "Shadow DOM:")} Encapsulate styles and markup within a component
${el("strong", "Shadow DOM:")} Encapsulate styles and markup within a component
`),
el("li").append(...T`
${el("strong", "HTML Templates:")} Define reusable markup structures
${el("strong", "HTML Templates:")} Define reusable markup structures (${el("em",
"the dd<el> replaces this part")})
`)
),
el("p").append(...T`
Let's start with a basic Custom Element example without dd<el> to establish the foundation:
Lets start with a basic Custom Element example without dd<el> to establish the foundation:
`),
el(code, { src: fileURL("./components/examples/customElement/native-basic.js"), page_id }),
@ -103,19 +108,20 @@ export function page({ pkg, info }){
el(h3, t`dd<el> Integration: Step 1 - Event Handling`),
el("p").append(...T`
The first step in integrating dd<el> with Web Components is enabling dd<el>'s event system to work with your
The first step in integrating dd<el> with Web Components is enabling dd<el>s event system to work with your
Custom Elements. This is done with ${el("code", "customElementWithDDE")}, which makes your Custom Element
compatible with dd<el>'s event handling.
compatible with dd<el>s event handling. (${el("em").append(...T`Notice that customElementWithDDE is
actually`)} ${el("a", { textContent: "decorator", ...references.decorators })})
`),
el("div", { className: "function-table" }).append(
el("h4", t`customElementWithDDE`),
el("dl").append(
el("dt", t`Purpose`),
el("dd", t`Enables dd<el>'s event system to work with your Custom Element`),
el("dd", t`Enables dd<el>s event system to work with your Custom Element`),
el("dt", t`Usage`),
el("dd", t`customElementWithDDE(YourElementClass)`),
el("dt", t`Benefits`),
el("dd", t`Allows using on.connected(), on.disconnected(), etc. with your element`)
el("dd", t`Allows using on.connected(), on.disconnected() or S.observedAttributes().`)
)
),
el(example, { src: fileURL("./components/examples/customElement/customElementWithDDE.js"), page_id }),
@ -123,26 +129,27 @@ export function page({ pkg, info }){
el("div", { className: "tip" }).append(
el("p").append(...T`
${el("strong", "Key Point:")} The ${el("code", "customElementWithDDE")} function adds event dispatching
to your Custom Element lifecycle methods, making them work seamlessly with dd<el>'s event system.
to your Custom Element lifecycle methods, making them work seamlessly with dd<el>s event system.
`)
),
el(h3, t`dd<el> Integration: Step 2 - Rendering Components`),
el("p").append(...T`
The next step is to use dd<el>'s component rendering within your Custom Element. This is done with
The next step is to use dd<el>s component rendering within your Custom Element. This is done with
${el("code", "customElementRender")}, which connects your dd<el> component function to the Custom Element.
`),
el("div", { className: "function-table" }).append(
el("h4", t`customElementRender`),
el("dl").append(
el("dt", t`Purpose`),
el("dd", t`Connects a dd<el> component function to a Custom Element`),
el("dd", t`Connects a dd<el> component function to a Custom Element`),
el("dt", t`Parameters`),
el("dd").append(
el("ol").append(
el("li", t`Target (usually this or this.shadowRoot)`),
el("li", t`Component function that returns a DOM tree`),
el("li", t`Optional: Attributes transformer function (default or S.observedAttributes)`)
el("li", t`Component function that returns a DOM tree`),
el("li", t`Optional: Attributes transformer function (empty by default or
S.observedAttributes)`)
)
),
el("dt", t`Returns`),
@ -153,7 +160,7 @@ export function page({ pkg, info }){
el("div", { className: "note" }).append(
el("p").append(...T`
In this example, we're using Shadow DOM (${el("code", "this.attachShadow()")}) for encapsulation,
In this example, were using Shadow DOM (${el("code", "this.attachShadow()")}) for encapsulation,
but you can also render directly to the element with ${el("code", "customElementRender(this, ...)")}.
`)
),
@ -161,7 +168,7 @@ export function page({ pkg, info }){
el(h3, t`Reactive Web Components with Signals`),
el("p").append(...T`
One of the most powerful features of integrating dd<el> with Web Components is connecting HTML attributes
to dd<el>'s reactive signals system. This creates truly reactive custom elements.
to dd<el>s reactive signals system. This creates truly reactive custom elements.
`),
el("div", { className: "tip" }).append(
el("p").append(...T`
@ -169,7 +176,8 @@ export function page({ pkg, info }){
`),
el("ol").append(
el("li").append(...T`
${el("code", "observedAttributes")} - Passes attributes as regular values (static)
Using standard attribute access (${el("code", "this.getAttribute(<name>)")}) - Passes attributes as
regular values (static)
`),
el("li").append(...T`
${el("code", "S.observedAttributes")} - Transforms attributes into signals (reactive)
@ -177,8 +185,8 @@ export function page({ pkg, info }){
)
),
el("p").append(...T`
Using ${el("code", "S.observedAttributes")} creates a reactive connection between your element's attributes
and its internal rendering. When attributes change, your component automatically updates!
Using the ${el("code", "S.observedAttributes")} creates a reactive connection between your elements
attributes and its internal rendering. When attributes change, your component automatically updates!
`),
el(example, { src: fileURL("./components/examples/customElement/observedAttributes.js"), page_id }),
@ -186,31 +194,32 @@ export function page({ pkg, info }){
el("h4", t`How S.observedAttributes Works`),
el("ol").append(
el("li", t`Takes each attribute listed in static observedAttributes`),
el("li", t`Creates a dd<el> signal for each one`),
el("li", t`Creates a dd<el> signal for each one`),
el("li", t`Automatically updates these signals when attributes change`),
el("li", t`Passes the signals to your component function`),
el("li", t`In opposite, updates of signals trigger attribute changes`),
el("li", t`Your component reacts to changes through signal subscriptions`)
)
),
el(h3, t`Working with Shadow DOM`),
el("p").append(...T`
Shadow DOM provides encapsulation for your component's styles and markup. When using dd<el> with Shadow DOM,
Shadow DOM provides encapsulation for your components styles and markup. When using dd<el> with Shadow DOM,
you get the best of both worlds: encapsulation plus declarative DOM creation.
`),
el("div", { className: "illustration" }).append(
el("h4", t`Shadow DOM Encapsulation`),
el("pre").append(el("code", `
<my-custom-element>
  
    #shadow-root
el(pre, { content: `
<my-custom-element>
#shadow-root
      Created with dd<el>
    
      <div>
       <h2>Title</h2>
       <p>Content</p>
`))
Created with dd<el>
<div>
<h2>Title</h2>
<p>Content</p>
` })
),
el(example, { src: fileURL("./components/examples/customElement/shadowRoot.js"), page_id }),
@ -232,7 +241,7 @@ export function page({ pkg, info }){
el("dt", t`Purpose`),
el("dd", t`Provides slot functionality when you cannot/do not want to use shadow DOM`),
el("dt", t`Parameters`),
el("dd", t`A mapping object of slot names to DOM elements`)
el("dd", t`A mapping object of slot names to DOM elements`)
)
),
@ -264,7 +273,7 @@ export function page({ pkg, info }){
el("dt", t`Events not firing properly`),
el("dd", t`Make sure you called customElementWithDDE before defining the element`),
el("dt", t`Attributes not updating`),
el("dd", t`Check that you've properly listed them in static observedAttributes`),
el("dd", t`Check that youve properly listed them in static observedAttributes`),
el("dt", t`Component not rendering`),
el("dd", t`Verify customElementRender is called in connectedCallback, not constructor`)
)

View File

@ -19,7 +19,7 @@ export function page({ pkg, info }){
return el(simplePage, { info, pkg }).append(
el("p").append(...T`
Debugging is an essential part of application development. This guide provides techniques
and best practices for debugging applications built with dd<el>, with a focus on signals.
and best practices for debugging applications built with dd<el>, with a focus on signals.
`),
el(h3, t`Debugging signals`),
@ -30,42 +30,40 @@ export function page({ pkg, info }){
el("h4", t`Inspecting signal values`),
el("p").append(...T`
The simplest way to debug a signal is to log its current value by calling the get method:
The simplest way to debug a signal is to log its current value by calling the get method:
`),
el(code, { content: `
const signal = S(0);
console.log('Current value:', signal.get());
// without triggering updates
console.log('Current value:', signal.valueOf());
`, page_id }),
const signal = S(0);
console.log('Current value:', signal.get());
// without triggering updates
console.log('Current value:', signal.valueOf());
`, page_id }),
el("p").append(...T`
You can also monitor signal changes by adding a listener:
You can also monitor signal changes by adding a listener:
`),
el(code, {
content:
"// Log every time the signal changes\nS.on(signal, value => console.log('Signal changed:', value));",
page_id }),
el(code, { content: `
// Log every time the signal changes
S.on(signal, value => console.log('Signal changed:', value));
`, page_id }),
el("h4", t`Debugging derived signals`),
el("p").append(...T`
With derived signals (created with ${el("code", "S(() => computation))")}), debugging is a bit more complex
because the value depends on other signals. To understand why a derived signal isn't updating correctly:
With derived signals (created with ${el("code", "S(() => computation))")}), debugging is a bit more complex
because the value depends on other signals. To understand why a derived signal isnt updating correctly:
`),
el("ol").append(
el("li", t`Check that all dependency signals are updating correctly`),
el("li", t`Add logging inside the computation function to see when it runs`),
el("li", t`Add logging/debugger inside the computation function to see when it runs`),
el("li", t`Verify that the computation function actually accesses the signal values with .get()`)
),
el(example, { src: fileURL("./components/examples/debugging/consoleLog.js"), page_id }),
el(h3, t`Common signal debugging issues`),
el("h4", t`Signal updates not triggering UI changes`),
el("p").append(...T`
If signal updates aren't reflected in the UI, check:
`),
el("p", t`If signal updates arent reflected in the UI, check:`),
el("ul").append(
el("li", t`That you're using signal.set() to update the value, not modifying objects/arrays directly`),
el("li", t`For mutable objects, ensure you're using actions or making proper copies before updating`),
el("li", t`That youre using signal.set() to update the value, not modifying objects/arrays directly`),
el("li", t`For mutable objects, ensure youre using actions or making proper copies before updating`),
el("li", t`That the signal is actually connected to the DOM element (check your S.el or attribute binding code)`)
),
el(code, { src: fileURL("./components/examples/debugging/mutations.js"), page_id }),
@ -77,12 +75,10 @@ console.log('Current value:', signal.valueOf());
`),
el("h4", t`Performance issues with frequently updating signals`),
el("p").append(...T`
If you notice performance issues with signals that update very frequently:
`),
el("p", t`If you notice performance issues with signals that update very frequently:`),
el("ul").append(
el("li", t`Consider debouncing or throttling signal updates`),
el("li", t`Make sure derived signals don't perform expensive calculations unnecessarily`),
el("li", t`Make sure derived signals dont perform expensive calculations unnecessarily`),
el("li", t`Keep signal computations focused and minimal`)
),
el(code, { src: fileURL("./components/examples/debugging/debouncing.js"), page_id }),
@ -96,13 +92,13 @@ console.log('Current value:', signal.valueOf());
el("p").append(...T`
dd<el> marks components in the DOM with special comment nodes to help you identify component boundaries.
Components created with ${el("code", "el(ComponentFunction)")} are marked with comment nodes
${el("code", "<!--<dde:mark type=\"component\" name=\"MyComponent\" host=\"parentElement\"/>-->")} and
${el("code", `<!--<dde:mark type="component" name="MyComponent" host="parentElement"/>-->`)} and
includes:
`),
el("ul").append(
el("li", "type - Identifies the type of marker (\"component\", \"reactive\", or \"later\")"),
el("li", "name - The name of the component function"),
el("li", "host - Indicates whether the host is \"this\" (for DocumentFragments) or \"parentElement\""),
el("li", t`type - Identifies the type of marker ("component", "reactive", or "later")`),
el("li", t`name - The name of the component function`),
el("li", t`host - Indicates whether the host is "this" (for DocumentFragments) or "parentElement"`),
),
el("h4", t`Finding reactive elements in the DOM`),
@ -111,9 +107,9 @@ console.log('Current value:', signal.valueOf());
that are automatically updated when signal values change. These elements are wrapped in special
comment nodes for debugging (to be true they are also used internally, so please do not edit them by hand):
`),
el(code, { src: fileURL("./components/examples/debugging/dom-reactive-mark.js"), page_id }),
el(code, { src: fileURL("./components/examples/debugging/dom-reactive-mark.html"), page_id }),
el("p").append(...T`
This is particularly useful when debugging why a reactive section isn't updating as expected.
This is particularly useful when debugging why a reactive section isnt updating as expected.
You can inspect the elements between the comment nodes to see their current state and the
signal connections through \`__dde_reactive\` of the host element.
`),
@ -124,48 +120,49 @@ console.log('Current value:', signal.valueOf());
`),
el("p").append(...T`
${el("code", "<element>.__dde_reactive")} - An array property on DOM elements that tracks signal-to-element
relationships. This allows you to quickly identify which elements are reactive and what signals they're
bound to. Each entry in the array contains:
relationships. This allows you to quickly identify which elements are reactive and what signals theyre
bound to. Each entry in the array contains:
`),
el("ul").append(
el("li", "A pair of signal and listener function: [signal, listener]"),
el("li", "Additional context information about the element or attribute"),
el("li", "Automatically managed by signal.el(), signal.observedAttributes(), and processReactiveAttribute()")
el("li", t`A pair of signal and listener function: [signal, listener]`),
el("li", t`Additional context information about the element or attribute`),
el("li", t`Automatically managed by signal.el(), signal.observedAttributes(), and processReactiveAttribute()`)
),
el("p").append(...T`
These properties make it easier to understand the reactive structure of your application when inspecting elements.
These properties make it easier to understand the reactive structure of your application when inspecting
elements.
`),
el(example, { src: fileURL("./components/examples/signals/debugging-dom.js"), page_id }),
el("h4", t`Examining signal connections`),
el("p").append(...T`
${el("code", "<signal>.__dde_signal")} - A Symbol property used to identify and store the internal state of
${el("code", "<signal>.__dde_signal")} - A Symbol property used to identify and store the internal state of
signal objects. It contains the following information:
`),
el("ul").append(
el("li", "listeners: A Set of functions called when the signal value changes"),
el("li", "actions: Custom actions that can be performed on the signal"),
el("li", "onclear: Functions to run when the signal is cleared"),
el("li", "host: Reference to the host element/scope"),
el("li", "defined: Stack trace information for debugging"),
el("li", "readonly: Boolean flag indicating if the signal is read-only")
el("li", t`listeners: A Set of functions called when the signal value changes`),
el("li", t`actions: Custom actions that can be performed on the signal`),
el("li", t`onclear: Functions to run when the signal is cleared`),
el("li", t`host: Reference to the host element/scope`),
el("li", t`defined: Stack trace information for debugging`),
el("li", t`readonly: Boolean flag indicating if the signal is read-only`)
),
el("p").append(...T`
to determine the current value of the signal, call ${el("code", "signal.valueOf()")}.
`),
el("p").append(...T`
You can inspect (host) element relationships and bindings with signals in the DevTools console using
${el("code", "$0.__dde_reactive")} (for currently selected element). In the console you will see a list of
${el("code", "[ [ signal, listener ], element, property ]")}, where:
${el("code", "$0.__dde_reactive")} (for currently selected element). In the console you will see a list of
${el("code", `[ [ signal, listener ], element, property ]`)}, where:
`),
el("ul").append(
el("li", "signal — the signal triggering the changes"),
el("li", "listener — the listener function (this is an internal function for dd<el>)"),
el("li", "element — the DOM element that is bound to the signal"),
el("li", "property — the attribute or property name which is changing based on the signal"),
el("li", t`signal — the signal triggering the changes`),
el("li", t`listener — the listener function (this is an internal function for dd<el>)`),
el("li", t`element — the DOM element that is bound to the signal`),
el("li", t`property — the attribute or property name which is changing based on the signal`),
),
el("p").append(...T`
the structure of \`__dde_reactive\` utilizes the browser's behavior of packing the first field,
the structure of \`__dde_reactive\` utilizes the browsers behavior of packing the first field,
so you can see the element and property that changes in the console right away.
`),

View File

@ -25,8 +25,8 @@ export function page({ pkg, info }){
el(h3, t`DOM Element Extensions with Addons`),
el("p").append(...T`
The primary method for extending DOM elements in dd<el> is through the Addon pattern.
Addons are functions that take an element and applying some functionality to it. This pattern enables a
clean, functional approach to element enhancement.
Addons are functions that take an element and applying some functionality to it. This pattern enables
a clean, functional approach to element enhancement.
`),
el("div", { className: "callout" }).append(
el("h4", t`What are Addons?`),
@ -34,61 +34,60 @@ export function page({ pkg, info }){
Addons are simply functions with the signature: (element) => void. They:
`),
el("ul").append(
el("li", t`Accept a DOM element as input`),
el("li", t`Accept a DOM element as input`),
el("li", t`Apply some behavior, property, or attribute to the element`),
)
),
el(code, { content: `
// Basic structure of an addon
function myAddon(config) {
return function(element) {
// Apply functionality to element
element.dataset.myAddon = config.option;
};
}
// Basic structure of an addon
function myAddon(config) {
return function(element) {
// Apply functionality to element
element.dataset.myAddon = config.option;
};
}
// Using an addon
el("div", { id: "example" }, myAddon({ option: "value" }));
`.trim(), page_id }),
// Using an addon
el("div", { id: "example" }, myAddon({ option: "value" }));
`, page_id }),
el(h3, t`Resource Cleanup with Abort Signals`),
el("p").append(...T`
When extending elements with functionality that uses resources like event listeners, timers,
or external subscriptions, it's critical to clean up these resources when the element is removed
or external subscriptions, its critical to clean up these resources when the element is removed
from the DOM. dd<el> provides utilities for this through AbortSignal integration.
`),
el("div", { className: "tip" }).append(
el("p").append(...T`
The ${el("code", "on.disconnectedAsAbort")} utility creates an AbortSignal that automatically
triggers when an element is disconnected from the DOM, making cleanup much easier to manage.
The ${el("code", "scope.signal")} property creates an AbortSignal that automatically
triggers when an element is disconnected from the DOM, making cleanup much easier to manage.
`)
),
el(code, { content: `
// Third-party library addon with proper cleanup
function externalLibraryAddon(config, signal) {
return function(element) {
// Initialize the third-party library
const instance = new ExternalLibrary(element, config);
// Third-party library addon with proper cleanup
function externalLibraryAddon(config, signal) {
return function(element) {
// Initialize the third-party library
const instance = new ExternalLibrary(element, config);
// Set up cleanup when the element is removed
signal.addEventListener('abort', () => {
instance.destroy();
});
// Set up cleanup when the element is removed
signal.addEventListener('abort', () => {
instance.destroy();
});
return element;
};
}
// dde component
function Component(){
const { host }= scope;
const signal= on.disconnectedAsAbort(host);
return el("div", null, externalLibraryAddon({ option: "value" }, signal));
}
`.trim(), page_id }),
return element;
};
}
// dde component
function Component(){
const { signal }= scope;
return el("div", null, externalLibraryAddon({ option: "value" }, signal));
}
`, page_id }),
el(h3, t`Building Library-Independent Extensions`),
el("p").append(...T`
When creating extensions, it's a good practice to make them as library-independent as possible.
When creating extensions, its a good practice to make them as library-independent as possible.
This approach enables better interoperability and future-proofing.
`),
el("div", { className: "illustration" }).append(
@ -97,37 +96,37 @@ function Component(){
el("div", { className: "tab" }).append(
el("h5", t`✅ Library-Independent`),
el(code, { content: `
function enhancementElement({ signal, ...config }) {
// do something
return function(element) {
// do something
signal.addEventListener('abort', () => {
// do cleanup
});
};
}
`.trim(), page_id })
function enhancementElement({ signal, ...config }) {
// do something
return function(element) {
// do something
signal.addEventListener('abort', () => {
// do cleanup
});
};
}
`, page_id })
),
el("div", { className: "tab" }).append(
el("h5", t`⚠️ Library-Dependent`),
el(code, { content: `
// Tightly coupled to dd<el>
function enhancementElement(config) {
return function(element) {
// do something
on.disconnected(()=> {
// do cleanup
})(element);
};
}
`.trim(), page_id })
// Tightly coupled to dd<el>
function enhancementElement(config) {
return function(element) {
// do something
on.disconnected(()=> {
// do cleanup
})(element);
};
}
`, page_id })
)
)
),
el(h3, t`Signal Extensions and Future Compatibility`),
el(h3, t`Signal Extensions and Factory Patterns`),
el("p").append(...T`
Unlike DOM elements, signal functionality in dd<el> currently lacks a standardized
Unlike DOM elements, signal functionality in dd<el> currently lacks a standardized
way to create library-independent extensions. This is because signals are implemented
differently across libraries.
`),
@ -139,35 +138,99 @@ function enhancementElement(config) {
native signals without breaking changes when they become available.
`)
),
el("h4", t`The Signal Factory Pattern`),
el("p").append(...T`
For now, when extending signals functionality, focus on clear interfaces and isolation to make
A powerful approach for extending signal functionality is the "Signal Factory" pattern.
This approach encapsulates specific behavior in a function that creates and configures a signal.
`),
el(code, { content: `
/**
* Creates a signal for managing route state
*
* @param {typeof S} signal - The signal constructor
*/
function routerSignal(signal){
const initial = location.hash.replace("#", "") || "all";
return signal(initial, {
/**
* Set the current route
* @param {"all"|"active"|"completed"} hash - The route to set
*/
set(hash){
location.hash = hash;
this.value = hash;
}
});
}
// Usage
const pageS = routerSignal(S);
// Update URL hash and signal value in one operation
S.action(pageS, "set", "active");
// React to signal changes in the UI
el("nav").append(
el("a", {
href: "#",
className: S(()=> pageS.get() === "all" ? "selected" : ""),
textContent: "All"
})
);
`, page_id }),
el("div", { className: "callout" }).append(
el("h4", t`Benefits of Signal Factories`),
el("ul").append(
el("li", t`Encapsulate related behavior in a single, reusable function`),
el("li", t`Create domain-specific signals with custom actions`),
el("li", t`Improve maintainability by centralizing similar logic`),
el("li", t`Enable better testability by accepting the signal constructor as a parameter`),
el("li", t`Create a clear semantic boundary around related state operations`)
)
),
el("p").append(...T`
Note how the factory accepts the signal constructor as a parameter, making it easier to test
and potentially migrate to different signal implementations in the future.
`),
el("h4", t`Other Signal Extension Approaches`),
el("p").append(...T`
For simpler cases, you can also extend signals with clear interfaces and isolation to make
future migration easier.
`),
el(code, { content: `
// Signal extension with clear interface
function createEnhancedSignal(initialValue) {
const signal = S(initialValue);
// Signal extension with clear interface
function createEnhancedSignal(initialValue) {
const signal = S(initialValue);
// Extension functionality
const increment = () => signal.set(signal.get() + 1);
const decrement = () => signal.set(signal.get() - 1);
// Extension functionality
const increment = () => signal.set(signal.get() + 1);
const decrement = () => signal.set(signal.get() - 1);
// Return the original signal with added methods
return Object.assign(signal, {
increment,
decrement
});
}
// Return the original signal with added methods
return { signal, increment, decrement };
}
// Usage
const counter = createEnhancedSignal(0);
el("button")({ onclick: () => counter.increment() }, "Increment");
el("div", S.text\`Count: \${counter}\`);
`.trim(), page_id }),
// Usage
const counter = createEnhancedSignal(0);
el("button", { textContent: "Increment", onclick: () => counter.increment() });
el("div", S.text\`Count: \${counter}\`);
`, page_id }),
el("div", { className: "tip" }).append(
el("p").append(...T`
When designing signal extensions, consider creating specialized signals for common patterns like:
forms, API requests, persistence, animations, or routing. These can significantly reduce
boilerplate code in your applications.
`)
),
el(h3, t`Using Signals Independently`),
el("p").append(...T`
While signals are tightly integrated with DDE's DOM elements, you can also use them independently.
While signals are tightly integrated with DDEs DOM elements, you can also use them independently.
This can be useful when you need reactivity in non-UI code or want to integrate with other libraries.
`),
el("p").append(...T`
@ -175,32 +238,34 @@ el("div", S.text\`Count: \${counter}\`);
`),
el("ol").append(
el("li").append(...T`
${el("strong", "Standard import")}: ${el("code", "import { S } from \"deka-dom-el/signals\";")}
This automatically registers signals with DDE's DOM reactivity system
${el("strong", "Standard import")}: ${el("code", `import { S } from "deka-dom-el/signals";`)}
This automatically registers signals with DDEs DOM reactivity system
`),
el("li").append(...T`
${el("strong", "Independent import")}: ${el("code", "import { S } from \"deka-dom-el/src/signals-lib\";")}
${el("strong", "Independent import")}: ${el("code", `import { S } from "deka-dom-el/src/signals-lib";`)}
This gives you just the signal system without DOM integration
`)
),
el(code, { content: `// Independent signals without DOM integration
import { signal as S, isSignal } from "deka-dom-el/src/signals-lib";
el(code, { content: `
// Independent signals without DOM integration
import { signal, isSignal } from "deka-dom-el/src/signals-lib";
// Create and use signals as usual
const count = S(0);
const doubled = S(() => count.get() * 2);
// Create and use signals as usual
const count = signal(0);
const doubled = signal(() => count.get() * 2);
// Subscribe to changes
S.on(count, value => console.log(value));
// Subscribe to changes
signal.on(count, value => console.log(value));
// Update signal value
count.set(5); // Logs: 5
console.log(doubled.get()); // 10`, page_id }),
// Update signal value
count.set(5); // Logs: 5
console.log(doubled.get()); // 10
`, page_id }),
el("p").append(...T`
The independent signals API includes all core functionality (${el("code", "S()")}, ${el("code", "S.on()")},
${el("code", "S.action()")}).
`),
el("div", { class: "callout" }).append(
el("div", { className: "callout" }).append(
el("h4", t`When to Use Independent Signals`),
el("ul").append(
el("li", t`For non-UI state management in your application`),
@ -213,12 +278,16 @@ console.log(doubled.get()); // 10`, page_id }),
el("ol").append(
el("li").append(...T`
${el("strong", "Use AbortSignals for cleanup:")} Always implement proper resource cleanup with
${el("code", "on.disconnectedAsAbort")} or similar mechanisms
${el("code", "scope.signal")} or similar mechanisms
`),
el("li").append(...T`
${el("strong", "Separate core logic from library adaptation:")} Make your core functionality work
with standard DOM APIs when possible
`),
el("li").append(...T`
${el("strong", "Use signal factories for common patterns:")} Create reusable signal factories that encapsulate
domain-specific behavior and state logic
`),
el("li").append(...T`
${el("strong", "Document clearly:")} Provide clear documentation on how your extension works
and what resources it uses
@ -247,8 +316,11 @@ console.log(doubled.get()); // 10`, page_id }),
el("dt", t`Mutating element prototypes`),
el("dd", t`Prefer compositional approaches with addons over modifying element prototypes`),
el("dt", t`Duplicating similar signal logic across components`),
el("dd", t`Use signal factories to encapsulate and reuse related signal behavior`),
el("dt", t`Complex initialization in addons`),
el("dd", t`Split complex logic into a separate initialization function that the addon can call`)
el("dd", t`Split complex logic into a separate initialization function that the addon can call`)
)
)
);

View File

@ -0,0 +1,387 @@
import { T, t } from "./utils/index.js";
export const info= {
title: t`Performance Optimization`,
fullTitle: t`Performance Optimization with dd<el>`,
description: t`Techniques for optimizing your dd<el> applications, focusing on memoization and efficient rendering.`,
};
import { el } from "deka-dom-el";
import { simplePage } from "./layout/simplePage.html.js";
import { example } from "./components/example.html.js";
import { h3 } from "./components/pageUtils.html.js";
import { code } from "./components/code.html.js";
import { mnemonic } from "./components/mnemonic/optimization-init.js";
/** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url);
const references= {
/** memo documentation */
memo_docs: {
title: t`dd<el> memo API documentation`,
href: "https://github.com/jaandrle/deka-dom-el#memo-api",
},
/** AbortController */
mdn_abort: {
title: t`MDN documentation for AbortController`,
href: "https://developer.mozilla.org/en-US/docs/Web/API/AbortController",
},
/** Performance API */
mdn_perf: {
title: t`MDN documentation for Web Performance API`,
href: "https://developer.mozilla.org/en-US/docs/Web/API/Performance_API",
},
/** Virtual DOM */
virtual_dom: {
title: t`Virtual DOM concept explanation`,
href: "https://reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom",
},
/** DocumentFragment */
mdn_fragment: {
title: t`MDN documentation for DocumentFragment`,
href: "https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment",
}
};
/** @param {import("./types.d.ts").PageAttrs} attrs */
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("p").append(...T`
As your applications grow, performance becomes increasingly important. dd<el> provides several
techniques to optimize rendering performance, especially when dealing with large lists or frequently
updating components. This guide focuses on memoization and other optimization strategies.
`),
el("div", { className: "callout" }).append(
el("h4", t`dd<el> Performance Optimization: Key Benefits`),
el("ul").append(
el("li", t`Efficient memoization system for component reuse`),
el("li", t`Targeted re-rendering without virtual DOM overhead`),
el("li", t`Memory management through AbortSignal integration`),
el("li", t`Optimized signal updates for reactive UI patterns`),
el("li", t`Simple debugging for performance bottlenecks`)
)
),
el(code, { src: fileURL("./components/examples/optimization/intro.js"), page_id }),
el(h3, t`Memoization with memo: Native vs dd<el>`),
el("p").append(...T`
In standard JavaScript applications, optimizing list rendering often involves manual caching
or relying on complex virtual DOM diffing algorithms. dd<el>'s ${el("code", "memo")} function
provides a simpler, more direct approach:
`),
el("div", { className: "illustration" }).append(
el("div", { className: "comparison" }).append(
el("div").append(
el("h5", t`Without Memoization`),
el(code, { content: `
// Each update to todosArray recreates all elements
function renderTodos(todosArray) {
return el("ul").append(
S.el(todosArray, todos => todos.map(todo=>
el("li", {
textContent: todo.text,
dataState: todo.completed ? "completed" : "",
})
))
);
}
`, page_id })
),
el("div").append(
el("h5", t`With dd<el>'s memo`),
el(code, { content: `
// With dd<el>s memoization
function renderTodos(todosArray) {
return el("ul").append(
S.el(todosArray, todos => todos.map(todo=>
// Reuses DOM elements when items havent changed
memo(todo.key, () =>
el("li", {
textContent: todo.text,
dataState: todo.completed ? "completed" : "",
})
)))
);
}
`, page_id })
)
)
),
el("p").append(...T`
The ${el("a", references.memo_docs).append(el("code", "memo"))} function in dd<el> allows you to
cache and reuse DOM elements instead of recreating them on every render, which can
significantly improve performance for components that render frequently or contain heavy computations.
`),
el("p").append(...T`
The memo system is particularly useful for:
`),
el("ul").append(
el("li", t`Lists that update frequently but where most items remain the same`),
el("li", t`Components with expensive rendering operations`),
el("li", t`Optimizing signal-driven UI updates`)
),
el(h3, t`Using memo with Signal Rendering`),
el("p").append(...T`
The most common use case for memoization is within ${el("code", "S.el()")} when rendering lists with
${el("code", "map()")}:
`),
el(code, { content: `
S.el(todosSignal, todos =>
el("ul").append(
...todos.map(todo =>
// Use a unique identifiers
memo(todo.id, () =>
el(TodoItem, todo)
))))
`, page_id }),
el("p").append(...T`
The ${el("code", "memo")} function in this context:
`),
el("ol").append(
el("li", t`Takes a unique key (todo.id) to identify this item`),
el("li", t`Caches the element created by the generator function`),
el("li", t`Returns the cached element on subsequent renders if the key remains the same`),
el("li", t`Only calls the generator function when rendering an item with a new key`)
),
el(example, { src: fileURL("./components/examples/optimization/memo.js"), page_id }),
el(h3, t`Creating Memoization Scopes`),
el("p").append(...T`
The ${el("code", "memo()")} uses cache store defined via the ${el("code", "memo.scope")} function.
That is actually what the ${el("code", "S.el")} is doing under the hood:
`),
el(code, { content: `
import { memo } from "deka-dom-el";
// Create a memoization scope
const renderItem = memo.scope(function(item) {
return el().append(
el("h3", item.title),
el("p", item.description),
// Expensive rendering operations...
memo(item, ()=> el("div", { className: "expensive-component" }))
);
});
// Use the memoized function
const items = [/* array of items */];
const container = el("div").append(
...items.map(item => renderItem(item))
);
`, page_id }),
el("p").append(...T`
The scope function accepts options to customize its behavior:
`),
el(code, { content: `
const renderList = memo.scope(function(list) {
return list.map(item =>
memo(item.id, () => el(ItemComponent, item))
);
}, {
// Only keep the cache from the most recent render
onlyLast: true,
// Clear cache when signal is aborted
signal: controller.signal
});
`, page_id }),
el("div", { className: "function-table" }).append(
el("dl").append(
el("dt", t`onlyLast Option`),
el("dd").append(...T`Only keeps the cache from the most recent function call,
which is useful when the entire collection is replaced. ${el("strong", "This is default behavior of ")
.append(el("code", "S.el"))}!`),
el("dt", t`signal Option`),
el("dd").append(...T`An ${el("a", references.mdn_abort).append(el("code", "AbortSignal"))}
that will clear the cache when aborted, helping with memory management`)
)
),
el(h3, t`Additional Optimization Techniques`),
el("h4", t`Minimizing Signal Updates`),
el("p").append(...T`
Signals are efficient, but unnecessary updates can impact performance:
`),
el("ul").append(
el("li", t`Avoid setting signal values that haven't actually changed`),
el("li", t`For frequently updating values (like scroll position), consider debouncing`),
el("li", t`Keep signal computations small and focused`),
el("li", t`Use derived signals to compute values only when dependencies change`)
),
el("h4", t`Optimizing List Rendering`),
el("p").append(...T`
Beyond memoization, consider these approaches for optimizing list rendering:
`),
el("ul").append(
el("li", t`Virtualize long lists to only render visible items`),
el("li", t`Use stable, unique keys for list items`),
el("li", t`Batch updates to signals that drive large lists`),
el("li", t`Consider using a memo scope for the entire list component`)
),
el("div", { className: "tip" }).append(
el("p").append(...T`
Memoization works best when your keys are stable and unique. Use IDs or other persistent
identifiers rather than array indices, which can change when items are reordered.
`),
el("p").append(...T`
Alternatively you can use any jsonable value as key, when the primitive values arent enough.
`)
),
el("h4", t`Memory Management`),
el("p").append(...T`
To prevent memory leaks and reduce memory consumption:
`),
el("ul").append(
el("li", t`Clear memo caches when components are removed`),
el("li", t`Use AbortSignals to manage memo lifetimes`),
el("li", t`Call S.clear() on signals that are no longer needed`),
el("li", t`Remove event listeners when elements are removed from the DOM`)
),
el("h4", t`Choosing the Right Optimization Approach`),
el("p").append(...T`
While memo is powerful, it's not always the best solution:
`),
el("table").append(
el("thead").append(
el("tr").append(
el("th", "Approach"),
el("th", "When to use")
)
),
el("tbody").append(
el("tr").append(
el("td", "memo"),
el("td", "Lists with stable items that infrequently change position")
),
el("tr").append(
el("td", "Signal computations"),
el("td", "Derived values that depend on other signals")
),
el("tr").append(
el("td", "Debouncing"),
el("td", "High-frequency events like scroll or resize")
),
el("tr").append(
el("td", "Stateful components"),
el("td", "Complex components with internal state")
)
)
),
el(h3, t`Known Issues and Limitations`),
el("p").append(...T`
While memoization is a powerful optimization technique, there are some limitations and edge cases to be aware of:
`),
el("h4", t`Document Fragments and Memoization`),
el("p").append(...T`
One important limitation to understand is how memoization interacts with
${el("a", references.mdn_fragment).append("DocumentFragment")} objects.
Functions like ${el("code", "S.el")} internally use DocumentFragment to efficiently handle multiple elements,
but this can lead to unexpected behavior with memoization.
`),
el(code, { content: `
// This pattern can lead to unexpected behavior
const memoizedFragment = memo("key", () => {
// Creates a DocumentFragment internally
return S.el(itemsSignal, items => items.map(item => el("div", item)));
});
// After the fragment is appended to the DOM, it becomes empty
container.append(memoizedFragment);
// On subsequent renders, the cached fragment is empty!
container.append(memoizedFragment); // Nothing gets appended
`, page_id }),
el("p").append(...T`
This happens because a DocumentFragment is emptied when it's appended to the DOM. When the fragment
is cached by memo and reused, it's already empty.
`),
el("div", { className: "tip" }).append(
el("h5", t`Solution: Memoize Individual Items`),
el(code, { content: `
// Correct approach: memoize the individual items, not the fragment
S.el(itemsSignal, items => items.map(item =>
memo(item.id, () => el("div", item))
));
// Or use a container element instead of relying on a fragment
memo("key", () =>
el("div", { className: "item-container" }).append(
S.el(itemsSignal, items => items.map(item => el("div", item)))
)
);
`, page_id })
),
el("p").append(...T`
Generally, you should either:
`),
el("ol").append(
el("li", t`Memoize individual items within the collection, not the entire collection result`),
el("li", t`Wrap the result in a container element instead of relying on fragment behavior`),
el("li", t`Be aware that S.el() and similar functions that return multiple elements are using fragments internally`)
),
el("div", { className: "note" }).append(
el("p").append(...T`
This limitation isn't specific to dd<el> but is related to how DocumentFragment works in the DOM.
Once a fragment is appended to the DOM, its child nodes are moved from the fragment to the target element,
leaving the original fragment empty.
`)
),
el(h3, t`Performance Debugging`),
el("p").append(...T`
To identify performance bottlenecks in your dd<el> applications:
`),
el("ol").append(
el("li").append(...T`Use ${el("a", references.mdn_perf).append("browser performance tools")} to profile
rendering times`),
el("li", t`Check for excessive signal updates using S.on() listeners with console.log`),
el("li", t`Verify memo usage by inspecting cache hit rates`),
el("li", t`Look for components that render more frequently than necessary`)
),
el("div", { className: "note" }).append(
el("p").append(...T`
For more details on debugging, see the ${el("a", { href: "p07-debugging.html", textContent: "Debugging" })} page.
`)
),
el(h3, t`Best Practices for Optimized Rendering`),
el("ol").append(
el("li").append(...T`
${el("strong", "Use memo for list items:")} Memoize items in lists, especially when they contain complex components.
`),
el("li").append(...T`
${el("strong", "Clean up with AbortSignals:")} Connect memo caches to component lifecycles using AbortSignals.
`),
el("li").append(...T`
${el("strong", "Profile before optimizing:")} Identify actual bottlenecks before adding optimization.
`),
el("li").append(...T`
${el("strong", "Use derived signals:")} Compute derived values efficiently with signal computations.
`),
el("li").append(...T`
${el("strong", "Avoid memoizing fragments:")} Memoize individual elements or use container elements
instead of DocumentFragments.
`)
),
el(mnemonic),
);
}

View File

@ -1,363 +0,0 @@
import { T, t } from "./utils/index.js";
export const info= {
title: t`Ireland Components`,
fullTitle: t`Interactive Demo Components with Server-Side Pre-Rendering`,
description: t`Creating live, interactive component examples in documentation with server-side
rendering and client-side hydration.`,
};
import { el } from "deka-dom-el";
import { simplePage } from "./layout/simplePage.html.js";
import { h3 } from "./components/pageUtils.html.js";
import { code } from "./components/code.html.js";
import { ireland } from "./components/ireland.html.js";
/** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url);
/** @param {import("./types.js").PageAttrs} attrs */
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("div", { className: "warning" }).append(
el("p").append(...T`
This part of the documentation is primarily intended for technical enthusiasts and authors of
3rd-party libraries. It describes an advanced feature, not a core part of the library. Most users will
not need to implement this functionality directly in their applications. This capability will hopefully
be covered by third-party libraries or frameworks that provide simpler SSR integration using
dd<el>.
`)
),
el(h3, t`What Are Ireland Components?`),
el("p").append(...T`
Ireland components are a special type of documentation component that:
`),
el("ul").append(
el("li", t`Display source code with syntax highlighting`),
el("li", t`Pre-render components on the server during documentation build`),
el("li", t`Copy component source files to the documentation output`),
el("li", t`Provide client-side rehydration for interactive demos`),
el("li", t`Allow users to run and experiment with components in real-time`)
),
el(h3, t`How Ireland Components Work`),
el("p").append(...T`
The Ireland component system consists of several parts working together:
`),
el("ol").append(
el("li").append(...T`
${el("strong", "Server-side rendering:")} Components are pre-rendered during the documentation build process
`),
el("li").append(...T`
${el("strong", "Component registration:")} Source files are copied to the documentation output directory
`),
el("li").append(...T`
${el("strong", "Client-side scripting:")} JavaScript code is generated to load and render components
`),
el("li").append(...T`
${el("strong", "User interaction:")} The "Run Component" button dynamically loads and renders the component
`)
),
el(h3, t`Implementation Architecture`),
el("p").append(...T`
The core of the Ireland system is implemented in ${el("code", "docs/components/ireland.html.js")}.
It integrates with the SSR build process using the ${el("code", "registerClientFile")} function
from ${el("code", "docs/ssr.js")}.
`),
el(code, { content: `
// Basic usage of an ireland component
el(ireland, {
src: fileURL("./components/examples/path/to/component.js"),
exportName: "NamedExport", // optional, defaults to "default",
})`, page_id }),
el("p").append(...T`
During the build process (${el("code", "bs/docs.js")}), the following happens:
`),
el("ol").append(
el("li", t`Component source code is loaded and displayed with syntax highlighting`),
el("li", t`Source files are registered to be copied to the output directory`),
el("li", t`Client-side scripts are generated for each page with ireland components`),
el("li", t`The component is wrapped in a UI container with controls`)
),
el(h3, t`Core Implementation Details`),
el("p").append(...T`
Let's look at the key parts of the ireland component implementation:
`),
el("h4", t`Building SSR`),
el(code, { content: `
// From bs/docs.js - Server-side rendering engine
import { createHTMl } from "./docs/jsdom.js";
import { register, queue } from "../jsdom.js";
import { path_target, dispatchEvent } from "../docs/ssr.js";
// For each page, render it on the server
for(const { id, info } of pages) {
// Create a virtual DOM environment for server-side rendering
const serverDOM = createHTMl("");
serverDOM.registerGlobally("HTMLScriptElement");
// Register dd<el> with the virtual DOM
const { el } = await register(serverDOM.dom);
// Import and render the page component
const { page } = await import(\`../docs/\${id}.html.js\`);
serverDOM.document.body.append(
el(page, { pkg, info }),
);
// Process the queue of asynchronous operations
await queue();
// Trigger render event handlers
dispatchEvent("oneachrender", document);
// Write the HTML to the output file
s.echo(serverDOM.serialize()).to(path_target.root+id+".html");
}
// Final build step - trigger SSR end event
dispatchEvent("onssrend");
`, page_id }),
el("h4", t`File Registration`),
el(code, { content: `
// From docs/ssr.js - File registration system
export function registerClientFile(url, { head, folder = "", replacer } = {}) {
// Ensure folder path ends with a slash
if(folder && !folder.endsWith("/")) folder += "/";
// Extract filename from URL
const file_name = url.pathname.split("/").pop();
// Create target directory if needed
s.mkdir("-p", path_target.root+folder);
// Get file content and apply optional replacer function
let content = s.cat(url);
if(replacer) content = s.echo(replacer(content.toString()));
// Write content to the output directory
content.to(path_target.root+folder+file_name);
// If a head element was provided, add it to the document
if(!head) return;
head[head instanceof HTMLScriptElement ? "src" : "href"] = file_name;
document.head.append(head);
}
`, page_id }),
el("h4", t`Server-Side Rendering`),
el(code, { content: `
// From docs/components/ireland.html.js - Server-side component implementation
export function ireland({ src, exportName = "default", props = {} }) {
// Calculate relative path for imports
const path = "./"+relative(dir, src.pathname);
// Generate unique ID for this component instance
const id = "ireland-" + generateComponentId(src);
// Create placeholder element
const element = el.mark({ type: "later", name: ireland.name });
// Import and render the component during SSR
queue(import(path).then(module => {
const component = module[exportName];
element.replaceWith(el(component, props, mark(id)));
}));
// Register client-side hydration on first component
if(!componentsRegistry.size)
addEventListener("oneachrender", registerClientPart);
// Store component info for client-side hydration
componentsRegistry.set(id, {
src,
path: dirFE+"/"+path.split("/").pop(),
exportName,
props,
});
return element;
}
// Register client-side resources
function registerClientPart() {
// Process all component registrations
const todo = Array.from(componentsRegistry.entries())
.map(([ id, d ]) => {
// Copy the component source file to output directory
registerClientFile(d.src, {
folder: dirFE,
// Replace bare imports for browser compatibility
replacer(file) {
return file.replaceAll(
/ from "deka-dom-el(\/signals)?";/g,
\` from "./esm-with-signals.js";\`
);
}
});
return [ id, d ];
});
// Serialize the component registry for client-side use
const store = JSON.stringify(JSON.stringify(todo));
// Copy client-side scripts to output
registerClientFile(new URL("./ireland.js.js", import.meta.url));
registerClientFile(new URL("../../dist/esm-with-signals.js", import.meta.url), { folder: dirFE });
// Add import map for package resolution
document.head.append(
el("script", { type: "importmap" }).append(\`
{
"imports": {
"deka-dom-el": "./\${dirFE}/esm-with-signals.js",
"deka-dom-el/signals": "./\${dirFE}/esm-with-signals.js"
}
}
\`.trim())
);
// Add bootstrap script to load components
document.body.append(
el("script", { type: "module" }).append(\`
import { loadIrelands } from "./ireland.js.js";
loadIrelands(new Map(JSON.parse(\${store})));
\`.trim())
);
}
`, page_id }),
el("h4", t`Client-Side Hydration`),
el(code, { content: `
// From docs/components/ireland.js.js - Client-side hydration
import { el } from "./irelands/esm-with-signals.js";
export function loadIrelands(store) {
// Find all marked components in the DOM
document.body.querySelectorAll("[data-dde-mark]").forEach(ireland => {
const { ddeMark } = ireland.dataset;
// Skip if this component isn't in our registry
if(!store.has(ddeMark)) return;
// Get component information
const { path, exportName, props } = store.get(ddeMark);
// Dynamically import the component module
import("./" + path).then(module => {
// Replace the server-rendered element with the client-side version
ireland.replaceWith(el(module[exportName], props));
});
});
}
`, page_id }),
el(h3, t`Live Example`),
el("p").append(...T`
Here's a live example of an Ireland component showing a standard counter.
The component is defined in ${el("code", "docs/components/examples/ireland-test/counter.js")} and
rendered with the Ireland component system:
`),
el(code, {
src: fileURL("./components/examples/ireland-test/counter.js"),
page_id
}),
el(ireland, {
src: fileURL("./components/examples/ireland-test/counter.js"),
exportName: "CounterStandard",
page_id
}),
el("p").append(...T`
When the "Run Component" button is clicked, the component is loaded and rendered dynamically.
The counter state is maintained using signals, allowing for reactive updates as you click
the buttons to increment and decrement the value.
`),
el(h3, t`Creating Your Own Components`),
el("p").append(...T`
To create components for use with the Ireland system, follow these guidelines:
`),
el("ol").append(
el("li").append(...T`
${el("strong", "Export a function:")} Components should be exported as named or default functions
`),
el("li").append(...T`
`),
el("li").append(...T`
${el("strong", "Accept props:")} Components should accept a props object, even if not using it
`),
el("li").append(...T`
${el("strong", "Manage reactivity:")} Use signals for state management where appropriate
`),
el("li").append(...T`
${el("strong", "Handle cleanup:")} Include any necessary cleanup for event listeners or signals
`)
),
el(h3, t`Practical Considerations and Limitations`),
el("p").append(...T`
When implementing Ireland components in real documentation, there are several important
considerations to keep in mind:
`),
el("div", { className: "warning" }).append(
el("h4", t`Module Resolution and Bundling`),
el("p").append(...T`
The examples shown here use bare module specifiers like ${el("code", "import { el } from \"deka-dom-el\"")}
which aren't supported in all browsers without importmaps. In a production implementation, you would need to:
`),
el("ol").append(
el("li", t`Replace bare import paths with actual paths during the build process`),
el("li", t`Bundle component dependencies to avoid multiple requests`),
el("li", t`Ensure all module dependencies are properly resolved and copied to the output directory`)
),
el("p").append(...T`
In this documentation, we replace the paths with ${el("code", "./esm-with-signals.js")} and provide
a bundled version of the library, but more complex components might require a dedicated bundling step.
`)
),
el("div", { className: "note" }).append(
el("h4", t`Component Dependencies`),
el("p").append(...T`
Real-world components typically depend on multiple modules and assets. The Ireland system would need
to be extended to:
`),
el("ul").append(
el("li", t`Detect and analyze all dependencies of a component`),
el("li", t`Bundle these dependencies together or ensure they're properly copied to the output directory`),
el("li", t`Handle non-JavaScript assets like CSS, images, or data files`)
)
),
el(h3, t`Advanced Usage`),
el("p").append(...T`
The Ireland system can be extended in several ways to address these limitations:
`),
el("ul").append(
el("li", t`Integrate with a bundler like esbuild, Rollup, or Webpack`),
el("li", t`Add props support for configuring components at runtime`),
el("li", t`Implement module caching to reduce network requests`),
el("li", t`Add code editing capabilities for interactive experimentation`),
el("li", t`Support TypeScript and other languages through transpilation`),
el("li", t`Implement state persistence between runs`)
),
el("p").append(...T`
This documentation site itself is built using the techniques described here,
showcasing how dd<el> can be used to create both the documentation and
the interactive examples within it. The implementation here is simplified for clarity,
while a production-ready system would need to address the considerations above.
`)
);
}

630
docs/p10-todomvc.html.js Normal file
View File

@ -0,0 +1,630 @@
import { T, t } from "./utils/index.js";
export const info= {
title: t`TodoMVC Implementation`,
fullTitle: t`TodoMVC with dd<el>`,
description: t`A complete TodoMVC implementation using dd<el> and signals, demonstrating real-world application
development.`,
};
import { el } from "deka-dom-el";
import { simplePage } from "./layout/simplePage.html.js";
import { example } from "./components/example.html.js";
import { h3 } from "./components/pageUtils.html.js";
import { code } from "./components/code.html.js";
/** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url);
const references= {
/** TodoMVC */
todomvc: {
title: t`TodoMVC Project`,
href: "https://todomvc.com/",
},
/** Demo source */
github_example: {
title: t`Full TodoMVC source code on GitHub`,
href: "https://github.com/jaandrle/deka-dom-el/blob/main/docs/components/examples/reallife/todomvc.js",
},
/** localStorage */
mdn_storage: {
title: t`MDN documentation for Web Storage API`,
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API",
},
/** Custom Events */
mdn_events: {
title: t`MDN documentation for Custom Events`,
href: "https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent"
},
/** requestAnimationFrame */
mdn_raf: {
title: t`MDN documentation for requestAnimationFrame`,
href: "https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame"
}
};
/** @param {import("./types.d.ts").PageAttrs} attrs */
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("p").append(...T`
${el("a", references.todomvc).append("TodoMVC")} is a project that helps developers compare different
frameworks by implementing the same todo application. This implementation showcases how dd<el>
can be used to build a complete, real-world application with all the expected features of a modern
web app.
`),
el("div", { className: "callout" }).append(
el("h4", t`TodoMVC with dd<el>: Key Features`),
el("ul").append(
el("li", t`Reactive UI with signals for state management`),
el("li", t`Component-based architecture with composable functions`),
el("li", t`Event handling with the 'on' and 'dispatchEvent' utilities`),
el("li", t`Performance optimization with memoization`),
el("li", t`Persistent storage with localStorage`),
el("li", t`Client-side routing with URL hash-based filtering`),
el("li", t`Component scopes for proper encapsulation`)
)
),
el("p").append(...T`
Below is a fully working TodoMVC implementation. You can interact with it directly in this
documentation page. The example demonstrates how dd<el> handles common app development
challenges in a clean, maintainable way.
`),
el(example, { src: fileURL("./components/examples/reallife/todomvc.js"), variant: "big", page_id }),
el(h3, t`Application Architecture Overview`),
el("p").append(...T`
The TodoMVC implementation is structured around several key components:
`),
el("div", { className: "function-table" }).append(
el("dl").append(
el("dt", t`Main Todos Component`),
el("dd", t`The core component that orchestrates the entire application, handling routing, state, and
rendering the UI`),
el("dt", t`TodoItem Component`),
el("dd", t`A reusable component for rendering individual todo items with editing capabilities`),
el("dt", t`Signal-based State`),
el("dd", t`Custom signals for managing todos and routing state with reactive updates`),
el("dt", t`Performance Optimization`),
el("dd", t`Memoization of components and reactive elements to minimize DOM updates`),
el("dt", t`Custom Events`),
el("dd", t`Communication between components using custom events for cleaner architecture`)
)
),
el(h3, t`Reactive State Management with Signals`),
el("p").append(...T`
The application uses three primary signals to manage state:
`),
el(code, { content: `
// Signal for current route (all/active/completed)
const pageS = routerSignal(S);
// Signal for the todos collection with custom actions
const todosS = todosSignal();
// Derived signal that filters todos based on current route
const filteredTodosS = S(()=> {
const todos = todosS.get();
const filter = pageS.get();
return todos.filter(todo => {
if (filter === "active") return !todo.completed;
if (filter === "completed") return todo.completed;
return true; // "all"
});
});
`, page_id }),
el("p").append(...T`
The ${el("code", "todosSignal")} function creates a custom signal with actions for manipulating the todos:
`),
el(code, { content: `
/**
* Creates a signal for managing todos with persistence
*
* Features:
* - Loads todos from localStorage on initialization
* - Automatically saves todos to localStorage on changes
* - Provides actions for adding, editing, deleting todos
*/
function todosSignal(){
const store_key = "dde-todos";
// Try to load todos from localStorage
let savedTodos = [];
try {
const stored = localStorage.getItem(store_key);
if (stored) {
savedTodos = JSON.parse(stored);
}
} catch (_) {}
const out= S(/** @type {Todo[]} */(savedTodos || []), {
/**
* Add a new todo
* @param {string} value - The title of the new todo
*/
add(value){
this.value.push({
completed: false,
title: value,
id: uuid(),
});
},
/**
* Edit an existing todo
* @param {{ id: string, [key: string]: any }} data - Object containing id and fields to update
*/
edit({ id, ...update }){
const index = this.value.findIndex(t => t.id === id);
if (index === -1) return this.stopPropagation();
Object.assign(this.value[index], update);
},
/**
* Delete a todo by id
* @param {string} id - The id of the todo to delete
*/
delete(id){
const index = this.value.findIndex(t => t.id === id);
if (index === -1) return this.stopPropagation();
this.value.splice(index, 1);
},
/**
* Remove all completed todos
*/
clearCompleted() {
this.value = this.value.filter(todo => !todo.completed);
},
/**
* Handle cleanup when signal is cleared
*/
[S.symbols.onclear](){
}
});
/**
* Save todos to localStorage whenever the signal changes
* @param {Todo[]} value - Current todos array
*/
S.on(out, /** @param {Todo[]} value */ function saveTodos(value) {
try {
localStorage.setItem(store_key, JSON.stringify(value));
} catch (e) {
console.error("Failed to save todos to localStorage", e);
}
});
return out;
}
`, page_id }),
el("div", { className: "note" }).append(
el("p").append(...T`
Using ${el("a", references.mdn_storage).append("localStorage")} allows the application to persist todos
even when the page is refreshed. The ${el("code", "S.on")} listener ensures todos are saved
after every state change, providing automatic persistence without explicit calls.
`)
),
el(h3, t`Integration of Signals and Reactive UI`),
el("p").append(...T`
The implementation demonstrates a clean integration between signal state and reactive UI:
`),
el("h4", t`1. Derived Signals for Filtering`),
el(code, { content: `
/** Derived signal that filters todos based on current route */
const filteredTodosS = S(()=> {
const todos = todosS.get();
const filter = pageS.get();
return todos.filter(todo => {
if (filter === "active") return !todo.completed;
if (filter === "completed") return todo.completed;
return true; // "all"
});
});
// Using the derived signal in the UI
el("ul", { className: "todo-list" }).append(
S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
)
)
`, page_id }),
el("p").append(...T`
The derived signal automatically recalculates whenever either the todos list or the current filter changes,
ensuring the UI always shows the correct filtered todos.
`),
el("h4", t`2. Local Component State`),
el(code, { content: `
function TodoItem({ id, title, completed }) {
const { host }= scope;
// Local UI state signals
const isEditing = S(false);
const isCompleted = S(completed);
/** @type {(id: string) => void} Dispatch function for deleting todo */
const dispatchDelete= dispatchEvent("todo:delete", host);
/** @type {(data: {id: string, [key: string]: any}) => void} Dispatch function for editing todo */
const dispatchEdit = dispatchEvent("todo:edit", host);
// Event handlers that update local state
/** @type {ddeElementAddon<HTMLInputElement>} */
const onToggleCompleted = on("change", (ev) => {
const completed= /** @type {HTMLInputElement} */(ev.target).checked;
isCompleted.set(completed);
dispatchEdit({ id, completed });
});
// UI that responds to local state
return el("li", {
classList: { completed: isCompleted, editing: isEditing }
}).append(
// Component content...
);
}
`, page_id }),
el("p").append(...T`
The TodoItem component maintains its own local UI state with signals, providing immediate
UI feedback while still communicating changes to the parent via events.
`),
el("h4", t`3. Reactive Properties`),
el(code, { content: `
// Dynamic class attributes
el("a", {
textContent: "All",
className: S(()=> pageS.get() === "all" ? "selected" : ""),
href: "#"
})
// Reactive classList
el("li", {
classList: { completed: isCompleted, editing: isEditing }
})
`, page_id }),
el("div", { className: "tip" }).append(
el("p").append(...T`
Binding signals directly to element properties creates a reactive UI that automatically updates
when state changes, without the need for explicit DOM manipulation or virtual DOM diffing.
`)
),
el(h3, t`Performance Optimization with Memoization`),
el("p").append(...T`
The implementation uses ${el("code", "memo")} to optimize performance in several key areas:
`),
el("h4", t`Memoizing Todo Items`),
el(code, { content: `
el("ul", { className: "todo-list" }).append(
S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
)
)
`, page_id }),
el("p").append(...T`
This approach ensures that:
`),
el("ul").append(
el("li", t`Todo items are only re-rendered when their data changes`),
el("li", t`The same DOM elements are reused, even as todos are filtered`),
el("li", t`Each todo is memoized independently using its unique ID`),
el("li", t`The UI remains responsive even with a large number of todos`)
),
el("h4", t`Memoizing UI Sections`),
el(code, { content: `
S.el(todosS, todos => memo(todos.length, length=> length
? el("footer", { className: "footer" }).append(
// Footer content...
)
: el()
))
`, page_id }),
el("p").append(...T`
By memoizing based on the todos length, the entire footer component is only re-rendered
when todos are added or removed, not when their properties change. This improves performance
by avoiding unnecessary DOM operations.
`),
el("div", { className: "tip" }).append(
el("p").append(...T`
Memoization is especially important for UI elements that are expensive to render or that contain
many child elements. The ${el("code", "memo")} function allows precise control over when components
should re-render, avoiding the overhead of virtual DOM diffing algorithms.
`)
),
el(h3, t`Component-Based Architecture with Events`),
el("p").append(...T`
The TodoMVC implementation demonstrates a clean component architecture with custom events
for communication between components:
`),
el("h4", t`1. Main Component Event Handling`),
el("p").append(...T`
The main Todos component sets up event listeners to handle actions from child components:
`),
el(code, { content: `
// Event handlers in the main component
const onDelete = on("todo:delete", ev => S.action(todosS, "delete", ev.detail));
const onEdit = on("todo:edit", ev => S.action(todosS, "edit", ev.detail));
`, page_id }),
el("h4", t`2. The TodoItem Component with Scopes and Local State`),
el("p").append(...T`
Each todo item is rendered by the TodoItem component that uses scopes, local signals, and custom events:
`),
el(code, { content: `
/**
* Component for rendering an individual todo item
*
* Features:
* - Display todo with completed state
* - Toggle completion status
* - Delete todo
* - Edit todo with double-click
* - Cancel edit with Escape key
*
* @param {Todo} todo - The todo item data
* @fires {void} todo:delete - todo deletion event
* @fires {Partial<Todo>} todo:edit - todo edits event
*/
function TodoItem({ id, title, completed }) {
const { host }= scope;
const isEditing = S(false);
const isCompleted = S(completed);
/** @type {(id: string) => void} Dispatch function for deleting todo */
const dispatchDelete= dispatchEvent("todo:delete", host);
/** @type {(data: {id: string, [key: string]: any}) => void} Dispatch function for editing todo */
const dispatchEdit = dispatchEvent("todo:edit", host);
// Event handlers that dispatch to parent
/** @type {ddeElementAddon<HTMLInputElement>} */
const onToggleCompleted = on("change", (ev) => {
const completed= /** @type {HTMLInputElement} */(ev.target).checked;
isCompleted.set(completed);
dispatchEdit({ id, completed });
});
/** @type {ddeElementAddon<HTMLButtonElement>} */
const onDelete = on("click", () => dispatchDelete(id));
// Component implementation...
}
`, page_id }),
el("div", { className: "tip" }).append(
el("p").append(...T`
Using ${el("code", "scope")} and ${el("a", references.mdn_events).append("custom events")}
creates a clean separation of concerns. Each TodoItem component dispatches events up to the parent
without directly manipulating the application state, following a unidirectional data flow pattern.
`)
),
el(h3, t`Improved DOM Updates with classList`),
el("p").append(...T`
The implementation uses the reactive ${el("code", "classList")} property for efficient class updates:
`),
el(code, { content: `
// Using classList with signals
return el("li", {
classList: { completed: isCompleted, editing: isEditing }
}).append(
// Component content...
);
`, page_id }),
el("p").append(...T`
Benefits of using ${el("code", "classList")}:
`),
el("ul").append(
el("li", t`More declarative code that clearly shows which classes are conditional`),
el("li", t`Direct binding to signal values for automatic updates`),
el("li", t`Fewer string manipulations and array operations`),
el("li", t`Optimized DOM updates that only change the specific classes that need to change`)
),
el(h3, t`Improved Focus Management`),
el("p").append(...T`
The implementation uses a dedicated function for managing focus in edit inputs:
`),
el(code, { content: `
/**
* Utility function to set focus on an input element
* Uses requestAnimationFrame to ensure the element is rendered
* before trying to focus it
*
* @param {HTMLInputElement} editInput - The input element to focus
* @returns {number} The requestAnimationFrame ID
*/
function addFocus(editInput){
return requestAnimationFrame(()=> {
editInput.focus();
editInput.selectionStart = editInput.selectionEnd = editInput.value.length;
});
}
// Used as an addon to the edit input
el("input", {
className: "edit",
name: "edit",
value: title,
"data-id": id
}, onBlurEdit, onKeyDown, addFocus)
`, page_id }),
el("p").append(...T`
This approach offers several advantages:
`),
el("ul").append(
el("li", t`Uses requestAnimationFrame for reliable focus timing after DOM updates`),
el("li", t`Encapsulates focus logic in a reusable function`),
el("li", t`Attaches directly to the element as an addon function`),
el("li", t`Automatically positions the cursor at the end of the input`)
),
el("div", { className: "note" }).append(
el("p").append(...T`
Using ${el("a", references.mdn_raf).append("requestAnimationFrame")} ensures that the focus operation
happens after the browser has finished rendering the DOM changes, which is more reliable than
using setTimeout.
`)
),
el(h3, t`Efficient Conditional Rendering`),
el("p").append(...T`
The implementation uses signals for efficient conditional rendering:
`),
el("h4", t`Conditional Todo List`),
el(code, { content: `
S.el(todosS, todos => todos.length
? el("main", { className: "main" }).append(
// Main content with toggle all and todo list
)
: el()
)
`, page_id }),
el("h4", t`Conditional Edit Form`),
el(code, { content: `
S.el(isEditing, editing => editing
? el("form", null, onSubmitEdit).append(
el("input", {
className: "edit",
name: "edit",
value: title,
"data-id": id
}, onBlurEdit, onKeyDown, addFocus)
)
: el()
)
`, page_id }),
el("h4", t`Conditional Clear Completed Button`),
el(code, { content: `
S.el(S(() => todosS.get().some(todo => todo.completed)),
hasTodosCompleted=> hasTodosCompleted
? el("button", { textContent: "Clear completed", className: "clear-completed" }, onClearCompleted)
: el()
)
`, page_id }),
el("div", { className: "note" }).append(
el("p").append(...T`
Unlike frameworks that use a virtual DOM, dd<el> directly updates only the specific DOM elements
that need to change. This approach is often more efficient for small to medium-sized applications,
especially when combined with strategic memoization.
`)
),
el(h3, t`Type Safety with JSDoc Comments`),
el("p").append(...T`
The implementation uses comprehensive JSDoc comments to provide type safety without requiring TypeScript:
`),
el(code, { content: `
/**
* Todo item data structure
* @typedef {{ title: string, id: string, completed: boolean }} Todo
*/
/**
* Component for rendering an individual todo item
*
* Features:
* - Display todo with completed state
* - Toggle completion status
* - Delete todo
* - Edit todo with double-click
* - Cancel edit with Escape key
*
* @param {Todo} todo - The todo item data
* @fires {void} todo:delete - todo deletion event
* @fires {Partial<Todo>} todo:edit - todo edits event
*/
function TodoItem({ id, title, completed }) {
// Implementation...
}
/**
* Event handler for keyboard events in edit mode
* @type {ddeElementAddon<HTMLInputElement>}
*/
const onKeyDown = on("keydown", event => {
if (event.key !== "Escape") return;
isEditing.set(false);
});
`, page_id }),
el("div", { className: "tip" }).append(
el("p").append(...T`
Using JSDoc comments provides many of the benefits of TypeScript (autocomplete, type checking,
documentation) while maintaining pure JavaScript code. This approach works well with modern
IDEs that support JSDoc type inference.
`)
),
el(h3, t`Best Practices Demonstrated`),
el("ol").append(
el("li").append(...T`
${el("strong", "Component Composition:")} Breaking the UI into focused, reusable components
`),
el("li").append(...T`
${el("strong", "Performance Optimization:")} Strategic memoization to minimize DOM operations
`),
el("li").append(...T`
${el("strong", "Reactive State Management:")} Using signals with derived computations
`),
el("li").append(...T`
${el("strong", "Event-Based Communication:")} Using custom events for component communication
`),
el("li").append(...T`
${el("strong", "Local Component State:")} Maintaining UI state within components for better encapsulation
`),
el("li").append(...T`
${el("strong", "Declarative Class Management:")} Using the classList property for cleaner class handling
`),
el("li").append(...T`
${el("strong", "Focus Management:")} Reliable input focus with requestAnimationFrame
`),
el("li").append(...T`
${el("strong", "Persistent Storage:")} Automatically saving application state with signal listeners
`),
el("li").append(...T`
${el("strong", "Type Safety:")} Using comprehensive JSDoc comments for type checking and documentation
`),
el("li").append(...T`
${el("strong", "Composable Event Handlers:")} Attaching multiple event handlers to elements
`)
),
el("div", { className: "callout" }).append(
el("h4", t`Key Takeaways`),
el("p").append(...T`
This TodoMVC implementation showcases the strengths of dd<el> for building real-world applications:
`),
el("ul").append(
el("li", t`Clean composition of components with clear responsibilities`),
el("li", t`Precise, targeted DOM updates without virtual DOM overhead`),
el("li", t`Strategic memoization for optimal performance`),
el("li", t`Reactive data flow with signals and derived computations`),
el("li", t`Local component state for better encapsulation`),
el("li", t`Lightweight event system for component communication`)
)
),
el("p").append(...T`
You can find the ${el("a", references.github_example).append("complete source code")} for this example on GitHub.
Feel free to use it as a reference for your own projects or as a starting point for more complex applications.
`),
);
}

View File

@ -26,11 +26,25 @@ export function page({ pkg, info }){
`)
),
el("p").append(...T`
dd<el> isn't limited to browser environments. Thanks to its flexible architecture,
dd<el> isnt limited to browser environments. Thanks to its flexible architecture,
it can be used for server-side rendering (SSR) to generate static HTML files.
This is achieved through integration with for example ${el("a", { href: "https://github.com/tmpvar/jsdom",
textContent: "jsdom" })}, a JavaScript implementation of web standards for Node.js.
textContent: "jsdom" })}, a JavaScript implementation of web standards for Node.js.
`),
el("p").append(...T`
Additionally, you might consider using these alternative solutions:
`),
el("ul").append(
el("li").append(...T`
${el("a", { href: "https://github.com/capricorn86/happy-dom", textContent: "happy-dom" })}
A JavaScript implementation of a web browser without its graphical user interface thats faster than jsdom
`),
el("li").append(...T`
${el("a", { href: "https://github.com/WebReflection/linkedom", textContent: "linkedom" })}
A lightweight DOM implementation specifically designed for SSR with significantly better performance
than jsdom
`),
),
el(code, { src: fileURL("./components/examples/ssr/intro.js"), page_id }),
el(h3, t`Why Server-Side Rendering?`),
@ -48,40 +62,40 @@ export function page({ pkg, info }){
el(h3, t`How jsdom Integration Works`),
el("p").append(...T`
The jsdom export in dd<el> provides the necessary tools to use the library in Node.js
by integrating with jsdom. Here's what it does:
by integrating with jsdom. Heres what it does:
`),
el("ol").append(
el("li", t`Creates a virtual DOM environment in Node.js using jsdom`),
el("li", t`Creates a virtual DOM environment in Node.js using jsdom`),
el("li", t`Registers DOM globals like HTMLElement, document, etc. for dd<el> to use`),
el("li", t`Sets an SSR flag in the environment to enable SSR-specific behaviors`),
el("li", t`Provides a promise queue system for managing async operations during rendering`),
el("li", t`Sets an SSR flag in the environment to enable SSR-specific behaviors`),
el("li", t`Provides a promise queue system for managing async operations during rendering`),
el("li", t`Handles DOM property/attribute mapping differences between browsers and jsdom`)
),
el(code, { src: fileURL("./components/examples/ssr/start.js"), page_id }),
el(h3, t`Basic SSR Example`),
el("p").append(...T`
Here's a simple example of how to use dd<el> for server-side rendering in a Node.js script:
Heres a simple example of how to use dd<el> for server-side rendering in a Node.js script:
`),
el(code, { src: fileURL("./components/examples/ssr/basic-example.js"), page_id }),
el(h3, t`Building a Static Site Generator`),
el(h3, t`Building a Static Site Generator`),
el("p").append(...T`
You can build a complete static site generator with dd<el>. In fact, this documentation site
is built using dd<el> for server-side rendering! Here's how the documentation build process works:
You can build a complete static site generator with dd<el>. In fact, this documentation site
is built using dd<el> for server-side rendering! Heres how the documentation build process works:
`),
el(code, { src: fileURL("./components/examples/ssr/static-site-generator.js"), page_id }),
el(h3, t`Working with Async Content in SSR`),
el("p").append(...T`
The jsdom export includes a queue system to handle asynchronous operations during rendering.
The jsdom export includes a queue system to handle asynchronous operations during rendering.
This is crucial for components that fetch data or perform other async tasks.
`),
el(code, { src: fileURL("./components/examples/ssr/async-data.js"), page_id }),
el(h3, t`Working with Dynamic Imports for SSR`),
el("p").append(...T`
When structuring server-side rendering code, a crucial pattern to follow is using dynamic imports
When structuring server-side rendering code, a crucial pattern to follow is using dynamic imports
for both the deka-dom-el/jsdom module and your page components.
`),
el("p").append(...T`
@ -94,7 +108,7 @@ export function page({ pkg, info }){
`),
el("li").append(...T`
${el("strong", "Environment registration timing:")} The jsdom module auto-registers the DOM environment
when imported, which must happen ${el("em", "after")} you've created your JSDOM instance and
when imported, which must happen ${el("em", "after")} youve created your JSDOM instance and
${el("em", "before")} you import your components using ${el("code", "import { el } from \"deka-dom-el\";")}.
`),
el("li").append(...T`
@ -113,9 +127,9 @@ export function page({ pkg, info }){
`),
el("ul").append(
el("li", t`Browser-specific APIs like window.localStorage are not available in jsdom by default`),
el("li", t`Event listeners added during SSR won't be functional in the final HTML unless hydrated on the client`),
el("li", t`Event listeners added during SSR wont be functional in the final HTML unless hydrated on the client`),
el("li", t`Some DOM features may behave differently in jsdom compared to real browsers`),
el("li", t`For large sites, you may need to optimize memory usage by creating a new jsdom instance for each page`)
el("li", t`For large sites, you may need to optimize memory usage by creating a new jsdom instance for each page`)
),
el("p").append(...T`
For advanced SSR applications, consider implementing hydration on the client-side to restore
@ -124,7 +138,7 @@ export function page({ pkg, info }){
el(h3, t`Real Example: How This Documentation is Built`),
el("p").append(...T`
This documentation site itself is built using dd<el>'s SSR capabilities.
This documentation site itself is built using dd<el>s SSR capabilities.
The build process collects all page components, renders them with jsdom, and outputs static HTML files.
`),
el(code, { src: fileURL("./components/examples/ssr/static-site-generator.js"), page_id }),

335
docs/p12-ireland.html.js Normal file
View File

@ -0,0 +1,335 @@
import { T, t } from "./utils/index.js";
export const info= {
title: t`Ireland Components`,
fullTitle: t`Interactive Demo Components with Server-Side Pre-Rendering`,
description: t`Creating live, interactive component examples in documentation with server-side
rendering and client-side hydration.`,
};
import { el } from "deka-dom-el";
import { simplePage } from "./layout/simplePage.html.js";
import { h3 } from "./components/pageUtils.html.js";
import { code } from "./components/code.html.js";
import { ireland } from "./components/ireland.html.js";
/** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url);
/** @param {import("./types.js").PageAttrs} attrs */
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("div", { className: "warning" }).append(
el("p").append(...T`
This part of the documentation is primarily intended for technical enthusiasts and authors of
3rd-party libraries. It describes an advanced feature, not a core part of the library. Most users will
not need to implement this functionality directly in their applications. This capability will hopefully
be covered by third-party libraries or frameworks that provide simpler SSR integration using
dd<el>.
`)
),
el(h3, t`What Are Ireland Components?`),
el("p").append(...T`
Ireland components are a special type of component that:
`),
el("ul").append(
el("li", t`Pre-render components on the server during SSR build`),
el("li", t`Provide client-side rehydration for the component`),
),
el(h3, t`How Ireland Components Work`),
el("p").append(...T`
The Ireland component system consists of several parts working together:
`),
el("ol").append(
el("li").append(...T`
${el("strong", "Server-side rendering:")} Components are pre-rendered during the documentation build process
`),
el("li").append(...T`
${el("strong", "Component registration:")} Source files are copied to the documentation output directory
`),
el("li").append(...T`
${el("strong", "Client-side scripting:")} JavaScript code is generated to load and render components
`),
),
el(h3, t`Implementation Architecture`),
el("p").append(...T`
The core of the Ireland system is implemented in ${el("code", "docs/components/ireland.html.js")}.
It integrates with the SSR build process using the ${el("code", "registerClientFile")} function
from ${el("code", "docs/ssr.js")}.
`),
el(code, { content: `
// Basic usage of an ireland component
el(ireland, {
src: fileURL("./components/examples/path/to/component.js"),
exportName: "NamedExport", // optional, defaults to "default",
})
`, page_id }),
el("p").append(...T`
During the build process (${el("code", "bs/docs.js")}), the following happens:
`),
el("ol").append(
el("li", t`Component source code is loaded and displayed with syntax highlighting`),
el("li", t`Source files are registered to be copied to the output directory`),
el("li", t`Client-side scripts are generated for each page with ireland components`),
el("li", t`The component is rerendered on the page ready`),
),
el(h3, t`Core Implementation Details`),
el("p").append(...T`
Let's look at the key parts of the ireland component implementation:
`),
el("h4", t`Building SSR`),
el(code, { content: `
// From bs/docs.js - Server-side rendering engine
import { createHTMl } from "./docs/jsdom.js";
import { register, queue } from "../jsdom.js";
import { path_target, dispatchEvent } from "../docs/ssr.js";
// For each page, render it on the server
for(const { id, info } of pages) {
// Create a virtual DOM environment for server-side rendering
const serverDOM = createHTMl("");
serverDOM.registerGlobally("HTMLScriptElement");
// Register dd<el> with the virtual DOM
const { el } = await register(serverDOM.dom);
// Import and render the page component
const { page } = await import(\`../docs/\${id}.html.js\`);
serverDOM.document.body.append(
el(page, { pkg, info }),
);
// Process the queue of asynchronous operations
await queue();
// Trigger render event handlers
dispatchEvent("oneachrender", document);
// Write the HTML to the output file
s.echo(serverDOM.serialize()).to(path_target.root+id+".html");
}
// Final build step - trigger SSR end event
dispatchEvent("onssrend");
`, page_id }),
el("h4", t`File Registration`),
el(code, { content: `
// From docs/ssr.js - File registration system
export function registerClientFile(url, { head, folder = "", replacer } = {}) {
// Ensure folder path ends with a slash
if(folder && !folder.endsWith("/")) folder += "/";
// Extract filename from URL
const file_name = url.pathname.split("/").pop();
// Create target directory if needed
s.mkdir("-p", path_target.root+folder);
// Get file content and apply optional replacer function
let content = s.cat(url);
if(replacer) content = s.echo(replacer(content.toString()));
// Write content to the output directory
content.to(path_target.root+folder+file_name);
// If a head element was provided, add it to the document
if(!head) return;
head[head instanceof HTMLScriptElement ? "src" : "href"] = file_name;
document.head.append(head);
}
`, page_id }),
el("h4", t`Server-Side Rendering`),
el(code, { content: `
// From docs/components/ireland.html.js - Server-side component implementation
export function ireland({ src, exportName = "default", props = {} }) {
// Calculate relative path for imports
const path = "./"+relative(dir, src.pathname);
// Generate unique ID for this component instance
const id = "ireland-" + generateComponentId(src);
// Create placeholder element
const element = el.mark({ type: "later", name: ireland.name });
// Import and render the component during SSR
queue(import(path).then(module => {
const component = module[exportName];
element.replaceWith(el(component, props, mark(id)));
}));
// Register client-side hydration on first component
if(!componentsRegistry.size)
addEventListener("oneachrender", registerClientPart);
// Store component info for client-side hydration
componentsRegistry.set(id, {
src,
path: dirFE+"/"+path.split("/").pop(),
exportName,
props,
});
return element;
}
// Register client-side resources
function registerClientPart() {
// Process all component registrations
const todo = Array.from(componentsRegistry.entries())
.map(([ id, d ]) => {
// Copy the component source file to output directory
registerClientFile(d.src, {
folder: dirFE,
// Replace bare imports for browser compatibility
replacer(file) {
return file.replaceAll(
/ from "deka-dom-el(\/signals)?";/g,
\` from "./esm-with-signals.js";\`
);
}
});
return [ id, d ];
});
// Serialize the component registry for client-side use
const store = JSON.stringify(JSON.stringify(todo));
// Copy client-side scripts to output
registerClientFile(new URL("./ireland.js.js", import.meta.url));
registerClientFile(new URL("../../dist/esm-with-signals.js", import.meta.url), { folder: dirFE });
// Add import map for package resolution
document.head.append(
el("script", { type: "importmap" }).append(\`
{
"imports": {
"deka-dom-el": "./\${dirFE}/esm-with-signals.js",
"deka-dom-el/signals": "./\${dirFE}/esm-with-signals.js"
}
}
\`.trim())
);
// Add bootstrap script to load components
document.body.append(
el("script", { type: "module" }).append(\`
import { loadIrelands } from "./ireland.js.js";
loadIrelands(new Map(JSON.parse(\${store})));
\`.trim())
);
}
`, page_id }),
el("h4", t`Client-Side Hydration`),
el(code, { content: `
// From docs/components/ireland.js.js - Client-side hydration
import { el } from "./irelands/esm-with-signals.js";
export function loadIrelands(store) {
// Find all marked components in the DOM
document.body.querySelectorAll("[data-dde-mark]").forEach(ireland => {
const { ddeMark } = ireland.dataset;
// Skip if this component isnt in our registry
if(!store.has(ddeMark)) return;
// Get component information
const { path, exportName, props } = store.get(ddeMark);
// Dynamically import the component module
import("./" + path).then(module => {
// Replace the server-rendered element with the client-side version
ireland.replaceWith(el(module[exportName], props));
});
});
}
`, page_id }),
el(h3, t`Live Example`),
el("p").append(...T`
Heres a live example of an Ireland component showing a standard counter.
The component is defined in ${el("code", "docs/components/examples/ireland-test/counter.js")} and
rendered with the Ireland component system:
`),
el(code, {
src: fileURL("./components/examples/ireland-test/counter.js"),
page_id
}),
el(ireland, {
src: fileURL("./components/examples/ireland-test/counter.js"),
exportName: "CounterStandard",
page_id
}),
el("p").append(...T`
When the page is loaded, the component is also loaded and rendered. The counter state is maintained
using signals, allowing for reactive updates as you click the buttons to increment and decrement the
value.
`),
el(h3, t`Practical Considerations and Limitations`),
el("p").append(...T`
When implementing Ireland components in real documentation, there are several important
considerations to keep in mind:
`),
el("div", { className: "warning" }).append(
el("h4", t`Module Resolution and Bundling`),
el("p").append(...T`
The examples shown here use bare module specifiers like ${el("code",
`import { el } from "deka-dom-el"`)} which arent supported in all browsers without importmaps.
In a production implementation, you would need to: `),
el("ol").append(
el("li", t`Replace bare import paths with actual paths during the build process`),
el("li", t`Bundle component dependencies to avoid multiple requests`),
el("li", t`Ensure all module dependencies are properly resolved and copied to the output directory`)
),
el("p").append(...T`
In this documentation, we replace the paths with ${el("code", "./esm-with-signals.js")} and provide
a bundled version of the library, but more complex components might require a dedicated bundling step.
`)
),
el("div", { className: "note" }).append(
el("h4", t`Component Dependencies`),
el("p").append(...T`
Real-world components typically depend on multiple modules and assets. The Ireland system would need
to be extended to:
`),
el("ul").append(
el("li", t`Detect and analyze all dependencies of a component`),
el("li", t`Bundle these dependencies together or ensure they're properly copied to the output directory`),
el("li", t`Handle non-JavaScript assets like CSS, images, or data files`)
)
),
el(h3, t`Advanced Usage`),
el("p").append(...T`
The Ireland system can be extended in several ways to address these limitations:
`),
el("ul").append(
el("li", t`Integrate with a bundler like esbuild, Rollup, or Webpack`),
el("li", t`Add props support for configuring components at runtime`),
el("li", t`Implement module caching to reduce network requests`),
el("li", t`Add code editing capabilities for interactive experimentation`),
el("li", t`Support TypeScript and other languages through transpilation`),
el("li", t`Implement state persistence between runs`)
),
el("p").append(...T`
This documentation site itself is built using the techniques described here,
showcasing how dd<el> can be used to create both the documentation and
the interactive examples within it. The implementation here is simplified for clarity,
while a production-ready system would need to address the considerations above.
`)
);
}

383
docs/p13-appendix.html.js Normal file
View File

@ -0,0 +1,383 @@
import { T, t } from "./utils/index.js";
export const info= {
title: t`Appendix & Summary`,
fullTitle: t`dd<el> Comprehensive Reference`,
description: t`A final overview, case studies, key concepts, and best practices for working with deka-dom-el.`,
};
import { el } from "deka-dom-el";
import { simplePage } from "./layout/simplePage.html.js";
import { h3 } from "./components/pageUtils.html.js";
import { code } from "./components/code.html.js";
/** @param {string} url */
const references= {
/** GitHub repository */
github: {
title: t`dd<el> GitHub Repository`,
href: "https://github.com/jaandrle/deka-dom-el",
},
/** TodoMVC */
todomvc: {
title: t`TodoMVC Implementation`,
href: "p10-todomvc.html",
},
/** Performance best practices */
performance: {
title: t`Performance Optimization Guide`,
href: "p09-optimization.html",
}
};
/** @param {import("./types.d.ts").PageAttrs} attrs */
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("p").append(...T`
This reference guide provides a comprehensive summary of dd<el>'s key concepts, best practices,
case studies, and advanced techniques. Use it as a quick reference when working with the library
or to deepen your understanding of its design principles and patterns.
`),
el(h3, t`Core Principles of dd<el>`),
el("p").append(...T`
At its foundation, dd<el> is built on several core principles that shape its design and usage:
`),
el("div", { className: "callout" }).append(
el("h4", t`Guiding Principles`),
el("ul").append(
el("li").append(...T`
${el("strong", "DOM-First Approach:")} Working directly with the real DOM instead of virtual DOM
abstractions
`),
el("li").append(...T`
${el("strong", "Declarative Syntax:")} Creating UIs by describing what they should look like, not
how to create them
`),
el("li").append(...T`
${el("strong", "Minimal Overhead:")} Staying close to standard Web APIs without unnecessary
abstractions
`),
el("li").append(...T`
${el("strong", "Progressive Enhancement:")} Starting simple and adding complexity only when needed
`),
el("li").append(...T`
${el("strong", "Functional Composition:")} Building UIs through function composition rather than
inheritance
`),
el("li").append(...T`
${el("strong", "Clear Patterns:")} Promoting maintainable code organization with the 3PS pattern
`),
el("li").append(...T`
${el("strong", "Targeted Reactivity:")} Using signals for efficient, fine-grained updates
`),
el("li").append(...T`
${el("strong", "Unix Philosophy:")} Doing one thing well and allowing composability with other tools
`)
)
),
el(h3, t`Case Studies & Real-World Applications`),
el("h4", t`TodoMVC Implementation`),
el("p").append(...T`
The ${el("a", references.todomvc).append("TodoMVC implementation")} showcases how dd<el> handles a complete,
real-world application with all standard features of a modern web app:
`),
el("ul").append(
el("li", t`Persistent storage with localStorage`),
el("li", t`Reactive UI with automatic updates`),
el("li", t`Client-side routing with hash-based URLs`),
el("li", t`Component-based architecture`),
el("li", t`Performance optimization with memoization`),
el("li", t`Custom event system for component communication`),
el("li", t`Proper focus management and accessibility`)
),
el("p").append(...T`
Key takeaways from the TodoMVC example:
`),
el("ul").append(
el("li").append(...T`
Signal factories like ${el("code", "routerSignal")} and ${el("code", "todosSignal")}
encapsulate related functionality
`),
el("li").append(...T`
Custom events provide clean communication between components
`),
el("li").append(...T`
Targeted memoization improves rendering performance dramatically
`),
el("li").append(...T`
Derived signals simplify complex UI logic like filtering
`)
),
el("h4", t`Migrating from Traditional Approaches`),
el("p").append(...T`
When migrating from traditional DOM manipulation or other frameworks to dd<el>:
`),
el("ol").append(
el("li").append(...T`
${el("strong", "Start with state:")}: Convert global variables or ad-hoc state to signals
`),
el("li").append(...T`
${el("strong", "Replace query selectors:")}: Replace getElementById/querySelector with direct references to elements
`),
el("li").append(...T`
${el("strong", "Convert imperative updates:")}: Replace manual DOM updates with declarative signal bindings
`),
el("li").append(...T`
${el("strong", "Refactor into components:")}: Organize related UI elements into component functions
`)
),
el(code, { content: `
// Before: Imperative DOM updates
function updateUI() {
document.getElementById('counter').textContent = count;
document.getElementById('status').className = count > 10 ? 'warning' : '';
}
// After: Declarative with dd<el>
const countS = S(0);
el("div").append(
el("span", { id: "counter", textContent: countS }),
el("div", {
id: "status",
className: S(() => countS.get() > 10 ? 'warning' : '')
})
);
`, page_id }),
el(h3, t`Key Concepts Reference`),
el("div", { className: "function-table" }).append(
el("h4", t`Elements & DOM Creation`),
el("dl").append(
el("dt", t`el(tag|component, props, ...addons)`),
el("dd", t`Core function for creating DOM elements with declarative properties`),
el("dt", t`el().append(...children)`),
el("dd", t`Add child elements to a parent element`),
el("dt", t`memo(key, () => element)`),
el("dd", t`Cache and reuse DOM elements for performance optimization`),
el("dt", t`on(eventType, handler)`),
el("dd", t`Attach event handlers to elements as addons`)
)
),
el("div", { className: "function-table" }).append(
el("h4", t`Signals & Reactivity`),
el("dl").append(
el("dt", t`S(initialValue)`),
el("dd", t`Create a signal with an initial value`),
el("dt", t`S(() => computation)`),
el("dd", t`Create a derived signal that updates when dependencies change`),
el("dt", t`S.el(signal, data => element)`),
el("dd", t`Create reactive elements that update when a signal changes`),
el("dt", t`S.action(signal, "method", ...args)`),
el("dd", t`Call custom methods defined on a signal`),
el("dt", t`signal.get()`),
el("dd", t`Get the current value of a signal`),
el("dt", t`signal.set(newValue)`),
el("dd", t`Update a signal's value and trigger reactive updates`)
)
),
el("div", { className: "function-table" }).append(
el("h4", t`Component Patterns`),
el("dl").append(
el("dt", t`Function Components`),
el("dd", t`Javascript functions that return DOM elements`),
el("dt", t`scope Object`),
el("dd", t`Provides access to component context, signal, host element`),
el("dt", t`dispatchEvent(type, element)`),
el("dd", t`Creates a function for dispatching custom events`),
el("dt", t`Signal Factories`),
el("dd", t`Functions that create and configure signals with domain-specific behavior`)
)
),
el(h3, t`Best Practices Summary`),
el("div").append(
el("h4", t`Code Organization`),
el("ul").append(
el("li").append(...T`
${el("strong", "Follow the 3PS pattern:")}: Separate state creation, binding to elements, and state updates
`),
el("li").append(...T`
${el("strong", "Use component functions:")}: Create reusable UI components as functions
`),
el("li").append(...T`
${el("strong", "Create signal factories:")}: Extract reusable signal patterns into factory functions
`),
el("li").append(...T`
${el("strong", "Leverage scopes:")}: Use scope for component context and clean resource management
`),
el("li").append(...T`
${el("strong", "Event delegation:")}: Prefer component-level event handlers over many individual handlers
`)
)
),
el("div").append(
el("h4", t`Performance Optimization`),
el("ul").append(
el("li").append(...T`
${el("strong", "Memoize list items:")}: Use ${el("code", "memo")} for items in frequently-updated lists
`),
el("li").append(...T`
${el("strong", "Avoid unnecessary signal updates:")}: Only update signals when values actually change
`),
el("li").append(...T`
${el("strong", "Use AbortSignals:")}: Clean up resources when components are removed
`),
el("li").append(...T`
${el("strong", "Prefer derived signals:")}: Use computed values instead of manual updates
`),
el("li").append(...T`
${el("strong", "Avoid memoizing fragments:")}: Never memoize DocumentFragments, only individual elements
`)
),
el("p").append(...T`
See the ${el("a", references.performance).append("Performance Optimization Guide")} for detailed strategies.
`)
),
el("div").append(
el("h4", t`Common Pitfalls to Avoid`),
el("dl").append(
el("dt", t`Excessive DOM Manipulation`),
el("dd", t`Let signals handle updates instead of manually manipulating the DOM after creation`),
el("dt", t`Forgetting to Clean Up Resources`),
el("dd", t`Use scope.signal or AbortSignals to handle resource cleanup when elements are removed`),
el("dt", t`Circular Signal Dependencies`),
el("dd", t`Avoid signals that depend on each other in a circular way, which can cause infinite update loops`),
el("dt", t`Memoizing with Unstable Keys`),
el("dd", t`Always use stable, unique identifiers as memo keys, not array indices or objects`),
el("dt", t`Deep Nesting Without Structure`),
el("dd", t`Break deeply nested element structures into smaller, logical component functions`)
)
),
el(h3, t`Feature Comparison with Other Libraries`),
el("table").append(
el("thead").append(
el("tr").append(
el("th", "Feature"),
el("th", "dd<el>"),
el("th", "React"),
el("th", "Vue"),
el("th", "Svelte")
)
),
el("tbody").append(
el("tr").append(
el("td", "No Build Step Required"),
el("td", "✅"),
el("td", "⚠️ JSX needs transpilation"),
el("td", "⚠️ SFC needs compilation"),
el("td", "❌ Requires compilation")
),
el("tr").append(
el("td", "Bundle Size (minimal)"),
el("td", "~10-15kb"),
el("td", "~40kb+"),
el("td", "~33kb+"),
el("td", "Minimal runtime")
),
el("tr").append(
el("td", "Reactivity Model"),
el("td", "Signal-based"),
el("td", "Virtual DOM diffing"),
el("td", "Proxy-based"),
el("td", "Compile-time reactivity")
),
el("tr").append(
el("td", "DOM Interface"),
el("td", "Direct DOM API"),
el("td", "Virtual DOM"),
el("td", "Virtual DOM"),
el("td", "Compiled DOM updates")
),
el("tr").append(
el("td", "Server-Side Rendering"),
el("td", "✅ Basic Support"),
el("td", "✅ Advanced"),
el("td", "✅ Advanced"),
el("td", "✅ Advanced")
)
)
),
el(h3, t`Looking Ahead: Future Directions`),
el("p").append(...T`
The dd<el> library continues to evolve, with several areas of focus for future development:
`),
el("ul").append(
el("li").append(...T`
${el("strong", "Future Compatibility:")} Alignment with the TC39 Signals proposal for native browser support
`),
el("li").append(...T`
${el("strong", "SSR Improvements:")} Enhanced server-side rendering capabilities
`),
el("li").append(...T`
${el("strong", "Ecosystem Growth:")} More utilities, patterns, and integrations with other libraries
`),
el("li").append(...T`
${el("strong", "Documentation Expansion:")} Additional examples, tutorials, and best practices
`),
el("li").append(...T`
${el("strong", "TypeScript Enhancements:")} Improved type definitions and inference
`)
),
el(h3, t`Contribution and Community`),
el("p").append(...T`
dd<el> is an open-source project that welcomes contributions from the community:
`),
el("ul").append(
el("li").append(...T`
${el("a", references.github).append("GitHub Repository")}: Star, fork, and contribute to the project
`),
el("li").append(...T`
Bug reports and feature requests: Open issues on GitHub
`),
el("li").append(...T`
Documentation improvements: Help expand and clarify these guides
`),
el("li").append(...T`
Examples and case studies: Share your implementations and solutions
`)
),
el("div", { className: "callout" }).append(
el("h4", t`Final Thoughts`),
el("p").append(...T`
dd<el> provides a lightweight yet powerful approach to building modern web interfaces
with minimal overhead and maximal flexibility. By embracing standard web technologies
rather than abstracting them away, it offers a development experience that scales
from simple interactive elements to complex applications while remaining close
to what makes the web platform powerful.
`),
el("p").append(...T`
Whether you're building a small interactive component or a full-featured application,
dd<el>'s combination of declarative syntax, targeted reactivity, and pragmatic design
provides the tools you need without the complexity you don't.
`)
),
);
}

82
index.d.ts vendored
View File

@ -7,10 +7,11 @@ type SupportedElement=
| CustomElementTagNameMap[keyof CustomElementTagNameMap]
declare global {
type ddeComponentAttributes= Record<any, any> | undefined;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node>= (element: El)=> any;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node>= (element: El, ...rest: any)=> any;
type ddeString= string | ddeSignal<string, {}>
type ddeStringable= ddeString | number | ddeSignal<number, {}>
}
type Host<EL extends SupportedElement>= (...addons: ddeElementAddon<EL>[])=> EL;
type PascalCase= `${Capitalize<string>}${string}`;
type AttrsModified= {
/**
@ -84,6 +85,7 @@ export namespace el {
is_open?: boolean
): Comment;
}
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement
export function el<
A extends ddeComponentAttributes,
@ -155,7 +157,6 @@ export function elNS(
)=> SupportedElement
export { elNS as createElementNS }
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
/** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
root: EL,
@ -170,14 +171,14 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
body: EL,
): EL
export function dispatchEvent(name: keyof DocumentEventMap | string, element: SupportedElement):
export function dispatchEvent(name: keyof DocumentEventMap | string, host: Host<SupportedElement>):
(data?: any)=> void;
export function dispatchEvent(name: keyof DocumentEventMap | string, options?: EventInit):
(element: SupportedElement, data?: any)=> void;
export function dispatchEvent(
name: keyof DocumentEventMap | string,
options: EventInit | null,
element: SupportedElement | (()=> SupportedElement)
host: Host<SupportedElement>
): (data?: any)=> void;
interface On{
/** Listens to the DOM event. See {@link Document.addEventListener} */
@ -225,7 +226,7 @@ export const on: On;
type Scope= {
scope: Node | Function | Object,
host: ddeElementAddon<any>,
host: Host<SupportedElement>,
custom_element: false | HTMLElement,
prevent: boolean
};
@ -241,7 +242,12 @@ export const scope: {
* It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`.
* */
host: (...addons: ddeElementAddon<SupportedElement>[])=> HTMLElement,
host: Host<SupportedElement>,
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
signal: AbortSignal,
state: Scope[],
/** Adds new child scope. All attributes are inherited by default. */
@ -262,7 +268,6 @@ export function customElementRender<
): EL
export function customElementWithDDE<EL extends (new ()=> HTMLElement)>(custom_element: EL): EL
export function lifecyclesToEvents<EL extends (new ()=> HTMLElement)>(custom_element: EL): EL
export function observedAttributes(custom_element: HTMLElement): Record<string, string>
/**
* This is used primarly for server side rendering. To be sure that all async operations
@ -285,6 +290,69 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
* */
export function queue(promise?: Promise<unknown>): Promise<unknown>;
/**
* Memoization utility for caching DOM elements to improve performance.
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
*
* @param key - Unique identifier for the element (usually an ID or unique value)
* @param generator - Function that creates the element
* @returns The cached element if the key exists, otherwise the result of the generator function
*
* @example
* ```ts
* // Within S.el for list rendering
* S.el(itemsSignal, (items, memo) =>
* el("ul").append(
* ...items.map(item =>
* memo(item.id, () => el(ItemComponent, item))
* )
* )
* )
* ```
*/
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
/**
* Memo namespace containing utility functions for memoization.
*/
export namespace memo {
/**
* Checks if an object is a memo scope.
* @param obj - The object to check
* @returns True if the object is a memo scope
*/
export function isScope(obj: any): boolean;
/**
* Creates a memoized function with optional cleanup support.
*
* @param fun - The function to memoize
* @param options - Configuration options
* @param options.signal - AbortSignal for cleanup
* @param options.onlyLast - When true, only keeps the cache from the most recent call
* @returns A memoized version of the function with a .clear() method
*
* @example
* ```ts
* const renderItems = memo.scope(function(items) {
* return items.map(item =>
* memo(item.id, () => el("div", item.name))
* );
* }, {
* signal: controller.signal,
* onlyLast: true
* });
* ```
*/
export function scope<F extends Function>(
fun: F,
options?: {
signal?: AbortSignal;
onlyLast?: boolean;
}
): F & { clear: () => void };
}
/* TypeScript MEH */
declare global{
type ddeAppend<el>= (...nodes: (Node | string)[])=> el;

View File

@ -1,3 +1,5 @@
export * from "./src/dom.js";
export * from "./src/customElement.js";
export * from "./src/events.js";
export { registerReactivity } from "./src/signals-lib/common.js";
export { memo } from "./src/memo.js";

1
jsdom.d.ts vendored
View File

@ -3,6 +3,7 @@ export * from "./index.d";
type JSDOM= {
window: Window,
document: Document,
Node: typeof Node,
HTMLElement: typeof HTMLElement,
SVGElement: typeof SVGElement,
DocumentFragment: typeof DocumentFragment,

View File

@ -1,4 +1,3 @@
//TODO: https://www.npmjs.com/package/html-element
import { enviroment as env } from './src/dom-common.js';
env.ssr= " ssr";
@ -17,7 +16,9 @@ env.setDeleteAttr= function(obj, prop, value){
if(value) return obj.setAttribute(prop, "");
obj.removeAttribute(prop);
};
const keys= { H: "HTMLElement", S: "SVGElement", F: "DocumentFragment", D: "document" };
const keys= {
N: "Node", H: "HTMLElement", S: "SVGElement", F: "DocumentFragment", D: "document", M: "MutationObserver",
};
let env_bk= {};
let dom_last;

254
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "deka-dom-el",
"version": "0.9.0",
"version": "0.9.1-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "deka-dom-el",
"version": "0.9.0",
"version": "0.9.1-alpha",
"license": "MIT",
"devDependencies": {
"@size-limit/preset-small-lib": "~11.2",
@ -23,14 +23,14 @@
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.3.tgz",
"integrity": "sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.1.tgz",
"integrity": "sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@csstools/css-calc": "^2.1.1",
"@csstools/css-color-parser": "^3.0.7",
"@csstools/css-calc": "^2.1.2",
"@csstools/css-color-parser": "^3.0.8",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"lru-cache": "^10.4.3"
@ -152,9 +152,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
"integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
"cpu": [
"ppc64"
],
@ -169,9 +169,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
"integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
"cpu": [
"arm"
],
@ -186,9 +186,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
"integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
"cpu": [
"arm64"
],
@ -203,9 +203,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
"integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
"cpu": [
"x64"
],
@ -220,9 +220,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
"integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
"cpu": [
"arm64"
],
@ -237,9 +237,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
"integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
"cpu": [
"x64"
],
@ -254,9 +254,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
"integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
"cpu": [
"arm64"
],
@ -271,9 +271,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
"integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
"cpu": [
"x64"
],
@ -288,9 +288,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
"integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
"cpu": [
"arm"
],
@ -305,9 +305,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
"integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
"cpu": [
"arm64"
],
@ -322,9 +322,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
"integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
"cpu": [
"ia32"
],
@ -339,9 +339,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
"integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
"cpu": [
"loong64"
],
@ -356,9 +356,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
"integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
"cpu": [
"mips64el"
],
@ -373,9 +373,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
"integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
"cpu": [
"ppc64"
],
@ -390,9 +390,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
"integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
"cpu": [
"riscv64"
],
@ -407,9 +407,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
"integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
"cpu": [
"s390x"
],
@ -424,9 +424,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
"integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
"cpu": [
"x64"
],
@ -441,9 +441,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
"integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
"cpu": [
"arm64"
],
@ -458,9 +458,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
"integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
"cpu": [
"x64"
],
@ -475,9 +475,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
"integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
"cpu": [
"arm64"
],
@ -492,9 +492,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
"integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
"cpu": [
"x64"
],
@ -509,9 +509,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
"integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
"cpu": [
"x64"
],
@ -526,9 +526,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
"integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
"cpu": [
"arm64"
],
@ -543,9 +543,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
"integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
"cpu": [
"ia32"
],
@ -560,9 +560,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
"integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
"cpu": [
"x64"
],
@ -698,9 +698,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.13.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
"version": "22.13.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1024,13 +1024,13 @@
}
},
"node_modules/cssstyle": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz",
"integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.0.tgz",
"integrity": "sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@asamuzakjp/css-color": "^2.8.2",
"@asamuzakjp/css-color": "^3.1.1",
"rrweb-cssom": "^0.8.0"
},
"engines": {
@ -1276,9 +1276,9 @@
}
},
"node_modules/esbuild": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
"integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -1289,31 +1289,31 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.0",
"@esbuild/android-arm": "0.25.0",
"@esbuild/android-arm64": "0.25.0",
"@esbuild/android-x64": "0.25.0",
"@esbuild/darwin-arm64": "0.25.0",
"@esbuild/darwin-x64": "0.25.0",
"@esbuild/freebsd-arm64": "0.25.0",
"@esbuild/freebsd-x64": "0.25.0",
"@esbuild/linux-arm": "0.25.0",
"@esbuild/linux-arm64": "0.25.0",
"@esbuild/linux-ia32": "0.25.0",
"@esbuild/linux-loong64": "0.25.0",
"@esbuild/linux-mips64el": "0.25.0",
"@esbuild/linux-ppc64": "0.25.0",
"@esbuild/linux-riscv64": "0.25.0",
"@esbuild/linux-s390x": "0.25.0",
"@esbuild/linux-x64": "0.25.0",
"@esbuild/netbsd-arm64": "0.25.0",
"@esbuild/netbsd-x64": "0.25.0",
"@esbuild/openbsd-arm64": "0.25.0",
"@esbuild/openbsd-x64": "0.25.0",
"@esbuild/sunos-x64": "0.25.0",
"@esbuild/win32-arm64": "0.25.0",
"@esbuild/win32-ia32": "0.25.0",
"@esbuild/win32-x64": "0.25.0"
"@esbuild/aix-ppc64": "0.25.1",
"@esbuild/android-arm": "0.25.1",
"@esbuild/android-arm64": "0.25.1",
"@esbuild/android-x64": "0.25.1",
"@esbuild/darwin-arm64": "0.25.1",
"@esbuild/darwin-x64": "0.25.1",
"@esbuild/freebsd-arm64": "0.25.1",
"@esbuild/freebsd-x64": "0.25.1",
"@esbuild/linux-arm": "0.25.1",
"@esbuild/linux-arm64": "0.25.1",
"@esbuild/linux-ia32": "0.25.1",
"@esbuild/linux-loong64": "0.25.1",
"@esbuild/linux-mips64el": "0.25.1",
"@esbuild/linux-ppc64": "0.25.1",
"@esbuild/linux-riscv64": "0.25.1",
"@esbuild/linux-s390x": "0.25.1",
"@esbuild/linux-x64": "0.25.1",
"@esbuild/netbsd-arm64": "0.25.1",
"@esbuild/netbsd-x64": "0.25.1",
"@esbuild/openbsd-arm64": "0.25.1",
"@esbuild/openbsd-x64": "0.25.1",
"@esbuild/sunos-x64": "0.25.1",
"@esbuild/win32-arm64": "0.25.1",
"@esbuild/win32-ia32": "0.25.1",
"@esbuild/win32-x64": "0.25.1"
}
},
"node_modules/escalade": {
@ -2095,9 +2095,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.2.tgz",
"integrity": "sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g==",
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.3.tgz",
"integrity": "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ==",
"dev": true,
"funding": [
{
@ -2738,22 +2738,22 @@
}
},
"node_modules/tldts": {
"version": "6.1.82",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.82.tgz",
"integrity": "sha512-KCTjNL9F7j8MzxgfTgjT+v21oYH38OidFty7dH00maWANAI2IsLw2AnThtTJi9HKALHZKQQWnNebYheadacD+g==",
"version": "6.1.84",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.84.tgz",
"integrity": "sha512-aRGIbCIF3teodtUFAYSdQONVmDRy21REM3o6JnqWn5ZkQBJJ4gHxhw6OfwQ+WkSAi3ASamrS4N4nyazWx6uTYg==",
"dev": true,
"license": "MIT",
"dependencies": {
"tldts-core": "^6.1.82"
"tldts-core": "^6.1.84"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "6.1.82",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.82.tgz",
"integrity": "sha512-Jabl32m21tt/d/PbDO88R43F8aY98Piiz6BVH9ShUlOAiiAELhEqwrAmBocjAqnCfoUeIsRU+h3IEzZd318F3w==",
"version": "6.1.84",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.84.tgz",
"integrity": "sha512-NaQa1W76W2aCGjXybvnMYzGSM4x8fvG2AN/pla7qxcg0ZHbooOPhA8kctmOZUDfZyhDL27OGNbwAeig8P4p1vg==",
"dev": true,
"license": "MIT"
},

View File

@ -1,6 +1,6 @@
{
"name": "deka-dom-el",
"version": "0.9.0",
"version": "0.9.1-alpha",
"description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.",
"author": "Jan Andrle <andrle.jan@centrum.cz>",
"license": "MIT",
@ -36,7 +36,8 @@
"files": [
"*.js",
"*.d.ts",
"src"
"src",
"tsconfig.json"
],
"engines": {
"node": ">=18"

3
signals.d.ts vendored
View File

@ -12,6 +12,7 @@ type Action<V>= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typ
type SymbolOnclear= symbol;
type Actions<V>= Record<string | SymbolOnclear, Action<V>>;
type OnListenerOptions= Pick<AddEventListenerOptions, "signal"> & { first_time?: boolean };
type SElement= Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
interface signal{
_: Symbol
/**
@ -61,7 +62,7 @@ interface signal{
* S.el(listS, list=> list.map(li=> el("li", li)));
* ```
* */
el<S extends any>(signal: Signal<S, any>, el: (v: S)=> Element | Element[] | DocumentFragment): DocumentFragment;
el<S extends any>(signal: Signal<S, any>, el: (v: S)=> SElement | SElement[]): DocumentFragment;
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
}

View File

@ -7,10 +7,10 @@ import { c_ch_o } from "./events-observer.js";
*
* @param {Element|ShadowRoot} target - The custom element or shadow root to render into
* @param {Function} render - The render function that returns content
* @param {Function|Object} [props=observedAttributes] - Props to pass to the render function
* @param {Function|Object} [props= {}] - Props to pass to the render function
* @returns {Node} The rendered content
*/
export function customElementRender(target, render, props= observedAttributes){
export function customElementRender(target, render, props= {}){
const custom_element= target.host || target;
scope.push({
scope: custom_element,
@ -69,15 +69,3 @@ export { lifecyclesToEvents as customElementWithDDE };
function wrapMethod(obj, method, apply){
obj[method]= new Proxy(obj[method] || (()=> {}), { apply });
}
import { observedAttributes as oA } from "./helpers.js";
/**
* Gets observed attributes for a custom element
*
* @param {Element} instance - Custom element instance
* @returns {Object} Object mapping camelCase attribute names to their values
*/
export function observedAttributes(instance){
return oA(instance, (i, n)=> i.getAttribute(n));
}

View File

@ -14,6 +14,7 @@ export const enviroment= {
setDeleteAttr,
ssr: "",
D: globalThis.document,
N: globalThis.Node,
F: globalThis.DocumentFragment,
H: globalThis.HTMLElement,
S: globalThis.SVGElement,

View File

@ -1,6 +1,7 @@
import { signals } from "./signals-lib/common.js";
import { enviroment as env } from './dom-common.js';
import { isInstance, isUndef, oAssign } from "./helpers.js";
import { on } from "./events.js";
/**
* Queues a promise, this is helpful for crossplatform components (on server side we can wait for all registered
@ -19,6 +20,8 @@ const scopes= [ {
host: c=> c ? c(env.D.body) : env.D.body,
prevent: true,
} ];
/** Store for disconnect abort controllers */
const store_abort= new WeakMap();
/**
* Scope management utility for tracking component hierarchies
*/
@ -35,6 +38,19 @@ export const scope= {
*/
get host(){ return this.current.host; },
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
get signal(){
const { host }= this;
if(store_abort.has(host)) return store_abort.get(host);
const a= new AbortController();
store_abort.set(host, a);
host(on.disconnected(()=> a.abort()));
return a.signal;
},
/**
* Prevents default behavior in the current scope
* @returns {Object} Current scope context

View File

@ -167,9 +167,9 @@ function connectionsChangesObserverConstructor(){
if(store.size > 30)//TODO?: limit
await requestIdle();
const out= [];
if(!isInstance(element, Node)) return out;
if(!isInstance(element, env.N)) return out;
for(const el of store.keys()){
if(el===element || !isInstance(el, Node)) continue;
if(el===element || !isInstance(el, env.N)) continue;
if(element.contains(el))
out.push(el);
}
@ -214,6 +214,7 @@ function connectionsChangesObserverConstructor(){
const ls= store.get(element);
if(!ls.length_d) continue;
// support for S.el, see https://vuejs.org/guide/extras/web-components.html#lifecycle
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
out= true;
}

View File

@ -1,5 +1,4 @@
export { registerReactivity } from './signals-lib/common.js';
import { enviroment as env, keyLTE, evc, evd, eva } from './dom-common.js';
import { keyLTE, evc, evd } from './dom-common.js';
import { oAssign, onAbort } from './helpers.js';
/**
@ -48,7 +47,6 @@ import { c_ch_o } from "./events-observer.js";
const lifeOptions= obj=> oAssign({}, typeof obj==="object" ? obj : null, { once: true });
//TODO: cleanUp when event before abort?
//TODO: docs (e.g.) https://nolanlawson.com/2024/01/13/web-component-gotcha-constructor-vs-connectedcallback/
/**
* Creates a function to register connected lifecycle event listeners
@ -88,53 +86,3 @@ on.disconnected= function(listener, options){
return element;
};
};
/** Store for disconnect abort controllers */
const store_abort= new WeakMap();
/**
* Creates an AbortController that triggers when the element disconnects
*
* @param {Function} host - Host element or function taking an element
* @returns {AbortSignal} AbortSignal that aborts on disconnect
*/
on.disconnectedAsAbort= function(host){
if(store_abort.has(host)) return store_abort.get(host);
const a= new AbortController();
store_abort.set(host, a);
host(on.disconnected(()=> a.abort()));
return a.signal;
};
/** Store for elements with attribute observers */
const els_attribute_store= new WeakSet();
/**
* Creates a function to register attribute change event listeners
*
* @param {Function} listener - Event handler
* @param {Object} [options] - Event listener options
* @returns {Function} Function that registers the attribute change listener
*/
on.attributeChanged= function(listener, options){
if(typeof options !== "object")
options= {};
return function registerElement(element){
element.addEventListener(eva, listener, options);
if(element[keyLTE] || els_attribute_store.has(element))
return element;
if(!env.M) return element;
const observer= new env.M(function(mutations){
for(const { attributeName, target } of mutations)
target.dispatchEvent(
new CustomEvent(eva, { detail: [ attributeName, target.getAttribute(attributeName) ] }));
});
const c= onAbort(options.signal, ()=> observer.disconnect());
if(c) observer.observe(element, { attributes: true });
//TODO: clean up when element disconnected
return element;
};
};

50
src/memo.js Normal file
View File

@ -0,0 +1,50 @@
import { hasOwn, oCreate } from "./helpers.js";
const memoMark= "__dde_memo";
const memo_scope= [];
/**
* ```js
* const fun= memo.scope(function (list){
* return list.map(item=> memo(item.key, ()=> el(heavy, item.title)));
* }, { onlyLast: true });
* ```
* this is internally used in `S.el`:
* ```
* S.el(listSignal, list=>
* list.map(item=> memo(item.key, ()=>
* el(heavy, item.title))));
* ```
* */
export function memo(key, generator){
if(!memo_scope.length) return generator(key);
const k= typeof key === "object" ? JSON.stringify(key) : key;
const [ { cache, after } ]= memo_scope;
return after(k, hasOwn(cache, k) ? cache[k] : generator(key));
}
memo.isScope= function(obj){ return obj[memoMark]; };
/**
* @param {Function} fun
* @param {Object} [options={}]
* @param {AbortSignal} options.signal
* @param {boolean} [options.onlyLast=false]
* */
memo.scope= function memoScope(fun, { signal, onlyLast }= {}){
let cache= oCreate();
function memoScope(...args){
if(signal && signal.aborted)
return fun.apply(this, args);
let cache_local= onlyLast ? cache : oCreate();
memo_scope.unshift({
cache,
after(key, val){ return (cache_local[key]= val); }
});
const out= fun.apply(this, args);
memo_scope.shift();
cache= cache_local;
return out;
}
memoScope[memoMark]= true;
memoScope.clear= ()=> cache= oCreate();
if(signal) signal.addEventListener("abort", memoScope.clear);
return memoScope;
};

View File

@ -159,38 +159,31 @@ signal.clear= function(...signals){
};
/** Property key for tracking reactive elements */
const key_reactive= "__dde_reactive";
import { enviroment as env } from "../dom-common.js";
import { enviroment as env, eva } from "../dom-common.js";
import { el } from "../dom.js";
import { scope } from "../dom.js";
import { on } from "../events.js";
import { memo } from "../memo.js";
export function cache(store= oCreate()){
return (key, fun)=> hasOwn(store, key) ? store[key] : (store[key]= fun());
}
/**
* Creates a reactive DOM element that re-renders when signal changes
*
* @TODO Third argument for handle `cache_tmp` in re-render
* @param {Object} s - Signal object to watch
* @param {Function} map - Function mapping signal value to DOM elements
* @returns {DocumentFragment} Fragment containing reactive elements
*/
signal.el= function(s, map){
map= memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start= el.mark({ type: "reactive", source: new Defined().compact }, true);
const mark_end= mark_start.end;
const out= env.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current }= scope;
let cache_shared= oCreate();
const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered
return removeSignalListener(s, reRenderReactiveElement);
const memo= cache(cache_shared);
cache_shared= oCreate();
scope.push(current);
let els= map(v, function useCache(key, fun){
return (cache_shared[key]= memo(key, fun));
});
let els= map(v);
scope.pop();
if(!Array.isArray(els))
els= [ els ];
@ -209,7 +202,7 @@ signal.el= function(s, map){
reRenderReactiveElement(s.get());
current.host(on.disconnected(()=>
/*! Clears cached elements for reactive element `S.el` */
cache_shared= {}
map.clear()
));
return out;
};
@ -265,7 +258,7 @@ const key_attributes= "__dde_attributes";
signal.observedAttributes= function(element){
const store= element[key_attributes]= {};
const attrs= observedAttributes(element, observedAttribute(store));
on.attributeChanged(function attributeChangeToSignal({ detail }){
on(eva, function attributeChangeToSignal({ detail }){
/*! This maps attributes to signals (`S.observedAttributes`).
Investigate `__dde_attributes` key of the element. */
const [ name, value ]= detail;