1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-04-02 20:15: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 - ☑️ **Lightweight** — ~10-15kB minified (original goal 10kB) with zero/minimal dependencies
- ✅ **Declarative & functional approach** for clean, maintainable code - ✅ **Declarative & functional approach** for clean, maintainable code
- ✅ **Signals and events** for reactive UI - ✅ **Signals and events** for reactive UI
- ✅ **Memoization for performance** — optimize rendering with intelligent caching
- ✅ **Optional build-in signals** with support for custom reactive implementations - ✅ **Optional build-in signals** with support for custom reactive implementations
- ✅ **Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom) - ✅ **Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom)
- ✅ **TypeScript support** (work in progress) - ✅ **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 - [potch/signals](https://github.com/potch/signals) - A small reactive signals library
- [jaandrle/dollar_dom_component](https://github.com/jaandrle/dollar_dom_component) - - [jaandrle/dollar_dom_component](https://github.com/jaandrle/dollar_dom_component) -
Functional DOM components without JSX/virtual DOM 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("") $.api("")
.command("main", "Build main files", { default: true }) .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({ const regular = await build({
files, files,
filesOut, filesOut,
minify: "no", minify: "no",
types,
}); });
const min = await build({ const min = await build({
files, files,
@ -18,6 +20,7 @@ $.api("")
return out.slice(0, idx)+".min"+out.slice(idx); return out.slice(0, idx)+".min"+out.slice(idx);
}, },
minify: "full", minify: "full",
types,
}); });
return $.exit(regular + min); return $.exit(regular + min);
}) })

View File

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

View File

@ -1,5 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -eou pipefail 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 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"> & { export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean; first_time?: boolean;
}; };
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal { export interface signal {
_: Symbol; _: Symbol;
/** /**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li))); * 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, {}>>; observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
} }
export const signal: signal; 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]; export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global { declare global {
type ddeComponentAttributes = Record<any, any> | undefined; 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 ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>; type ddeStringable = ddeString | number | Signal<number, {}>;
} }
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`; export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = { export type AttrsModified = {
/** /**
@ -145,6 +147,7 @@ export namespace el {
host?: "this" | "parentElement"; host?: "this" | "parentElement";
}, is_open?: boolean): Comment; }, 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 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 { export function el<A extends {
textContent: ddeStringable; 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; [key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement; }>, ...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 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 */ /** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL; 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 * @param body Body of the custom element
* */ * */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL; 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): (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 { export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */ /** 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; <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 const on: On;
export type Scope = { export type Scope = {
scope: Node | Function | Object; scope: Node | Function | Object;
host: ddeElementAddon<any>; host: Host<SupportedElement>;
custom_element: false | HTMLElement; custom_element: false | HTMLElement;
prevent: boolean; 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) * It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`. * `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[]; state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */ /** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>; 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 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 customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<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 * 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. * 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>; 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 */ /* TypeScript MEH */
declare global { declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el; type ddeAppend<el> = (...nodes: (Node | string)[]) => el;

View File

@ -32,8 +32,8 @@ function onAbort(signal2, listener) {
}; };
} }
function observedAttributes(instance, observedAttribute2) { function observedAttributes(instance, observedAttribute2) {
const { observedAttributes: observedAttributes3 = [] } = instance.constructor; const { observedAttributes: observedAttributes2 = [] } = instance.constructor;
return observedAttributes3.reduce(function(out, name) { return observedAttributes2.reduce(function(out, name) {
out[kebabToCamel(name)] = observedAttribute2(instance, name); out[kebabToCamel(name)] = observedAttribute2(instance, name);
return out; return out;
}, {}); }, {});
@ -91,6 +91,7 @@ var enviroment = {
setDeleteAttr, setDeleteAttr,
ssr: "", ssr: "",
D: globalThis.document, D: globalThis.document,
N: globalThis.Node,
F: globalThis.DocumentFragment, F: globalThis.DocumentFragment,
H: globalThis.HTMLElement, H: globalThis.HTMLElement,
S: globalThis.SVGElement, S: globalThis.SVGElement,
@ -111,6 +112,215 @@ var evc = "dde:connected";
var evd = "dde:disconnected"; var evd = "dde:disconnected";
var eva = "dde:attributeChanged"; 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 // src/dom.js
function queue(promise) { function queue(promise) {
return enviroment.q(promise); return enviroment.q(promise);
@ -122,6 +332,7 @@ var scopes = [{
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body, host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
prevent: true prevent: true
}]; }];
var store_abort = /* @__PURE__ */ new WeakMap();
var scope = { var scope = {
/** /**
* Gets the current scope * Gets the current scope
@ -138,6 +349,17 @@ var scope = {
return this.current.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 * Prevents default behavior in the current scope
* @returns {Object} Current scope context * @returns {Object} Current scope context
*/ */
@ -380,172 +602,8 @@ function setDelete(obj, key, val) {
return Reflect.deleteProperty(obj, key); 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 // src/customElement.js
function customElementRender(target, render, props = observedAttributes2) { function customElementRender(target, render, props = {}) {
const custom_element = target.host || target; const custom_element = target.host || target;
scope.push({ scope.push({
scope: custom_element, scope: custom_element,
@ -586,81 +644,40 @@ function wrapMethod(obj, method, apply) {
obj[method] = new Proxy(obj[method] || (() => { obj[method] = new Proxy(obj[method] || (() => {
}), { apply }); }), { apply });
} }
function observedAttributes2(instance) {
return observedAttributes(instance, (i, n) => i.getAttribute(n));
}
// src/events.js // src/memo.js
function dispatchEvent(name, options, host) { var memoMark = "__dde_memo";
if (typeof options === "function") { var memo_scope = [];
host = options; function memo(key, generator) {
options = null; if (!memo_scope.length) return generator(key);
} const k = typeof key === "object" ? JSON.stringify(key) : key;
if (!options) options = {}; const [{ cache, after }] = memo_scope;
return function dispatch(element, ...d) { return after(k, hasOwn(cache, k) ? cache[k] : generator(key));
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) { memo.isScope = function(obj) {
return function registerElement(element) { return obj[memoMark];
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) { memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
options = lifeOptions(options); let cache = oCreate();
return function registerElement(element) { function memoScope2(...args) {
element.addEventListener(evd, listener, options); if (signal2 && signal2.aborted)
if (element[keyLTE]) return element; return fun.apply(this, args);
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener)); let cache_local = onlyLast ? cache : oCreate();
if (c) c_ch_o.onDisconnected(element, listener); memo_scope.unshift({
return element; cache,
}; after(key, val) {
}; return cache_local[key] = val;
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)] })
);
}); });
const c = onAbort(options.signal, () => observer.disconnect()); const out = fun.apply(this, args);
if (c) observer.observe(element, { attributes: true }); memo_scope.shift();
return element; 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 // src/signals-lib/helpers.js
@ -776,25 +793,18 @@ signal.clear = function(...signals2) {
} }
}; };
var key_reactive = "__dde_reactive"; 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) { 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_start = createElement.mark({ type: "reactive", source: new Defined().compact }, true);
const mark_end = mark_start.end; const mark_end = mark_start.end;
const out = enviroment.D.createDocumentFragment(); const out = enviroment.D.createDocumentFragment();
out.append(mark_start, mark_end); out.append(mark_start, mark_end);
const { current } = scope; const { current } = scope;
let cache_shared = oCreate();
const reRenderReactiveElement = (v) => { const reRenderReactiveElement = (v) => {
if (!mark_start.parentNode || !mark_end.parentNode) if (!mark_start.parentNode || !mark_end.parentNode)
return removeSignalListener(s, reRenderReactiveElement); return removeSignalListener(s, reRenderReactiveElement);
const memo = cache(cache_shared);
cache_shared = oCreate();
scope.push(current); scope.push(current);
let els = map(v, function useCache(key, fun) { let els = map(v);
return cache_shared[key] = memo(key, fun);
});
scope.pop(); scope.pop();
if (!Array.isArray(els)) if (!Array.isArray(els))
els = [els]; els = [els];
@ -814,7 +824,7 @@ signal.el = function(s, map) {
current.host(on.disconnected( current.host(on.disconnected(
() => ( () => (
/*! Clears cached elements for reactive element `S.el` */ /*! Clears cached elements for reactive element `S.el` */
cache_shared = {} map.clear()
) )
)); ));
return out; return out;
@ -846,7 +856,7 @@ var key_attributes = "__dde_attributes";
signal.observedAttributes = function(element) { signal.observedAttributes = function(element) {
const store = element[key_attributes] = {}; const store = element[key_attributes] = {};
const attrs = observedAttributes(element, observedAttribute(store)); const attrs = observedAttributes(element, observedAttribute(store));
on.attributeChanged(function attributeChangeToSignal({ detail }) { on(eva, function attributeChangeToSignal({ detail }) {
/*! This maps attributes to signals (`S.observedAttributes`). /*! This maps attributes to signals (`S.observedAttributes`).
Investigate `__dde_attributes` key of the element. */ Investigate `__dde_attributes` key of the element. */
const [name, value] = detail; const [name, value] = detail;
@ -997,7 +1007,7 @@ export {
elementAttribute, elementAttribute,
isSignal, isSignal,
lifecyclesToEvents, lifecyclesToEvents,
observedAttributes2 as observedAttributes, memo,
on, on,
queue, queue,
registerReactivity, registerReactivity,

View File

@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & { export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean; first_time?: boolean;
}; };
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal { export interface signal {
_: Symbol; _: Symbol;
/** /**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li))); * 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, {}>>; observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
} }
export const signal: signal; 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]; export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global { declare global {
type ddeComponentAttributes = Record<any, any> | undefined; 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 ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>; type ddeStringable = ddeString | number | Signal<number, {}>;
} }
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`; export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = { export type AttrsModified = {
/** /**
@ -145,6 +147,7 @@ export namespace el {
host?: "this" | "parentElement"; host?: "this" | "parentElement";
}, is_open?: boolean): Comment; }, 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 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 { export function el<A extends {
textContent: ddeStringable; 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; [key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement; }>, ...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 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 */ /** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL; 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 * @param body Body of the custom element
* */ * */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL; 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): (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 { export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */ /** 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; <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 const on: On;
export type Scope = { export type Scope = {
scope: Node | Function | Object; scope: Node | Function | Object;
host: ddeElementAddon<any>; host: Host<SupportedElement>;
custom_element: false | HTMLElement; custom_element: false | HTMLElement;
prevent: boolean; 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) * It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`. * `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[]; state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */ /** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>; 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 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 customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<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 * 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. * 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>; 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 */ /* TypeScript MEH */
declare global { declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el; 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"> & { export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean; first_time?: boolean;
}; };
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal { export interface signal {
_: Symbol; _: Symbol;
/** /**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li))); * 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, {}>>; observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
} }
declare const signal: signal; 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]; export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global { declare global {
type ddeComponentAttributes = Record<any, any> | undefined; 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 ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>; type ddeStringable = ddeString | number | Signal<number, {}>;
} }
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`; export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = { export type AttrsModified = {
/** /**
@ -144,6 +146,7 @@ export namespace el {
host?: "this" | "parentElement"; host?: "this" | "parentElement";
}, is_open?: boolean): Comment; }, 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 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 { export function el<A extends {
textContent: ddeStringable; 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; [key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement; }>, ...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 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 */ /** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL; 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 * @param body Body of the custom element
* */ * */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL; 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): (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 { export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */ /** 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; <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 const on: On;
export type Scope = { export type Scope = {
scope: Node | Function | Object; scope: Node | Function | Object;
host: ddeElementAddon<any>; host: Host<SupportedElement>;
custom_element: false | HTMLElement; custom_element: false | HTMLElement;
prevent: boolean; 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) * It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`. * `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[]; state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */ /** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>; 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 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 customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<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 * 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. * 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>; 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 */ /* TypeScript MEH */
declare global { declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el; type ddeAppend<el> = (...nodes: (Node | string)[]) => el;

505
dist/esm.js vendored
View File

@ -1,4 +1,5 @@
// src/helpers.js // src/helpers.js
var hasOwn = (...a) => Object.prototype.hasOwnProperty.call(...a);
function isUndef(value) { function isUndef(value) {
return typeof value === "undefined"; return typeof value === "undefined";
} }
@ -8,6 +9,9 @@ function isInstance(obj, cls) {
function isProtoFrom(obj, cls) { function isProtoFrom(obj, cls) {
return Object.prototype.isPrototypeOf.call(cls, obj); return Object.prototype.isPrototypeOf.call(cls, obj);
} }
function oCreate(proto = null, p = {}) {
return Object.create(proto, p);
}
function oAssign(...o) { function oAssign(...o) {
return Object.assign(...o); return Object.assign(...o);
} }
@ -21,16 +25,6 @@ function onAbort(signal, listener) {
signal.removeEventListener("abort", 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 // src/signals-lib/common.js
var signals_global = { var signals_global = {
@ -68,6 +62,7 @@ var enviroment = {
setDeleteAttr, setDeleteAttr,
ssr: "", ssr: "",
D: globalThis.document, D: globalThis.document,
N: globalThis.Node,
F: globalThis.DocumentFragment, F: globalThis.DocumentFragment,
H: globalThis.HTMLElement, H: globalThis.HTMLElement,
S: globalThis.SVGElement, S: globalThis.SVGElement,
@ -88,6 +83,215 @@ var evc = "dde:connected";
var evd = "dde:disconnected"; var evd = "dde:disconnected";
var eva = "dde:attributeChanged"; 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 // src/dom.js
function queue(promise) { function queue(promise) {
return enviroment.q(promise); return enviroment.q(promise);
@ -99,6 +303,7 @@ var scopes = [{
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body, host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
prevent: true prevent: true
}]; }];
var store_abort = /* @__PURE__ */ new WeakMap();
var scope = { var scope = {
/** /**
* Gets the current scope * Gets the current scope
@ -115,6 +320,17 @@ var scope = {
return this.current.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 * Prevents default behavior in the current scope
* @returns {Object} Current scope context * @returns {Object} Current scope context
*/ */
@ -357,172 +573,8 @@ function setDelete(obj, key, val) {
return Reflect.deleteProperty(obj, key); 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 // src/customElement.js
function customElementRender(target, render, props = observedAttributes2) { function customElementRender(target, render, props = {}) {
const custom_element = target.host || target; const custom_element = target.host || target;
scope.push({ scope.push({
scope: custom_element, scope: custom_element,
@ -563,81 +615,40 @@ function wrapMethod(obj, method, apply) {
obj[method] = new Proxy(obj[method] || (() => { obj[method] = new Proxy(obj[method] || (() => {
}), { apply }); }), { apply });
} }
function observedAttributes2(instance) {
return observedAttributes(instance, (i, n) => i.getAttribute(n));
}
// src/events.js // src/memo.js
function dispatchEvent(name, options, host) { var memoMark = "__dde_memo";
if (typeof options === "function") { var memo_scope = [];
host = options; function memo(key, generator) {
options = null; if (!memo_scope.length) return generator(key);
} const k = typeof key === "object" ? JSON.stringify(key) : key;
if (!options) options = {}; const [{ cache, after }] = memo_scope;
return function dispatch(element, ...d) { return after(k, hasOwn(cache, k) ? cache[k] : generator(key));
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) { memo.isScope = function(obj) {
return function registerElement(element) { return obj[memoMark];
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) { memo.scope = function memoScope(fun, { signal, onlyLast } = {}) {
options = lifeOptions(options); let cache = oCreate();
return function registerElement(element) { function memoScope2(...args) {
element.addEventListener(evd, listener, options); if (signal && signal.aborted)
if (element[keyLTE]) return element; return fun.apply(this, args);
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener)); let cache_local = onlyLast ? cache : oCreate();
if (c) c_ch_o.onDisconnected(element, listener); memo_scope.unshift({
return element; cache,
}; after(key, val) {
}; return cache_local[key] = val;
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)] })
);
}); });
const c = onAbort(options.signal, () => observer.disconnect()); const out = fun.apply(this, args);
if (c) observer.observe(element, { attributes: true }); memo_scope.shift();
return element; cache = cache_local;
}; return out;
}
memoScope2[memoMark] = true;
memoScope2.clear = () => cache = oCreate();
if (signal) signal.addEventListener("abort", memoScope2.clear);
return memoScope2;
}; };
export { export {
assign, assign,
@ -653,7 +664,7 @@ export {
createElementNS as elNS, createElementNS as elNS,
elementAttribute, elementAttribute,
lifecyclesToEvents, lifecyclesToEvents,
observedAttributes2 as observedAttributes, memo,
on, on,
queue, queue,
registerReactivity, 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"> & { export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean; first_time?: boolean;
}; };
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal { export interface signal {
_: Symbol; _: Symbol;
/** /**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li))); * 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, {}>>; observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
} }
declare const signal: signal; 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]; export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global { declare global {
type ddeComponentAttributes = Record<any, any> | undefined; 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 ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>; type ddeStringable = ddeString | number | Signal<number, {}>;
} }
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`; export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = { export type AttrsModified = {
/** /**
@ -144,6 +146,7 @@ export namespace el {
host?: "this" | "parentElement"; host?: "this" | "parentElement";
}, is_open?: boolean): Comment; }, 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 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 { export function el<A extends {
textContent: ddeStringable; 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; [key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement; }>, ...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 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 */ /** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL; 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 * @param body Body of the custom element
* */ * */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL; 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): (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 { export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */ /** 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; <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 const on: On;
export type Scope = { export type Scope = {
scope: Node | Function | Object; scope: Node | Function | Object;
host: ddeElementAddon<any>; host: Host<SupportedElement>;
custom_element: false | HTMLElement; custom_element: false | HTMLElement;
prevent: boolean; 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) * It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`. * `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[]; state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */ /** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>; 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 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 customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<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 * 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. * 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>; 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 */ /* TypeScript MEH */
declare global { declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el; 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"> & { export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean; first_time?: boolean;
}; };
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal { export interface signal {
_: Symbol; _: Symbol;
/** /**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li))); * 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, {}>>; observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
} }
export const signal: signal; 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]; export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global { declare global {
type ddeComponentAttributes = Record<any, any> | undefined; 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 ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>; type ddeStringable = ddeString | number | Signal<number, {}>;
} }
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`; export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = { export type AttrsModified = {
/** /**
@ -145,6 +147,7 @@ export namespace el {
host?: "this" | "parentElement"; host?: "this" | "parentElement";
}, is_open?: boolean): Comment; }, 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 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 { export function el<A extends {
textContent: ddeStringable; 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; [key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement; }>, ...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 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 */ /** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL; 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 * @param body Body of the custom element
* */ * */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL; 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): (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 { export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */ /** 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; <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 const on: On;
export type Scope = { export type Scope = {
scope: Node | Function | Object; scope: Node | Function | Object;
host: ddeElementAddon<any>; host: Host<SupportedElement>;
custom_element: false | HTMLElement; custom_element: false | HTMLElement;
prevent: boolean; 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) * It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`. * `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[]; state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */ /** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>; 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 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 customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<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 * 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. * 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>; 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 */ /* TypeScript MEH */
declare global { declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el; type ddeAppend<el> = (...nodes: (Node | string)[]) => el;

View File

@ -35,7 +35,7 @@ var DDE = (() => {
elementAttribute: () => elementAttribute, elementAttribute: () => elementAttribute,
isSignal: () => isSignal, isSignal: () => isSignal,
lifecyclesToEvents: () => lifecyclesToEvents, lifecyclesToEvents: () => lifecyclesToEvents,
observedAttributes: () => observedAttributes2, memo: () => memo,
on: () => on, on: () => on,
queue: () => queue, queue: () => queue,
registerReactivity: () => registerReactivity, registerReactivity: () => registerReactivity,
@ -78,8 +78,8 @@ var DDE = (() => {
}; };
} }
function observedAttributes(instance, observedAttribute2) { function observedAttributes(instance, observedAttribute2) {
const { observedAttributes: observedAttributes3 = [] } = instance.constructor; const { observedAttributes: observedAttributes2 = [] } = instance.constructor;
return observedAttributes3.reduce(function(out, name) { return observedAttributes2.reduce(function(out, name) {
out[kebabToCamel(name)] = observedAttribute2(instance, name); out[kebabToCamel(name)] = observedAttribute2(instance, name);
return out; return out;
}, {}); }, {});
@ -137,6 +137,7 @@ var DDE = (() => {
setDeleteAttr, setDeleteAttr,
ssr: "", ssr: "",
D: globalThis.document, D: globalThis.document,
N: globalThis.Node,
F: globalThis.DocumentFragment, F: globalThis.DocumentFragment,
H: globalThis.HTMLElement, H: globalThis.HTMLElement,
S: globalThis.SVGElement, S: globalThis.SVGElement,
@ -157,6 +158,215 @@ var DDE = (() => {
var evd = "dde:disconnected"; var evd = "dde:disconnected";
var eva = "dde:attributeChanged"; 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 // src/dom.js
function queue(promise) { function queue(promise) {
return enviroment.q(promise); return enviroment.q(promise);
@ -168,6 +378,7 @@ var DDE = (() => {
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body, host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
prevent: true prevent: true
}]; }];
var store_abort = /* @__PURE__ */ new WeakMap();
var scope = { var scope = {
/** /**
* Gets the current scope * Gets the current scope
@ -184,6 +395,17 @@ var DDE = (() => {
return this.current.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 * Prevents default behavior in the current scope
* @returns {Object} Current scope context * @returns {Object} Current scope context
*/ */
@ -426,172 +648,8 @@ var DDE = (() => {
return Reflect.deleteProperty(obj, key); 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 // src/customElement.js
function customElementRender(target, render, props = observedAttributes2) { function customElementRender(target, render, props = {}) {
const custom_element = target.host || target; const custom_element = target.host || target;
scope.push({ scope.push({
scope: custom_element, scope: custom_element,
@ -632,81 +690,40 @@ var DDE = (() => {
obj[method] = new Proxy(obj[method] || (() => { obj[method] = new Proxy(obj[method] || (() => {
}), { apply }); }), { apply });
} }
function observedAttributes2(instance) {
return observedAttributes(instance, (i, n) => i.getAttribute(n));
}
// src/events.js // src/memo.js
function dispatchEvent(name, options, host) { var memoMark = "__dde_memo";
if (typeof options === "function") { var memo_scope = [];
host = options; function memo(key, generator) {
options = null; if (!memo_scope.length) return generator(key);
} const k = typeof key === "object" ? JSON.stringify(key) : key;
if (!options) options = {}; const [{ cache, after }] = memo_scope;
return function dispatch(element, ...d) { return after(k, hasOwn(cache, k) ? cache[k] : generator(key));
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) { memo.isScope = function(obj) {
return function registerElement(element) { return obj[memoMark];
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) { memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
options = lifeOptions(options); let cache = oCreate();
return function registerElement(element) { function memoScope2(...args) {
element.addEventListener(evd, listener, options); if (signal2 && signal2.aborted)
if (element[keyLTE]) return element; return fun.apply(this, args);
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener)); let cache_local = onlyLast ? cache : oCreate();
if (c) c_ch_o.onDisconnected(element, listener); memo_scope.unshift({
return element; cache,
}; after(key, val) {
}; return cache_local[key] = val;
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)] })
);
}); });
const c = onAbort(options.signal, () => observer.disconnect()); const out = fun.apply(this, args);
if (c) observer.observe(element, { attributes: true }); memo_scope.shift();
return element; 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 // src/signals-lib/helpers.js
@ -822,25 +839,18 @@ var DDE = (() => {
} }
}; };
var key_reactive = "__dde_reactive"; 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) { 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_start = createElement.mark({ type: "reactive", source: new Defined().compact }, true);
const mark_end = mark_start.end; const mark_end = mark_start.end;
const out = enviroment.D.createDocumentFragment(); const out = enviroment.D.createDocumentFragment();
out.append(mark_start, mark_end); out.append(mark_start, mark_end);
const { current } = scope; const { current } = scope;
let cache_shared = oCreate();
const reRenderReactiveElement = (v) => { const reRenderReactiveElement = (v) => {
if (!mark_start.parentNode || !mark_end.parentNode) if (!mark_start.parentNode || !mark_end.parentNode)
return removeSignalListener(s, reRenderReactiveElement); return removeSignalListener(s, reRenderReactiveElement);
const memo = cache(cache_shared);
cache_shared = oCreate();
scope.push(current); scope.push(current);
let els = map(v, function useCache(key, fun) { let els = map(v);
return cache_shared[key] = memo(key, fun);
});
scope.pop(); scope.pop();
if (!Array.isArray(els)) if (!Array.isArray(els))
els = [els]; els = [els];
@ -860,7 +870,7 @@ var DDE = (() => {
current.host(on.disconnected( current.host(on.disconnected(
() => ( () => (
/*! Clears cached elements for reactive element `S.el` */ /*! Clears cached elements for reactive element `S.el` */
cache_shared = {} map.clear()
) )
)); ));
return out; return out;
@ -892,7 +902,7 @@ var DDE = (() => {
signal.observedAttributes = function(element) { signal.observedAttributes = function(element) {
const store = element[key_attributes] = {}; const store = element[key_attributes] = {};
const attrs = observedAttributes(element, observedAttribute(store)); const attrs = observedAttributes(element, observedAttribute(store));
on.attributeChanged(function attributeChangeToSignal({ detail }) { on(eva, function attributeChangeToSignal({ detail }) {
/*! This maps attributes to signals (`S.observedAttributes`). /*! This maps attributes to signals (`S.observedAttributes`).
Investigate `__dde_attributes` key of the element. */ Investigate `__dde_attributes` key of the element. */
const [name, value] = detail; 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"> & { export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean; first_time?: boolean;
}; };
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal { export interface signal {
_: Symbol; _: Symbol;
/** /**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li))); * 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, {}>>; observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
} }
export const signal: signal; 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]; export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global { declare global {
type ddeComponentAttributes = Record<any, any> | undefined; 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 ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>; type ddeStringable = ddeString | number | Signal<number, {}>;
} }
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`; export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = { export type AttrsModified = {
/** /**
@ -145,6 +147,7 @@ export namespace el {
host?: "this" | "parentElement"; host?: "this" | "parentElement";
}, is_open?: boolean): Comment; }, 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 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 { export function el<A extends {
textContent: ddeStringable; 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; [key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement; }>, ...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 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 */ /** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL; 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 * @param body Body of the custom element
* */ * */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL; 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): (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 { export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */ /** 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; <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 const on: On;
export type Scope = { export type Scope = {
scope: Node | Function | Object; scope: Node | Function | Object;
host: ddeElementAddon<any>; host: Host<SupportedElement>;
custom_element: false | HTMLElement; custom_element: false | HTMLElement;
prevent: boolean; 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) * It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`. * `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[]; state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */ /** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>; 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 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 customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<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 * 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. * 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>; 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 */ /* TypeScript MEH */
declare global { declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el; 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"> & { export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean; first_time?: boolean;
}; };
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal { export interface signal {
_: Symbol; _: Symbol;
/** /**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li))); * 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, {}>>; observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
} }
declare const signal: signal; 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]; export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global { declare global {
type ddeComponentAttributes = Record<any, any> | undefined; 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 ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>; type ddeStringable = ddeString | number | Signal<number, {}>;
} }
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`; export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = { export type AttrsModified = {
/** /**
@ -144,6 +146,7 @@ export namespace el {
host?: "this" | "parentElement"; host?: "this" | "parentElement";
}, is_open?: boolean): Comment; }, 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 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 { export function el<A extends {
textContent: ddeStringable; 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; [key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement; }>, ...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 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 */ /** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL; 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 * @param body Body of the custom element
* */ * */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL; 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): (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 { export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */ /** 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; <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 const on: On;
export type Scope = { export type Scope = {
scope: Node | Function | Object; scope: Node | Function | Object;
host: ddeElementAddon<any>; host: Host<SupportedElement>;
custom_element: false | HTMLElement; custom_element: false | HTMLElement;
prevent: boolean; 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) * It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`. * `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[]; state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */ /** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>; 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 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 customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<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 * 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. * 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>; 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 */ /* TypeScript MEH */
declare global { declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el; type ddeAppend<el> = (...nodes: (Node | string)[]) => el;

505
dist/iife.js vendored
View File

@ -33,7 +33,7 @@ var DDE = (() => {
elNS: () => createElementNS, elNS: () => createElementNS,
elementAttribute: () => elementAttribute, elementAttribute: () => elementAttribute,
lifecyclesToEvents: () => lifecyclesToEvents, lifecyclesToEvents: () => lifecyclesToEvents,
observedAttributes: () => observedAttributes2, memo: () => memo,
on: () => on, on: () => on,
queue: () => queue, queue: () => queue,
registerReactivity: () => registerReactivity, registerReactivity: () => registerReactivity,
@ -42,6 +42,7 @@ var DDE = (() => {
}); });
// src/helpers.js // src/helpers.js
var hasOwn = (...a) => Object.prototype.hasOwnProperty.call(...a);
function isUndef(value) { function isUndef(value) {
return typeof value === "undefined"; return typeof value === "undefined";
} }
@ -51,6 +52,9 @@ var DDE = (() => {
function isProtoFrom(obj, cls) { function isProtoFrom(obj, cls) {
return Object.prototype.isPrototypeOf.call(cls, obj); return Object.prototype.isPrototypeOf.call(cls, obj);
} }
function oCreate(proto = null, p = {}) {
return Object.create(proto, p);
}
function oAssign(...o) { function oAssign(...o) {
return Object.assign(...o); return Object.assign(...o);
} }
@ -64,16 +68,6 @@ var DDE = (() => {
signal.removeEventListener("abort", 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 // src/signals-lib/common.js
var signals_global = { var signals_global = {
@ -111,6 +105,7 @@ var DDE = (() => {
setDeleteAttr, setDeleteAttr,
ssr: "", ssr: "",
D: globalThis.document, D: globalThis.document,
N: globalThis.Node,
F: globalThis.DocumentFragment, F: globalThis.DocumentFragment,
H: globalThis.HTMLElement, H: globalThis.HTMLElement,
S: globalThis.SVGElement, S: globalThis.SVGElement,
@ -131,6 +126,215 @@ var DDE = (() => {
var evd = "dde:disconnected"; var evd = "dde:disconnected";
var eva = "dde:attributeChanged"; 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 // src/dom.js
function queue(promise) { function queue(promise) {
return enviroment.q(promise); return enviroment.q(promise);
@ -142,6 +346,7 @@ var DDE = (() => {
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body, host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
prevent: true prevent: true
}]; }];
var store_abort = /* @__PURE__ */ new WeakMap();
var scope = { var scope = {
/** /**
* Gets the current scope * Gets the current scope
@ -158,6 +363,17 @@ var DDE = (() => {
return this.current.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 * Prevents default behavior in the current scope
* @returns {Object} Current scope context * @returns {Object} Current scope context
*/ */
@ -400,172 +616,8 @@ var DDE = (() => {
return Reflect.deleteProperty(obj, key); 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 // src/customElement.js
function customElementRender(target, render, props = observedAttributes2) { function customElementRender(target, render, props = {}) {
const custom_element = target.host || target; const custom_element = target.host || target;
scope.push({ scope.push({
scope: custom_element, scope: custom_element,
@ -606,81 +658,40 @@ var DDE = (() => {
obj[method] = new Proxy(obj[method] || (() => { obj[method] = new Proxy(obj[method] || (() => {
}), { apply }); }), { apply });
} }
function observedAttributes2(instance) {
return observedAttributes(instance, (i, n) => i.getAttribute(n));
}
// src/events.js // src/memo.js
function dispatchEvent(name, options, host) { var memoMark = "__dde_memo";
if (typeof options === "function") { var memo_scope = [];
host = options; function memo(key, generator) {
options = null; if (!memo_scope.length) return generator(key);
} const k = typeof key === "object" ? JSON.stringify(key) : key;
if (!options) options = {}; const [{ cache, after }] = memo_scope;
return function dispatch(element, ...d) { return after(k, hasOwn(cache, k) ? cache[k] : generator(key));
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) { memo.isScope = function(obj) {
return function registerElement(element) { return obj[memoMark];
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) { memo.scope = function memoScope(fun, { signal, onlyLast } = {}) {
options = lifeOptions(options); let cache = oCreate();
return function registerElement(element) { function memoScope2(...args) {
element.addEventListener(evd, listener, options); if (signal && signal.aborted)
if (element[keyLTE]) return element; return fun.apply(this, args);
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener)); let cache_local = onlyLast ? cache : oCreate();
if (c) c_ch_o.onDisconnected(element, listener); memo_scope.unshift({
return element; cache,
}; after(key, val) {
}; return cache_local[key] = val;
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)] })
);
}); });
const c = onAbort(options.signal, () => observer.disconnect()); const out = fun.apply(this, args);
if (c) observer.observe(element, { attributes: true }); memo_scope.shift();
return element; 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); 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"> & { export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
first_time?: boolean; first_time?: boolean;
}; };
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
export interface signal { export interface signal {
_: Symbol; _: Symbol;
/** /**
@ -63,7 +64,7 @@ export interface signal {
* S.el(listS, list=> list.map(li=> el("li", li))); * 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, {}>>; observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
} }
declare const signal: signal; 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]; export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
declare global { declare global {
type ddeComponentAttributes = Record<any, any> | undefined; 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 ddeString = string | Signal<string, {}>;
type ddeStringable = ddeString | number | Signal<number, {}>; type ddeStringable = ddeString | number | Signal<number, {}>;
} }
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
export type PascalCase = `${Capitalize<string>}${string}`; export type PascalCase = `${Capitalize<string>}${string}`;
export type AttrsModified = { export type AttrsModified = {
/** /**
@ -144,6 +146,7 @@ export namespace el {
host?: "this" | "parentElement"; host?: "this" | "parentElement";
}, is_open?: boolean): Comment; }, 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 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 { export function el<A extends {
textContent: ddeStringable; 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; [key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement; }>, ...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 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 */ /** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL; 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 * @param body Body of the custom element
* */ * */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL; 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): (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 { export interface On {
/** Listens to the DOM event. See {@link Document.addEventListener} */ /** 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; <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 const on: On;
export type Scope = { export type Scope = {
scope: Node | Function | Object; scope: Node | Function | Object;
host: ddeElementAddon<any>; host: Host<SupportedElement>;
custom_element: false | HTMLElement; custom_element: false | HTMLElement;
prevent: boolean; 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) * It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`. * `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[]; state: Scope[];
/** Adds new child scope. All attributes are inherited by default. */ /** Adds new child scope. All attributes are inherited by default. */
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>; 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 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 customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
export function lifecyclesToEvents<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 * 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. * 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>; 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 */ /* TypeScript MEH */
declare global { declare global {
type ddeAppend<el> = (...nodes: (Node | string)[]) => el; 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 }){ export function code({ id, src, content, language= "js", className= host.slice(1), page_id }){
if(src) content= s.cat(src); if(src) content= s.cat(src);
content= normalizeIndentation(content);
let dataJS; let dataJS;
if(page_id){ if(page_id){
registerClientPart(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() }) 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= {}; let is_registered= {};
/** @param {string} page_id */ /** @param {string} page_id */
function registerClientPart(page_id){ function registerClientPart(page_id){
@ -207,33 +212,6 @@ function registerClientPart(page_id){
document.head.append( document.head.append(
// Use a newer version of Shiki with better performance // 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 }), 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( registerClientFile(
@ -245,3 +223,9 @@ function registerClientPart(page_id){
is_registered[page_id]= true; 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; 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(); 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 {object} attrs
* @param {URL} attrs.src Example code file path * @param {URL} attrs.src Example code file path
* @param {"js"|"ts"|"html"|"css"} [attrs.language="js"] Language of the code * @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 * @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); registerClientPart(page_id);
const content= s.cat(src).toString() const content= s.cat(src).toString()
.replaceAll(/ from "deka-dom-el(\/signals)?";/g, ' from "./esm-with-signals.js";'); .replaceAll(/ from "deka-dom-el(\/signals)?";/g, ' from "./esm-with-signals.js";');
const id= "code-example-"+generateCodeId(src); const id= "code-example-"+generateCodeId(src);
return el().append( 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 }) elCode({ id, content, extension: "."+language })
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,11 @@ document.body.append(
); );
import { chainableAppend } from "deka-dom-el"; 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)); const createElement= tag=> chainableAppend(document.createElement(tag));
document.body.append( document.body.append(
createElement("p").append( createElement("p").append(

View File

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

View File

@ -2,14 +2,14 @@
const div = document.createElement('div'); const div = document.createElement('div');
const h1 = document.createElement('h1'); const h1 = document.createElement('h1');
h1.textContent = 'Title'; h1.textContent = 'Title';
div.appendChild(h1); div.append(h1);
const p = document.createElement('p'); const p = document.createElement('p');
p.textContent = 'Paragraph'; p.textContent = 'Paragraph';
div.appendChild(p); div.append(p);
// appendChild doesn't return parent // append doesn't return parent
// so chaining is not possible // so chaining is not possible
// Add to DOM // 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"; 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 }), el=> log({ type: "dde:created", detail: el }),
on.connected(log), on.connected(log),
on.disconnected(log), on.disconnected(log),
on.attributeChanged(log)); );
document.body.append( document.body.append(
paragraph, paragraph,

View File

@ -6,9 +6,8 @@ let count = 0;
button.addEventListener('click', () => { button.addEventListener('click', () => {
count++; count++;
document.querySelector('p').textContent = document.querySelector('p').textContent =
'Clicked ' + count + ' times'; 'Clicked ' + count + ' times';
if (count > 10) { if (count > 10)
button.disabled = true; 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"; import { S } from "deka-dom-el/signals";
// A HelloWorld component using the 3PS pattern // A HelloWorld component using the 3PS pattern
@ -27,4 +27,4 @@ function HelloWorld({ emoji = "🚀" }) {
// Use the component in your app // Use the component in your app
document.body.append( document.body.append(
el(HelloWorld, { emoji: "🎉" }) el(HelloWorld, { emoji: "🎉" })
); );

View File

@ -15,15 +15,15 @@ function HelloWorldComponent({ initial }){
return el().append( return el().append(
el("p", { el("p", {
textContent: S(() => `Hello World ${emoji().repeat(clicks())}`), textContent: S(() => `Hello World ${emoji.get().repeat(clicks.get())}`),
className: "example", className: "example",
ariaLive: "polite", //OR ariaset: { live: "polite" }, ariaLive: "polite", //OR ariaset: { live: "polite" },
dataset: { example: "Example" }, //OR dataExample: "Example", dataset: { example: "Example" }, //OR dataExample: "Example",
}), }),
el("button", el("button",
{ textContent: "Fire", type: "button" }, { textContent: "Fire", type: "button" },
on("click", ()=> clicks(clicks() + 1)), on("click", ()=> clicks.set(clicks.get() + 1)),
on("keyup", ()=> clicks(clicks() - 2)), on("keyup", ()=> clicks.set(clicks.get() - 2)),
), ),
el("select", null, onChange).append( el("select", null, onChange).append(
el(OptionComponent, "🎉", isSelected),//OR { textContent: "🎉" } 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 textContent= S("Click to change text.");
const onclickChange= on("click", function redispatch(){ const onclickChange= on("click", function redispatch(){
textContent("Text changed! "+(new Date()).toString()) textContent.set("Text changed! "+(new Date()).toString())
}); });
return el("p", textContent, onclickChange); 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 // THE HOST IS PROBABLY DIFFERENT THAN
// YOU EXPECT AND SIGNAL MAY BE // YOU EXPECT AND SIGNAL MAY BE
// UNEXPECTEDLY REMOVED!!! // UNEXPECTEDLY REMOVED!!!
host().querySelector("button").disabled = count.get() >= 10; S.on(count, (count)=>
host().querySelector("button").disabled = count >= 10
);
}; };
setTimeout(()=> { setTimeout(()=> {
// ok, BUT consider extract to separate function // ok, BUT consider extract to separate function

View File

@ -1,6 +1,5 @@
// Handling async data in SSR // Handling async data in SSR
import { JSDOM } from "jsdom"; import { JSDOM } from "jsdom";
import { S } from "deka-dom-el/signals";
import { register, queue } from "deka-dom-el/jsdom"; import { register, queue } from "deka-dom-el/jsdom";
async function renderWithAsyncData() { async function renderWithAsyncData() {
@ -8,23 +7,7 @@ async function renderWithAsyncData() {
const { el } = await register(dom); const { el } = await register(dom);
// Create a component that fetches data // Create a component that fetches data
function AsyncComponent() { const { AsyncComponent } = await import("./components/AsyncComponent.js");
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)
);
}
// Render the page // Render the page
dom.window.document.body.append( dom.window.document.body.append(
@ -41,3 +24,24 @@ async function renderWithAsyncData() {
} }
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); const { el } = await register(dom);
// Create a simple header component // Create a simple header component
// can be separated into a separate file and use `import { el } from "deka-dom-el"`
function Header({ title }) { function Header({ title }) {
return el("header").append( return el("header").append(
el("h1", title), el("h1", title),

View File

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

View File

@ -1,6 +1,6 @@
// Basic jsdom integration example // Basic jsdom integration example
import { JSDOM } from "jsdom"; 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 // Create a jsdom instance
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>"); const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");

View File

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

View File

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

View File

@ -16,15 +16,16 @@ export function mnemonic(){
el("code", "dispatchEvent(<event>[, <options>])(element)"), el("code", "dispatchEvent(<event>[, <options>])(element)"),
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>]))") " — 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("li").append(
el("code", "dispatchEvent(<event>[, <options>])(<element>[, <detail>])"), el("code", "dispatchEvent(<event>[, <options>])(<element>[, <detail>])"),
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>] ))"), " or ", " — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>] ))"), " or ",
el("code", "<element>.dispatchEvent(new CustomEvent(<event>, { detail: <detail> }))") 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("li").append(
el("code", "scope.host(...<addons>)"), el("code", "scope.host(...<addons>)"),
" — use addons to current component", " — 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; SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
--body-max-width: 40rem; --body-max-width: 40rem;
--sidebar-width: 20rem;
--header-height: 4rem; --header-height: 4rem;
--border-radius: 0.375rem; --border-radius: 0.375rem;
@ -73,6 +72,7 @@ styles.css`
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
tab-size: var(--tab-size, 2rem);
} }
/* Accessibility improvements */ /* Accessibility improvements */
@ -215,8 +215,8 @@ body {
} }
@media (min-width: 768px) { @media (min-width: 768px) {
body { body {
grid-template-rows: var(--header-height) 1fr; grid-template-rows: auto 1fr;
grid-template-columns: var(--sidebar-width) 1fr; grid-template-columns: auto 1fr;
grid-template-areas: grid-template-areas:
"header header" "header header"
"sidebar content"; "sidebar content";
@ -250,7 +250,7 @@ h2 {
/* Section headings with better visual hierarchy */ /* Section headings with better visual hierarchy */
body > main h3, body > main h4 { 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 */ /* 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("h4", t`What Makes dd<el> Special`),
el("ul").append( el("ul").append(
el("li", t`No build step required — use directly in the browser`), 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`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`) el("li", t`Clean code organization with the 3PS pattern`)
) )
), ),
@ -67,7 +67,7 @@ export function page({ pkg, info }){
`), `),
el("ol").append( el("ol").append(
el("li").append(...T` 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("li").append(...T`
${el("strong", "Bind to Elements")}: Define how UI elements react to state changes ${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("div", { className: "note" }).append(
el("p").append(...T` el("p").append(...T`
The 3PS pattern becomes especially powerful when combined with components, allowing you to create 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. following sections.
`) `)
), ),
el(h3, t`How to Use This Documentation`), el(h3, t`How to Use This Documentation`),
el("p").append(...T` 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("ol", { start: 2 }).append(
el("li").append(...T`${el("strong", "Elements")} — Creating and manipulating DOM elements`), el("li").append(...T`${el("a", { href: "p02-elements.html" }).append(el("strong", "Elements"))} — Creating
el("li").append(...T`${el("strong", "Events")} — Handling user interactions and lifecycle events`), and manipulating DOM elements`),
el("li").append(...T`${el("strong", "Signals")} — Adding reactivity to your UI`), el("li").append(...T`${el("a", { href: "p03-events.html" }).append(el("strong", "Events and Addons"))} —
el("li").append(...T`${el("strong", "Scopes")} — Managing component lifecycles`), Handling user interactions and lifecycle events`),
el("li").append(...T`${el("strong", "Custom Elements")} — Building web components`), el("li").append(...T`${el("a", { href: "p04-signals.html" }).append(el("strong", "Signals"))} — Adding
el("li").append(...T`${el("strong", "Debugging")} — Tools to help you build and fix your apps`), reactivity to your UI`),
el("li").append(...T`${el("strong", "Extensions")} — Integrating third-party functionalities`), el("li").append(...T`${el("a", { href: "p05-scopes.html" }).append(el("strong", "Scopes"))} — Managing
el("li").append(...T`${el("strong", "Ireland Components")} component lifecycles`),
Creating interactive demos with server-side pre-rendering`), el("li").append(...T`${el("a", { href: "p06-customElement.html" }).append(el("strong", "Web Components"))} —
el("li").append(...T`${el("strong", "SSR")} — Server-side rendering with dd<el>`) 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` el("p").append(...T`
Each section builds on the previous ones, so we recommend following them in order. 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) { @media (max-width: 767px) {
${host_nav} { ${host_nav} {
padding: 0.75rem;
display: flex; display: flex;
flex-direction: row; flex-flow: row wrap;
flex-wrap: wrap;
gap: 0.5rem;
border-bottom: 1px solid var(--border);
border-right: none;
justify-content: center; justify-content: center;
} }
@ -121,14 +116,7 @@ ${host_nav} a .nav-number {
white-space: nowrap; white-space: nowrap;
} }
${host_nav} a .nav-number {
width: auto;
margin-right: 0.25rem;
}
${host_nav} a:first-child { ${host_nav} a:first-child {
margin-bottom: 0;
margin-right: 0.5rem;
min-width: 100%; min-width: 100%;
justify-content: center; justify-content: center;
} }

View File

@ -3,6 +3,10 @@ import { el, simulateSlots } from "deka-dom-el";
import { header } from "./head.html.js"; import { header } from "./head.html.js";
import { prevNext } from "../components/pageUtils.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 */ /** @param {Pick<import("../types.d.ts").PageAttrs, "pkg" | "info">} attrs */
export function simplePage({ pkg, info }){ export function simplePage({ pkg, info }){
@ -26,6 +30,9 @@ export function simplePage({ pkg, info }){
// Navigation between pages // Navigation between pages
el(prevNext, info) 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, dd<el> provides a simple yet powerful approach to element creation that is declarative, chainable,
and maintains a clean syntax close to HTML structure. 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("h4", t`dd<el> Elements: Key Benefits`),
el("ul").append( el("ul").append(
el("li", t`Declarative element creation with intuitive property assignment`), el("li", t`Declarative element creation with intuitive property assignment`),
el("li", t`Chainable methods for natural DOM tree construction`), el("li", t`Chainable methods for natural DOM tree construction`),
el("li", t`Simplified component patterns for code reuse`), 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`) el("li", t`Smart element return values for cleaner code flow`)
) )
), ),
@ -71,10 +71,10 @@ export function page({ pkg, info }){
el("p").append(...T` el("p").append(...T`
In standard JavaScript, you create DOM elements using the In standard JavaScript, you create DOM elements using the
${el("a", references.mdn_create).append(el("code", "document.createElement()"))} method ${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", { className: "illustration" }).append(
el("div", { class: "comparison" }).append( el("div", { className: "comparison" }).append(
el("div").append( el("div").append(
el("h5", t`Native DOM API`), el("h5", t`Native DOM API`),
el(code, { src: fileURL("./components/examples/elements/native-dom-create.js"), page_id }) 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` 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. with enhanced property assignment.
`), `),
el(example, { src: fileURL("./components/examples/elements/dekaCreateElement.js"), page_id }), el(example, { src: fileURL("./components/examples/elements/dekaCreateElement.js"), page_id }),
el(h3, t`Advanced Property Assignment`), el(h3, t`Advanced Property Assignment`),
el("p").append(...T` 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 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("dl").append(
el("dt", t`Property vs Attribute Priority`), el("dt", t`Property vs Attribute Priority`),
el("dd", t`Prefers IDL properties, falls back to setAttribute() when no writable property exists`), el("dd", t`Prefers IDL properties, falls back to setAttribute() when no writable property exists`),
el("dt", t`Data and ARIA Attributes`), 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("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("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("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("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(example, { src: fileURL("./components/examples/elements/dekaAssign.js"), page_id }),
el("div", { class: "note" }).append( el("div", { className: "note" }).append(
el("p").append(...T` el("p").append(...T`
You can explore standard HTML element properties in the MDN documentation for You can explore standard HTML element properties in the MDN documentation for
${el("a", { textContent: "HTMLElement", ...references.mdn_el })} (base class) ${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(h3, t`Building DOM Trees with Chainable Methods`),
el("p").append(...T` el("p").append(...T`
One of the most powerful features of dd<el> is its approach to building element trees. 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 Unlike the native DOM API which doesnt return the parent after ${el("code", "append()")}, dd<el>s
append() always returns the parent element: ${el("code", "append()")} always returns the parent element:
`), `),
el("div", { class: "illustration" }).append( el("div", { className: "illustration" }).append(
el("div", { class: "comparison" }).append( el("div", { className: "comparison" }).append(
el("div", { class: "bad-practice" }).append( el("div", { className: "bad-practice" }).append(
el("h5", t`❌ Native DOM API`), el("h5", t`❌ Native DOM API`),
el(code, { src: fileURL("./components/examples/elements/native-dom-tree.js"), page_id }) 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("h5", t`✅ dd<el> Approach`),
el(code, { src: fileURL("./components/examples/elements/dde-dom-tree.js"), page_id }) 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` el("p").append(...T`
This chainable pattern is much cleaner and easier to follow, especially for deeply nested elements. 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 }), 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. 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. 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` el("p").append(...T`
It's helpful to use naming conventions similar to native DOM elements for your components. Its 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 })} This allows you to keeps your code consistent with the DOM API.
and 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`), 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. from the props object for cleaner component code.
`), `),
el("li").append(...T` 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. ${el("code", ".append()")} to build complex DOM trees for better performance and cleaner code.
`), `),
), ),

View File

@ -36,11 +36,6 @@ const references= {
mdn_mutation: { mdn_mutation: {
href: "https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver", 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 */ /** @param {import("./types.d.ts").PageAttrs} attrs */
export function page({ pkg, info }){ export function page({ pkg, info }){
@ -48,11 +43,11 @@ export function page({ pkg, info }){
return el(simplePage, { info, pkg }).append( return el(simplePage, { info, pkg }).append(
el("p").append(...T` el("p").append(...T`
Events are at the core of interactive web applications. dd<el> provides a clean, declarative approach to 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. functionalities into your UI templates.
`), `),
el("div", { className: "callout" }).append( 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("ul").append(
el("li", t`Integrate event handling directly in element declarations`), el("li", t`Integrate event handling directly in element declarations`),
el("li", t`Leverage lifecycle events for better component design`), el("li", t`Leverage lifecycle events for better component design`),
@ -68,23 +63,23 @@ export function page({ pkg, info }){
el("p").append(...T` el("p").append(...T`
In JavaScript you can listen to native DOM events using In JavaScript you can listen to native DOM events using
${el("a", references.mdn_listen).append(el("code", "element.addEventListener(type, listener, options)"))}. ${el("a", references.mdn_listen).append(el("code", "element.addEventListener(type, listener, options)"))}.
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: style:
`), `),
el("div", { className: "illustration" }).append( el("div", { className: "illustration" }).append(
el("div", { className: "tabs" }).append( el("div", { className: "tabs" }).append(
el("div", { className: "tab" }).append( el("div", { className: "tab" }).append(
el("h5", t`Native DOM API`), 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("div", { className: "tab" }).append(
el("h5", t`dd<el> Approach`), 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` 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. directly into element declarations.
`), `),
el(example, { src: fileURL("./components/examples/events/compare.js"), page_id }), 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(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(h3, t`Three Ways to Handle Events`),
el("div", { className: "tabs" }).append( 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("h4", t`HTML Attribute Style`),
el(code, { src: fileURL("./components/examples/events/attribute-event.js"), page_id }), el(code, { src: fileURL("./components/examples/events/attribute-event.js"), page_id }),
el("p").append(...T` el("p").append(...T`
@ -109,25 +108,25 @@ export function page({ pkg, info }){
useful for SSR scenarios. 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("h4", t`Property Assignment`),
el(code, { src: fileURL("./components/examples/events/property-event.js"), page_id }), 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("h4", t`Addon Approach`),
el(code, { src: fileURL("./components/examples/events/chain-event.js"), page_id }), 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` el("p").append(...T`
For a deeper comparison of these approaches, see 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(h3, t`Understanding Addons`),
el("p").append(...T` 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. An Addon is any function that accepts an HTML element as its first parameter.
`), `),
el("div", { className: "callout" }).append( el("div", { className: "callout" }).append(
@ -152,11 +151,12 @@ export function page({ pkg, info }){
el(h3, t`Lifecycle Events`), el(h3, t`Lifecycle Events`),
el("p").append(...T` el("p").append(...T`
Addons are called immediately when an element is created, even before it's connected to the live DOM. 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. You can think of an Addon as an "oncreate" event handler.
`), `),
el("p").append(...T` 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("div", { className: "function-table" }).append(
el("dl").append( el("dl").append(
@ -165,31 +165,30 @@ export function page({ pkg, info }){
el("dt", t`on.disconnected(callback)`), el("dt", t`on.disconnected(callback)`),
el("dd", t`Fires when the element is removed from the DOM`), 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(example, { src: fileURL("./components/examples/events/live-cycle.js"), page_id }),
el("div", { className: "note" }).append( el("div", { className: "note" }).append(
el("p").append(...T` el("p").append(...T`
For regular elements (non-custom elements), dd<el> uses For regular elements (non-custom elements), dd<el> uses ${el("a",
${el("a", references.mdn_mutation).append(el("code", "MutationObserver"), " | MDN")} references.mdn_mutation).append(el("code", "MutationObserver"), " | MDN")} internally to track
internally to track lifecycle events. lifecycle events.
`) `)
), ),
el("div", { className: "warning" }).append( el("div", { className: "warning" }).append(
el("ul").append( el("ul").append(
el("li").append(...T` 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` el("li").append(...T`
Use lifecycle events sparingly, as they require internal tracking Use lifecycle events sparingly, as they require internal tracking
`), `),
el("li").append(...T` 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` el("li").append(...T`
see section later in documentation regarding hosts elements see section later in documentation regarding hosts elements
@ -202,11 +201,11 @@ export function page({ pkg, info }){
el(h3, t`Dispatching Custom Events`), el(h3, t`Dispatching Custom Events`),
el("p").append(...T` el("p").append(...T`
This makes it easy to implement component communication through events, This makes it easy to implement component communication through events, following standard web platform
following standard web platform patterns. The curried approach allows for easy reuse patterns. The curried approach allows for easy reuse of event dispatchers throughout your application.
of event dispatchers throughout your application.
`), `),
el(example, { src: fileURL("./components/examples/events/compareDispatch.js"), page_id }), 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(h3, t`Best Practices`),
el("ol").append( el("ol").append(
@ -217,7 +216,8 @@ export function page({ pkg, info }){
${el("strong", "Leverage lifecycle events")}: For component setup and teardown ${el("strong", "Leverage lifecycle events")}: For component setup and teardown
`), `),
el("li").append(...T` 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("li").append(...T`
${el("strong", "Maintain consistency")}: Choose one event binding approach and stick with it ${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 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. 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("h4", t`What Makes Signals Special?`),
el("ul").append( el("ul").append(
el("li", t`Fine-grained reactivity without complex state management`), 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 Signals organize your code into three distinct parts, following the
${el("a", { textContent: t`3PS principle`, href: "./#h-3ps" })}: ${el("a", { textContent: t`3PS principle`, href: "./#h-3ps" })}:
`), `),
el("div", { class: "signal-diagram" }).append( el("div", { className: "signal-diagram" }).append(
el("div", { class: "signal-part" }).append( el("div", { className: "signal-part" }).append(
el("h4", t`PART 1: Create Signal`), el("h4", t`PART 1: Create Signal`),
el(code, { content: "const count = S(0);", page_id }), 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("h4", t`PART 2: React to Changes`),
el(code, { content: "S.on(count, value => updateUI(value));", page_id }), el(code, { content: "S.on(count, value => updateUI(value));", page_id }),
el("p", t`Subscribe to signal changes with callbacks or effects`) 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("h4", t`PART 3: Update Signal`),
el(code, { content: "count.set(count.get() + 1);", page_id }), el(code, { content: "count.set(count.get() + 1);", page_id }),
el("p", t`Modify the signal value, which automatically triggers updates`) 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(example, { src: fileURL("./components/examples/signals/signals.js"), page_id }),
el("div", { class: "note" }).append( el("div", { className: "note" }).append(
el("p").append(...T` el("p").append(...T`
Signals implement the ${el("a", { textContent: t`Publishsubscribe pattern`, ...references.wiki_pubsub })}, 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 })}. })}, 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, })}. This architecture allows different parts of your application to stay synchronized through
without direct dependencies on each other. Compare for example with ${el("a", { textContent: a shared signal, without direct dependencies on each other. Compare for example with ${el("a", {
t`fpubsub library`, ...references.fpubsub })}. textContent: t`fpubsub library`, ...references.fpubsub })}.
`) `)
), ),
el(h3, t`Signal Essentials: Core API`), el(h3, t`Signal Essentials: Core API`),
el("div", { class: "function-table" }).append( el("div", { className: "function-table" }).append(
el("dl").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("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("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("dd", t`signal.set(newValue) → updates the value and notifies subscribers`),
el("dt", t`Subscribing to Changes`), el("dt", t`Subscribing to Changes`),
@ -115,51 +115,53 @@ export function page({ pkg, info }){
) )
), ),
el("p").append(...T` el("p").append(...T`
Signals can be created with any type of value, but they work best with Signals can be created with any type of value, but they work best with ${el("a", { textContent:
${el("a", { textContent: t`primitive types`, ...references.mdn_primitive })} like strings, numbers, and booleans. t`primitive types`, ...references.mdn_primitive })} like strings, numbers, and booleans. For complex
For complex data types like objects and arrays, you'll want to use Actions (covered below). data types like objects and arrays, youll want to use Actions (covered below).
`), `),
el(h3, t`Derived Signals: Computed Values`), el(h3, t`Derived Signals: Computed Values`),
el("p").append(...T` el("p").append(...T`
Computed values (also called derived signals) automatically update when their dependencies change. 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(example, { src: fileURL("./components/examples/signals/derived.js"), page_id }),
el("p").append(...T` el("p").append(...T`
Derived signals are read-only - you can't call .set() on them. Their value is always computed Derived signals are read-only - you cant call ${el("code", ".set()")} on them. Their value is always
from their dependencies. They're perfect for transforming or combining data from other signals. 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(example, { src: fileURL("./components/examples/signals/computations-abort.js"), page_id }),
el(h3, t`Signal Actions: For Complex State`), el(h3, t`Signal Actions: For Complex State`),
el("p").append(...T` el("p").append(...T`
When working with objects, arrays, or other complex data structures, Signal Actions provide When working with objects, arrays, or other complex data structures. Signal Actions provide
a structured way to modify state while maintaining reactivity. 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("h4", t`Actions vs. Direct Mutation`),
el("div", { class: "comparison" }).append( el("div", { className: "comparison" }).append(
el("div", { class: "good-practice" }).append( el("div", { className: "good-practice" }).append(
el("h5", t`✅ With Actions`), el("h5", t`✅ With Actions`),
el(code, { content: `const todos = S([], { el(code, { content: `
add(text) { const todos = S([], {
this.value.push(text); add(text) {
// Subscribers notified automatically this.value.push(text);
} // Subscribers notified automatically
}); }
});
// Use the action // Use the action
S.action(todos, "add", "New todo");`, page_id }) 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("h5", t`❌ Without Actions`),
el(code, { content: ` el(code, { content: `
const todos = S([]); const todos = S([]);
// Directly mutating the array // Directly mutating the array
const items = todos.get(); const items = todos.get();
items.push("New todo"); items.push("New todo");
// This WON'T trigger updates!`, page_id })) // This WONT trigger updates!
`, page_id }))
), ),
), ),
el("p").append(...T` 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("li", t`Act similar to reducers in other state management libraries`)
), ),
el("p").append(...T` 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(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("p").append(...T`
${el("strong", "Special Action Methods")}: Signal actions can implement special lifecycle hooks: ${el("strong", "Special Action Methods")}: Signal actions can implement special lifecycle hooks:
`), `),
el("ul").append( 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: 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", { className: "tabs" }).append(
el("div", { class: "tab", "data-tab": "attributes" }).append( el("div", { className: "tab", dataTab: "attributes" }).append(
el("h4", t`Reactive Attributes`), el("h4", t`Reactive Attributes`),
el("p", t`Bind signal values directly to element attributes, properties, or styles:`), el("p", t`Bind signal values directly to element attributes, properties, or styles:`),
el(code, { content: `// Create a signal el(code, { content: `
const color = S("blue"); // Create a signal
const color = S("blue");
// Bind it to an element's style // Bind it to an elements style
el("div", { el("div", {
style: { style: {
color, // Updates when signal changes color, // Updates when signal changes
fontWeight: S(() => color.get() === "red" ? "bold" : "normal") fontWeight: S(() => color.get() === "red" ? "bold" : "normal")
} }
}, "This text changes color"); }, "This text changes color");
// Later: // Later:
color.set("red"); // UI updates automatically`, page_id }) 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("h4", t`Reactive Elements`),
el("p", t`Dynamically create or update elements based on signal values:`), el("p", t`Dynamically create or update elements based on signal values:`),
el(code, { content: `// Create an array signal el(code, { content: `
const items = S(["Apple", "Banana", "Cherry"]); // Create an array signal
const items = S(["Apple", "Banana", "Cherry"]);
// Create a dynamic list that updates when items change // Create a dynamic list that updates when items change
el("ul").append( el("ul").append(
S.el(items, items => S.el(items, items =>
items.map(item => el("li", item)) items.map(item => el("li", item))
) )
); );
// Later: // Later:
S.action(items, "push", "Dragonfruit"); // List updates automatically`, page_id }) 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("ol").append(
el("li").append(...T` 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("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("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("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("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("h4", t`Common Signal Pitfalls`),
el("dl").append( el("dl").append(
el("dt", t`UI not updating when array/object changes`), 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 { example } from "./components/example.html.js";
import { h3 } from "./components/pageUtils.html.js"; import { h3 } from "./components/pageUtils.html.js";
import { mnemonic } from "./components/mnemonic/scopes-init.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 */ /** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url); const fileURL= url=> new URL(url, import.meta.url);
const references= { const references= {
@ -31,7 +31,7 @@ export function page({ pkg, info }){
return el(simplePage, { info, pkg }).append( return el(simplePage, { info, pkg }).append(
el("p").append(...T` el("p").append(...T`
For state-less components we can use functions as UI components (see Elements page). But in real life, 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 })}. the ${el("a", { textContent: t`Garbage collection`, ...references.garbage_collection })}.
`), `),
el(code, { src: fileURL("./components/examples/scopes/intro.js"), page_id }), 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(h3, t`Understanding Host Elements and Scopes`),
el("p").append(...T` el("p").append(...T`
The ${el("strong", "host")} is the name for the element representing the component. This is typically the 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>)")}. just use ${el("code", "scope.host(...<addons>)")}.
`), `),
el("p").append(...T` el("p").append(...T`
Scopes are primarily needed when signals are used in DOM templates (with ${el("code", "el")}, ${el("code", 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. and cleaning up unused signals when components are removed from the DOM.
`), `),
el("div", { className: "illustration" }).append( el("div", { className: "illustration" }).append(
el("h4", t`Component Anatomy`), el("h4", t`Component Anatomy`),
el("pre").append(el("code", ` el(pre, { content: `
// 1. Component scope created // 1. Component scope created
el(MyComponent); el(MyComponent);
function MyComponent() { function MyComponent() {
  // 2. access the host element // 2. access the host element
  const { host } = scope; const { host } = scope;
  // 3. Add behavior to host // 3. Add behavior to host
  host( host(
  on.click(handleClick) on.click(handleClick)
  ); );
  // 4. Return the host element // 4. Return the host element
  return el("div", { return el("div", {
  className: "my-component" className: "my-component"
  }).append( }).append(
  el("h2", "Title"), el("h2", "Title"),
  el("p", "Content") el("p", "Content")
  ); );
} }
`.trim())) ` })
), ),
el("div", { className: "function-table" }).append( el("div", { className: "function-table" }).append(
el("h4", t`scope.host()`), el("h4", t`scope.host()`),
el("dl").append( el("dl").append(
el("dt", t`When called with no arguments`), 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("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(example, { src: fileURL("./components/examples/scopes/scopes-and-hosts.js"), page_id }),
el("div", { className: "tip" }).append( el("div", { className: "tip" }).append(
el("p").append(...T` el("p").append(...T`
${el("strong", "Best Practice:")} Always capture the host reference at the beginning of your component ${el("strong", "Best Practice:")} Always capture the host reference (or other scope related values) at
function using ${el("code", "const { host } = scope")} to avoid scope-related issues, especially with the beginning of your component function using ${el("code", "const { host } = scope")} to avoid
asynchronous code. scope-related issues, especially with ${el("em", "asynchronous code")}.
`), `),
el("p").append(...T` el("p").append(...T`
If you are interested in the implementation details, see Class-Based Components section. 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("div", { className: "illustration" }).append(
el("h4", t`Lifecycle Flow`), el("h4", t`Lifecycle Flow`),
el("pre").append(el("code", ` el(pre, { content: `
1. Component created scope established 1. Component created scope established
2. Component add<el> to DOM connected event 2. Component added to DOM connected event
3. Component interactions happen 3. Component interactions happen
4. Component removed from DOM disconnected event 4. Component removed from DOM disconnected event
5. Automatic cleanup of: 5. Automatic cleanup of:
  - Event listeners - Event listeners (browser)
  - Signal subscriptions - Signal subscriptions (dd<el> and browser)
  - Custom cleanup code - Custom cleanup code (dd<el> and user)
`)) ` })
), ),
el(example, { src: fileURL("./components/examples/scopes/cleaning.js"), page_id }), el(example, { src: fileURL("./components/examples/scopes/cleaning.js"), page_id }),
el("div", { className: "note" }).append( el("div", { className: "note" }).append(
el("p").append(...T` el("p").append(...T`
In this example, when you click "Remove", the component is removed from the DOM, and all its associated 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. resources are automatically cleaned up, including ${el("em",
This happens because the library internally registers a disconnected event handler on the host element. "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(h3, t`Declarative vs Imperative Components`),
el("p").append(...T` el("p").append(...T`
The library DOM API and signals work best when used declaratively. It means you split your app's logic 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", ...references.signals })}. into three parts as introduced in ${el("a", { textContent: "Signals (3PS)", ...references.signals })}.
`), `),
el("div", { className: "note" }).append( el("div", { className: "note" }).append(
el("p").append(...T` el("p").append(...T`
@ -145,17 +146,17 @@ function MyComponent() {
`) `)
), ),
el("div", { className: "tabs" }).append( 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("h4", t`✅ Declarative Approach`),
el("p", t`Define what your UI should look like based on state:`), 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(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("h4", t`⚠️ Imperative Approach`),
el("p", t`Manually update the DOM in response to events:`), el("p", t`Manually update the DOM in response to events:`),
el(code, { src: fileURL("./components/examples/scopes/imperative.js"), page_id }) 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("h4", t`❌ Mixed Approach`),
el("p", t`This approach should be avoided:`), el("p", t`This approach should be avoided:`),
el(code, { src: fileURL("./components/examples/scopes/mixed.js"), page_id }) 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("strong", "Define signals as constants:")} ${el("code", "const counter = S(0);")}
`), `),
el("li").append(...T` 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("li").append(...T`
${el("strong", "Keep components focused:")} Each component should do one thing well ${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("dd", t`Use arrow functions or .bind() to preserve context`),
el("dt", t`Mixing declarative and imperative styles`), 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 { example } from "./components/example.html.js";
import { h3 } from "./components/pageUtils.html.js"; import { h3 } from "./components/pageUtils.html.js";
import { mnemonic } from "./components/mnemonic/customElement-init.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 */ /** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url); const fileURL= url=> new URL(url, import.meta.url);
const references= { 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)`, 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", 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 */ /** @param {import("./types.d.ts").PageAttrs} attrs */
export function page({ pkg, info }){ export function page({ pkg, info }){
@ -56,7 +61,7 @@ export function page({ pkg, info }){
return el(simplePage, { info, pkg }).append( return el(simplePage, { info, pkg }).append(
el("p").append(...T` el("p").append(...T`
dd<el> pairs powerfully with ${el("a", references.mdn_web_components).append(el("strong", t`Web 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. declarative DOM construction and reactivity system.
`), `),
el("div", { className: "callout" }).append( 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`Reactive attribute updates through signals`),
el("li", t`Simplified event handling with the same events API`), el("li", t`Simplified event handling with the same events API`),
el("li", t`Clean component lifecycle management`), 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(code, { src: fileURL("./components/examples/customElement/intro.js"), page_id }),
el(h3, t`Getting Started: Web Components Basics`), el(h3, t`Getting Started: Web Components Basics`),
el("p").append(...T` 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: encapsulated functionality. They consist of three main technologies:
`), `),
el("ul").append( 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("strong", "Custom Elements:")} Create your own HTML tags with JS-defined behavior
`), `),
el("li").append(...T` 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("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` 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 }), 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(h3, t`dd<el> Integration: Step 1 - Event Handling`),
el("p").append(...T` 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 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("div", { className: "function-table" }).append(
el("h4", t`customElementWithDDE`), el("h4", t`customElementWithDDE`),
el("dl").append( el("dl").append(
el("dt", t`Purpose`), 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("dt", t`Usage`),
el("dd", t`customElementWithDDE(YourElementClass)`), el("dd", t`customElementWithDDE(YourElementClass)`),
el("dt", t`Benefits`), 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 }), 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("div", { className: "tip" }).append(
el("p").append(...T` el("p").append(...T`
${el("strong", "Key Point:")} The ${el("code", "customElementWithDDE")} function adds event dispatching ${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(h3, t`dd<el> Integration: Step 2 - Rendering Components`),
el("p").append(...T` 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("code", "customElementRender")}, which connects your dd<el> component function to the Custom Element.
`), `),
el("div", { className: "function-table" }).append( el("div", { className: "function-table" }).append(
el("h4", t`customElementRender`), el("h4", t`customElementRender`),
el("dl").append( el("dl").append(
el("dt", t`Purpose`), 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("dt", t`Parameters`),
el("dd").append( el("dd").append(
el("ol").append( el("ol").append(
el("li", t`Target (usually this or this.shadowRoot)`), el("li", t`Target (usually this or this.shadowRoot)`),
el("li", t`Component function that returns a DOM tree`), el("li", t`Component function that returns a DOM tree`),
el("li", t`Optional: Attributes transformer function (default or S.observedAttributes)`) el("li", t`Optional: Attributes transformer function (empty by default or
S.observedAttributes)`)
) )
), ),
el("dt", t`Returns`), el("dt", t`Returns`),
@ -153,7 +160,7 @@ export function page({ pkg, info }){
el("div", { className: "note" }).append( el("div", { className: "note" }).append(
el("p").append(...T` 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, ...)")}. 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(h3, t`Reactive Web Components with Signals`),
el("p").append(...T` el("p").append(...T`
One of the most powerful features of integrating dd<el> with Web Components is connecting HTML attributes 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("div", { className: "tip" }).append(
el("p").append(...T` el("p").append(...T`
@ -169,7 +176,8 @@ export function page({ pkg, info }){
`), `),
el("ol").append( el("ol").append(
el("li").append(...T` 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("li").append(...T`
${el("code", "S.observedAttributes")} - Transforms attributes into signals (reactive) ${el("code", "S.observedAttributes")} - Transforms attributes into signals (reactive)
@ -177,8 +185,8 @@ export function page({ pkg, info }){
) )
), ),
el("p").append(...T` el("p").append(...T`
Using ${el("code", "S.observedAttributes")} creates a reactive connection between your element's attributes Using the ${el("code", "S.observedAttributes")} creates a reactive connection between your elements
and its internal rendering. When attributes change, your component automatically updates! attributes and its internal rendering. When attributes change, your component automatically updates!
`), `),
el(example, { src: fileURL("./components/examples/customElement/observedAttributes.js"), page_id }), 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("h4", t`How S.observedAttributes Works`),
el("ol").append( el("ol").append(
el("li", t`Takes each attribute listed in static observedAttributes`), 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`Automatically updates these signals when attributes change`),
el("li", t`Passes the signals to your component function`), 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("li", t`Your component reacts to changes through signal subscriptions`)
) )
), ),
el(h3, t`Working with Shadow DOM`), el(h3, t`Working with Shadow DOM`),
el("p").append(...T` 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. you get the best of both worlds: encapsulation plus declarative DOM creation.
`), `),
el("div", { className: "illustration" }).append( el("div", { className: "illustration" }).append(
el("h4", t`Shadow DOM Encapsulation`), el("h4", t`Shadow DOM Encapsulation`),
el("pre").append(el("code", ` el(pre, { content: `
<my-custom-element> <my-custom-element>
  
    #shadow-root #shadow-root
      Created with dd<el> Created with dd<el>
    
      <div> <div>
       <h2>Title</h2> <h2>Title</h2>
       <p>Content</p> <p>Content</p>
`)) ` })
), ),
el(example, { src: fileURL("./components/examples/customElement/shadowRoot.js"), page_id }), 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("dt", t`Purpose`),
el("dd", t`Provides slot functionality when you cannot/do not want to use shadow DOM`), el("dd", t`Provides slot functionality when you cannot/do not want to use shadow DOM`),
el("dt", t`Parameters`), 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("dt", t`Events not firing properly`),
el("dd", t`Make sure you called customElementWithDDE before defining the element`), el("dd", t`Make sure you called customElementWithDDE before defining the element`),
el("dt", t`Attributes not updating`), 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("dt", t`Component not rendering`),
el("dd", t`Verify customElementRender is called in connectedCallback, not constructor`) 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( return el(simplePage, { info, pkg }).append(
el("p").append(...T` el("p").append(...T`
Debugging is an essential part of application development. This guide provides techniques 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`), el(h3, t`Debugging signals`),
@ -30,42 +30,40 @@ export function page({ pkg, info }){
el("h4", t`Inspecting signal values`), el("h4", t`Inspecting signal values`),
el("p").append(...T` 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: ` el(code, { content: `
const signal = S(0); const signal = S(0);
console.log('Current value:', signal.get()); console.log('Current value:', signal.get());
// without triggering updates // without triggering updates
console.log('Current value:', signal.valueOf()); console.log('Current value:', signal.valueOf());
`, page_id }), `, page_id }),
el("p").append(...T` 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, { el(code, { content: `
content: // Log every time the signal changes
"// Log every time the signal changes\nS.on(signal, value => console.log('Signal changed:', value));", S.on(signal, value => console.log('Signal changed:', value));
page_id }), `, page_id }),
el("h4", t`Debugging derived signals`), el("h4", t`Debugging derived signals`),
el("p").append(...T` el("p").append(...T`
With derived signals (created with ${el("code", "S(() => computation))")}), debugging is a bit more complex 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: because the value depends on other signals. To understand why a derived signal isnt updating correctly:
`), `),
el("ol").append( el("ol").append(
el("li", t`Check that all dependency signals are updating correctly`), 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("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(example, { src: fileURL("./components/examples/debugging/consoleLog.js"), page_id }),
el(h3, t`Common signal debugging issues`), el(h3, t`Common signal debugging issues`),
el("h4", t`Signal updates not triggering UI changes`), el("h4", t`Signal updates not triggering UI changes`),
el("p").append(...T` el("p", t`If signal updates arent reflected in the UI, check:`),
If signal updates aren't reflected in the UI, check:
`),
el("ul").append( el("ul").append(
el("li", t`That you're using signal.set() to update the value, not modifying objects/arrays directly`), el("li", t`That youre 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`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("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 }), 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("h4", t`Performance issues with frequently updating signals`),
el("p").append(...T` el("p", t`If you notice performance issues with signals that update very frequently:`),
If you notice performance issues with signals that update very frequently:
`),
el("ul").append( el("ul").append(
el("li", t`Consider debouncing or throttling signal updates`), 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("li", t`Keep signal computations focused and minimal`)
), ),
el(code, { src: fileURL("./components/examples/debugging/debouncing.js"), page_id }), 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` el("p").append(...T`
dd<el> marks components in the DOM with special comment nodes to help you identify component boundaries. 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 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: includes:
`), `),
el("ul").append( el("ul").append(
el("li", "type - Identifies the type of marker (\"component\", \"reactive\", or \"later\")"), el("li", t`type - Identifies the type of marker ("component", "reactive", or "later")`),
el("li", "name - The name of the component function"), el("li", t`name - The name of the component function`),
el("li", "host - Indicates whether the host is \"this\" (for DocumentFragments) or \"parentElement\""), el("li", t`host - Indicates whether the host is "this" (for DocumentFragments) or "parentElement"`),
), ),
el("h4", t`Finding reactive elements in the DOM`), 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 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): 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` 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 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. signal connections through \`__dde_reactive\` of the host element.
`), `),
@ -124,48 +120,49 @@ console.log('Current value:', signal.valueOf());
`), `),
el("p").append(...T` el("p").append(...T`
${el("code", "<element>.__dde_reactive")} - An array property on DOM elements that tracks signal-to-element ${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 relationships. This allows you to quickly identify which elements are reactive and what signals theyre
bound to. Each entry in the array contains: bound to. Each entry in the array contains:
`), `),
el("ul").append( el("ul").append(
el("li", "A pair of signal and listener function: [signal, listener]"), el("li", t`A pair of signal and listener function: [signal, listener]`),
el("li", "Additional context information about the element or attribute"), el("li", t`Additional context information about the element or attribute`),
el("li", "Automatically managed by signal.el(), signal.observedAttributes(), and processReactiveAttribute()") el("li", t`Automatically managed by signal.el(), signal.observedAttributes(), and processReactiveAttribute()`)
), ),
el("p").append(...T` 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(example, { src: fileURL("./components/examples/signals/debugging-dom.js"), page_id }),
el("h4", t`Examining signal connections`), el("h4", t`Examining signal connections`),
el("p").append(...T` 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: signal objects. It contains the following information:
`), `),
el("ul").append( el("ul").append(
el("li", "listeners: A Set of functions called when the signal value changes"), el("li", t`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", t`actions: Custom actions that can be performed on the signal`),
el("li", "onclear: Functions to run when the signal is cleared"), el("li", t`onclear: Functions to run when the signal is cleared`),
el("li", "host: Reference to the host element/scope"), el("li", t`host: Reference to the host element/scope`),
el("li", "defined: Stack trace information for debugging"), el("li", t`defined: Stack trace information for debugging`),
el("li", "readonly: Boolean flag indicating if the signal is read-only") el("li", t`readonly: Boolean flag indicating if the signal is read-only`)
), ),
el("p").append(...T` el("p").append(...T`
to determine the current value of the signal, call ${el("code", "signal.valueOf()")}. to determine the current value of the signal, call ${el("code", "signal.valueOf()")}.
`), `),
el("p").append(...T` el("p").append(...T`
You can inspect (host) element relationships and bindings with signals in the DevTools console using 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", "$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", `[ [ signal, listener ], element, property ]`)}, where:
`), `),
el("ul").append( el("ul").append(
el("li", "signal — the signal triggering the changes"), el("li", t`signal — the signal triggering the changes`),
el("li", "listener — the listener function (this is an internal function for dd<el>)"), el("li", t`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", t`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`property — the attribute or property name which is changing based on the signal`),
), ),
el("p").append(...T` 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. 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(h3, t`DOM Element Extensions with Addons`),
el("p").append(...T` el("p").append(...T`
The primary method for extending DOM elements in dd<el> is through the Addon pattern. 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 Addons are functions that take an element and applying some functionality to it. This pattern enables
clean, functional approach to element enhancement. a clean, functional approach to element enhancement.
`), `),
el("div", { className: "callout" }).append( el("div", { className: "callout" }).append(
el("h4", t`What are Addons?`), 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: Addons are simply functions with the signature: (element) => void. They:
`), `),
el("ul").append( 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("li", t`Apply some behavior, property, or attribute to the element`),
) )
), ),
el(code, { content: ` el(code, { content: `
// Basic structure of an addon // Basic structure of an addon
function myAddon(config) { function myAddon(config) {
return function(element) { return function(element) {
// Apply functionality to element // Apply functionality to element
element.dataset.myAddon = config.option; element.dataset.myAddon = config.option;
}; };
} }
// Using an addon // Using an addon
el("div", { id: "example" }, myAddon({ option: "value" })); el("div", { id: "example" }, myAddon({ option: "value" }));
`.trim(), page_id }), `, page_id }),
el(h3, t`Resource Cleanup with Abort Signals`), el(h3, t`Resource Cleanup with Abort Signals`),
el("p").append(...T` el("p").append(...T`
When extending elements with functionality that uses resources like event listeners, timers, 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. from the DOM. dd<el> provides utilities for this through AbortSignal integration.
`), `),
el("div", { className: "tip" }).append( el("div", { className: "tip" }).append(
el("p").append(...T` el("p").append(...T`
The ${el("code", "on.disconnectedAsAbort")} utility creates an AbortSignal that automatically 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. triggers when an element is disconnected from the DOM, making cleanup much easier to manage.
`) `)
), ),
el(code, { content: ` el(code, { content: `
// Third-party library addon with proper cleanup // Third-party library addon with proper cleanup
function externalLibraryAddon(config, signal) { function externalLibraryAddon(config, signal) {
return function(element) { return function(element) {
// Initialize the third-party library // Initialize the third-party library
const instance = new ExternalLibrary(element, config); const instance = new ExternalLibrary(element, config);
// Set up cleanup when the element is removed // Set up cleanup when the element is removed
signal.addEventListener('abort', () => { signal.addEventListener('abort', () => {
instance.destroy(); instance.destroy();
}); });
return element; return element;
}; };
} }
// dde component // dde component
function Component(){ function Component(){
const { host }= scope; const { signal }= scope;
const signal= on.disconnectedAsAbort(host); return el("div", null, externalLibraryAddon({ option: "value" }, signal));
return el("div", null, externalLibraryAddon({ option: "value" }, signal)); }
} `, page_id }),
`.trim(), page_id }),
el(h3, t`Building Library-Independent Extensions`), el(h3, t`Building Library-Independent Extensions`),
el("p").append(...T` 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. This approach enables better interoperability and future-proofing.
`), `),
el("div", { className: "illustration" }).append( el("div", { className: "illustration" }).append(
@ -97,37 +96,37 @@ function Component(){
el("div", { className: "tab" }).append( el("div", { className: "tab" }).append(
el("h5", t`✅ Library-Independent`), el("h5", t`✅ Library-Independent`),
el(code, { content: ` el(code, { content: `
function enhancementElement({ signal, ...config }) { function enhancementElement({ signal, ...config }) {
// do something // do something
return function(element) { return function(element) {
// do something // do something
signal.addEventListener('abort', () => { signal.addEventListener('abort', () => {
// do cleanup // do cleanup
}); });
}; };
} }
`.trim(), page_id }) `, page_id })
), ),
el("div", { className: "tab" }).append( el("div", { className: "tab" }).append(
el("h5", t`⚠️ Library-Dependent`), el("h5", t`⚠️ Library-Dependent`),
el(code, { content: ` el(code, { content: `
// Tightly coupled to dd<el> // Tightly coupled to dd<el>
function enhancementElement(config) { function enhancementElement(config) {
return function(element) { return function(element) {
// do something // do something
on.disconnected(()=> { on.disconnected(()=> {
// do cleanup // do cleanup
})(element); })(element);
}; };
} }
`.trim(), page_id }) `, page_id })
) )
) )
), ),
el(h3, t`Signal Extensions and Future Compatibility`), el(h3, t`Signal Extensions and Factory Patterns`),
el("p").append(...T` 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 way to create library-independent extensions. This is because signals are implemented
differently across libraries. differently across libraries.
`), `),
@ -139,35 +138,99 @@ function enhancementElement(config) {
native signals without breaking changes when they become available. native signals without breaking changes when they become available.
`) `)
), ),
el("h4", t`The Signal Factory Pattern`),
el("p").append(...T` 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. future migration easier.
`), `),
el(code, { content: ` el(code, { content: `
// Signal extension with clear interface // Signal extension with clear interface
function createEnhancedSignal(initialValue) { function createEnhancedSignal(initialValue) {
const signal = S(initialValue); const signal = S(initialValue);
// Extension functionality // Extension functionality
const increment = () => signal.set(signal.get() + 1); const increment = () => signal.set(signal.get() + 1);
const decrement = () => signal.set(signal.get() - 1); const decrement = () => signal.set(signal.get() - 1);
// Return the original signal with added methods // Return the original signal with added methods
return Object.assign(signal, { return { signal, increment, decrement };
increment, }
decrement
});
}
// Usage // Usage
const counter = createEnhancedSignal(0); const counter = createEnhancedSignal(0);
el("button")({ onclick: () => counter.increment() }, "Increment"); el("button", { textContent: "Increment", onclick: () => counter.increment() });
el("div", S.text\`Count: \${counter}\`); el("div", S.text\`Count: \${counter}\`);
`.trim(), page_id }), `, 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(h3, t`Using Signals Independently`),
el("p").append(...T` 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. This can be useful when you need reactivity in non-UI code or want to integrate with other libraries.
`), `),
el("p").append(...T` el("p").append(...T`
@ -175,32 +238,34 @@ el("div", S.text\`Count: \${counter}\`);
`), `),
el("ol").append( el("ol").append(
el("li").append(...T` el("li").append(...T`
${el("strong", "Standard import")}: ${el("code", "import { S } from \"deka-dom-el/signals\";")} ${el("strong", "Standard import")}: ${el("code", `import { S } from "deka-dom-el/signals";`)}
This automatically registers signals with DDE's DOM reactivity system This automatically registers signals with DDEs DOM reactivity system
`), `),
el("li").append(...T` 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 This gives you just the signal system without DOM integration
`) `)
), ),
el(code, { content: `// Independent signals without DOM integration el(code, { content: `
import { signal as S, isSignal } from "deka-dom-el/src/signals-lib"; // Independent signals without DOM integration
import { signal, isSignal } from "deka-dom-el/src/signals-lib";
// Create and use signals as usual // Create and use signals as usual
const count = S(0); const count = signal(0);
const doubled = S(() => count.get() * 2); const doubled = signal(() => count.get() * 2);
// Subscribe to changes // Subscribe to changes
S.on(count, value => console.log(value)); signal.on(count, value => console.log(value));
// Update signal value // Update signal value
count.set(5); // Logs: 5 count.set(5); // Logs: 5
console.log(doubled.get()); // 10`, page_id }), console.log(doubled.get()); // 10
`, page_id }),
el("p").append(...T` el("p").append(...T`
The independent signals API includes all core functionality (${el("code", "S()")}, ${el("code", "S.on()")}, The independent signals API includes all core functionality (${el("code", "S()")}, ${el("code", "S.on()")},
${el("code", "S.action()")}). ${el("code", "S.action()")}).
`), `),
el("div", { class: "callout" }).append( el("div", { className: "callout" }).append(
el("h4", t`When to Use Independent Signals`), el("h4", t`When to Use Independent Signals`),
el("ul").append( el("ul").append(
el("li", t`For non-UI state management in your application`), 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("ol").append(
el("li").append(...T` el("li").append(...T`
${el("strong", "Use AbortSignals for cleanup:")} Always implement proper resource cleanup with ${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("li").append(...T`
${el("strong", "Separate core logic from library adaptation:")} Make your core functionality work ${el("strong", "Separate core logic from library adaptation:")} Make your core functionality work
with standard DOM APIs when possible 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("li").append(...T`
${el("strong", "Document clearly:")} Provide clear documentation on how your extension works ${el("strong", "Document clearly:")} Provide clear documentation on how your extension works
and what resources it uses and what resources it uses
@ -247,8 +316,11 @@ console.log(doubled.get()); // 10`, page_id }),
el("dt", t`Mutating element prototypes`), el("dt", t`Mutating element prototypes`),
el("dd", t`Prefer compositional approaches with addons over modifying 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("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` 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. 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", 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(code, { src: fileURL("./components/examples/ssr/intro.js"), page_id }),
el(h3, t`Why Server-Side Rendering?`), el(h3, t`Why Server-Side Rendering?`),
@ -48,40 +62,40 @@ export function page({ pkg, info }){
el(h3, t`How jsdom Integration Works`), el(h3, t`How jsdom Integration Works`),
el("p").append(...T` el("p").append(...T`
The jsdom export in dd<el> provides the necessary tools to use the library in Node.js 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("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`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`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`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("li", t`Handles DOM property/attribute mapping differences between browsers and jsdom`)
), ),
el(code, { src: fileURL("./components/examples/ssr/start.js"), page_id }), el(code, { src: fileURL("./components/examples/ssr/start.js"), page_id }),
el(h3, t`Basic SSR Example`), el(h3, t`Basic SSR Example`),
el("p").append(...T` 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(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` el("p").append(...T`
You can build a complete static site generator with dd<el>. In fact, this documentation site 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: 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(code, { src: fileURL("./components/examples/ssr/static-site-generator.js"), page_id }),
el(h3, t`Working with Async Content in SSR`), el(h3, t`Working with Async Content in SSR`),
el("p").append(...T` 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. 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(code, { src: fileURL("./components/examples/ssr/async-data.js"), page_id }),
el(h3, t`Working with Dynamic Imports for SSR`), el(h3, t`Working with Dynamic Imports for SSR`),
el("p").append(...T` 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. for both the deka-dom-el/jsdom module and your page components.
`), `),
el("p").append(...T` el("p").append(...T`
@ -94,7 +108,7 @@ export function page({ pkg, info }){
`), `),
el("li").append(...T` el("li").append(...T`
${el("strong", "Environment registration timing:")} The jsdom module auto-registers the DOM environment ${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("em", "before")} you import your components using ${el("code", "import { el } from \"deka-dom-el\";")}.
`), `),
el("li").append(...T` el("li").append(...T`
@ -113,9 +127,9 @@ export function page({ pkg, info }){
`), `),
el("ul").append( el("ul").append(
el("li", t`Browser-specific APIs like window.localStorage are not available in jsdom by default`), 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`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` el("p").append(...T`
For advanced SSR applications, consider implementing hydration on the client-side to restore 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(h3, t`Real Example: How This Documentation is Built`),
el("p").append(...T` 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. 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 }), 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] | CustomElementTagNameMap[keyof CustomElementTagNameMap]
declare global { declare global {
type ddeComponentAttributes= Record<any, any> | undefined; 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 ddeString= string | ddeSignal<string, {}>
type ddeStringable= ddeString | number | ddeSignal<number, {}> type ddeStringable= ddeString | number | ddeSignal<number, {}>
} }
type Host<EL extends SupportedElement>= (...addons: ddeElementAddon<EL>[])=> EL;
type PascalCase= `${Capitalize<string>}${string}`; type PascalCase= `${Capitalize<string>}${string}`;
type AttrsModified= { type AttrsModified= {
/** /**
@ -84,6 +85,7 @@ export namespace el {
is_open?: boolean is_open?: boolean
): Comment; ): Comment;
} }
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement
export function el< export function el<
A extends ddeComponentAttributes, A extends ddeComponentAttributes,
@ -155,7 +157,6 @@ export function elNS(
)=> SupportedElement )=> SupportedElement
export { elNS as createElementNS } export { elNS as createElementNS }
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
/** Simulate slots for ddeComponents */ /** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>( export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
root: EL, root: EL,
@ -170,14 +171,14 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
body: EL, body: EL,
): EL ): EL
export function dispatchEvent(name: keyof DocumentEventMap | string, element: SupportedElement): export function dispatchEvent(name: keyof DocumentEventMap | string, host: Host<SupportedElement>):
(data?: any)=> void; (data?: any)=> void;
export function dispatchEvent(name: keyof DocumentEventMap | string, options?: EventInit): export function dispatchEvent(name: keyof DocumentEventMap | string, options?: EventInit):
(element: SupportedElement, data?: any)=> void; (element: SupportedElement, data?: any)=> void;
export function dispatchEvent( export function dispatchEvent(
name: keyof DocumentEventMap | string, name: keyof DocumentEventMap | string,
options: EventInit | null, options: EventInit | null,
element: SupportedElement | (()=> SupportedElement) host: Host<SupportedElement>
): (data?: any)=> void; ): (data?: any)=> void;
interface On{ interface On{
/** Listens to the DOM event. See {@link Document.addEventListener} */ /** Listens to the DOM event. See {@link Document.addEventListener} */
@ -225,7 +226,7 @@ export const on: On;
type Scope= { type Scope= {
scope: Node | Function | Object, scope: Node | Function | Object,
host: ddeElementAddon<any>, host: Host<SupportedElement>,
custom_element: false | HTMLElement, custom_element: false | HTMLElement,
prevent: boolean 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) * It can be also used to register Addon(s) (functions to be called when component is initized)
* `scope.host(on.connected(console.log))`. * `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[], state: Scope[],
/** Adds new child scope. All attributes are inherited by default. */ /** Adds new child scope. All attributes are inherited by default. */
@ -262,7 +268,6 @@ export function customElementRender<
): EL ): EL
export function customElementWithDDE<EL extends (new ()=> HTMLElement)>(custom_element: EL): 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 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 * 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>; 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 */ /* TypeScript MEH */
declare global{ declare global{
type ddeAppend<el>= (...nodes: (Node | string)[])=> el; type ddeAppend<el>= (...nodes: (Node | string)[])=> el;

View File

@ -1,3 +1,5 @@
export * from "./src/dom.js"; export * from "./src/dom.js";
export * from "./src/customElement.js"; export * from "./src/customElement.js";
export * from "./src/events.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= { type JSDOM= {
window: Window, window: Window,
document: Document, document: Document,
Node: typeof Node,
HTMLElement: typeof HTMLElement, HTMLElement: typeof HTMLElement,
SVGElement: typeof SVGElement, SVGElement: typeof SVGElement,
DocumentFragment: typeof DocumentFragment, 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'; import { enviroment as env } from './src/dom-common.js';
env.ssr= " ssr"; env.ssr= " ssr";
@ -17,7 +16,9 @@ env.setDeleteAttr= function(obj, prop, value){
if(value) return obj.setAttribute(prop, ""); if(value) return obj.setAttribute(prop, "");
obj.removeAttribute(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 env_bk= {};
let dom_last; let dom_last;

254
package-lock.json generated
View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "deka-dom-el", "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.", "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>", "author": "Jan Andrle <andrle.jan@centrum.cz>",
"license": "MIT", "license": "MIT",
@ -36,7 +36,8 @@
"files": [ "files": [
"*.js", "*.js",
"*.d.ts", "*.d.ts",
"src" "src",
"tsconfig.json"
], ],
"engines": { "engines": {
"node": ">=18" "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 SymbolOnclear= symbol;
type Actions<V>= Record<string | SymbolOnclear, Action<V>>; type Actions<V>= Record<string | SymbolOnclear, Action<V>>;
type OnListenerOptions= Pick<AddEventListenerOptions, "signal"> & { first_time?: boolean }; type OnListenerOptions= Pick<AddEventListenerOptions, "signal"> & { first_time?: boolean };
type SElement= Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
interface signal{ interface signal{
_: Symbol _: Symbol
/** /**
@ -61,7 +62,7 @@ interface signal{
* S.el(listS, list=> list.map(li=> el("li", li))); * 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, {}>>; 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 {Element|ShadowRoot} target - The custom element or shadow root to render into
* @param {Function} render - The render function that returns content * @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 * @returns {Node} The rendered content
*/ */
export function customElementRender(target, render, props= observedAttributes){ export function customElementRender(target, render, props= {}){
const custom_element= target.host || target; const custom_element= target.host || target;
scope.push({ scope.push({
scope: custom_element, scope: custom_element,
@ -69,15 +69,3 @@ export { lifecyclesToEvents as customElementWithDDE };
function wrapMethod(obj, method, apply){ function wrapMethod(obj, method, apply){
obj[method]= new Proxy(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, setDeleteAttr,
ssr: "", ssr: "",
D: globalThis.document, D: globalThis.document,
N: globalThis.Node,
F: globalThis.DocumentFragment, F: globalThis.DocumentFragment,
H: globalThis.HTMLElement, H: globalThis.HTMLElement,
S: globalThis.SVGElement, S: globalThis.SVGElement,

View File

@ -1,6 +1,7 @@
import { signals } from "./signals-lib/common.js"; import { signals } from "./signals-lib/common.js";
import { enviroment as env } from './dom-common.js'; import { enviroment as env } from './dom-common.js';
import { isInstance, isUndef, oAssign } from "./helpers.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 * 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, host: c=> c ? c(env.D.body) : env.D.body,
prevent: true, prevent: true,
} ]; } ];
/** Store for disconnect abort controllers */
const store_abort= new WeakMap();
/** /**
* Scope management utility for tracking component hierarchies * Scope management utility for tracking component hierarchies
*/ */
@ -35,6 +38,19 @@ export const scope= {
*/ */
get host(){ return this.current.host; }, 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 * Prevents default behavior in the current scope
* @returns {Object} Current scope context * @returns {Object} Current scope context

View File

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

View File

@ -1,5 +1,4 @@
export { registerReactivity } from './signals-lib/common.js'; import { keyLTE, evc, evd } from './dom-common.js';
import { enviroment as env, keyLTE, evc, evd, eva } from './dom-common.js';
import { oAssign, onAbort } from './helpers.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 }); const lifeOptions= obj=> oAssign({}, typeof obj==="object" ? obj : null, { once: true });
//TODO: cleanUp when event before abort? //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 * Creates a function to register connected lifecycle event listeners
@ -88,53 +86,3 @@ on.disconnected= function(listener, options){
return element; 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 */ /** Property key for tracking reactive elements */
const key_reactive= "__dde_reactive"; 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 { el } from "../dom.js";
import { scope } from "../dom.js"; import { scope } from "../dom.js";
import { on } from "../events.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 * 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 {Object} s - Signal object to watch
* @param {Function} map - Function mapping signal value to DOM elements * @param {Function} map - Function mapping signal value to DOM elements
* @returns {DocumentFragment} Fragment containing reactive elements * @returns {DocumentFragment} Fragment containing reactive elements
*/ */
signal.el= function(s, map){ 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_start= el.mark({ type: "reactive", source: new Defined().compact }, true);
const mark_end= mark_start.end; const mark_end= mark_start.end;
const out= env.D.createDocumentFragment(); const out= env.D.createDocumentFragment();
out.append(mark_start, mark_end); out.append(mark_start, mark_end);
const { current }= scope; const { current }= scope;
let cache_shared= oCreate();
const reRenderReactiveElement= v=> { const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered
return removeSignalListener(s, reRenderReactiveElement); return removeSignalListener(s, reRenderReactiveElement);
const memo= cache(cache_shared);
cache_shared= oCreate();
scope.push(current); scope.push(current);
let els= map(v, function useCache(key, fun){ let els= map(v);
return (cache_shared[key]= memo(key, fun));
});
scope.pop(); scope.pop();
if(!Array.isArray(els)) if(!Array.isArray(els))
els= [ els ]; els= [ els ];
@ -209,7 +202,7 @@ signal.el= function(s, map){
reRenderReactiveElement(s.get()); reRenderReactiveElement(s.get());
current.host(on.disconnected(()=> current.host(on.disconnected(()=>
/*! Clears cached elements for reactive element `S.el` */ /*! Clears cached elements for reactive element `S.el` */
cache_shared= {} map.clear()
)); ));
return out; return out;
}; };
@ -265,7 +258,7 @@ const key_attributes= "__dde_attributes";
signal.observedAttributes= function(element){ signal.observedAttributes= function(element){
const store= element[key_attributes]= {}; const store= element[key_attributes]= {};
const attrs= observedAttributes(element, observedAttribute(store)); const attrs= observedAttributes(element, observedAttribute(store));
on.attributeChanged(function attributeChangeToSignal({ detail }){ on(eva, function attributeChangeToSignal({ detail }){
/*! This maps attributes to signals (`S.observedAttributes`). /*! This maps attributes to signals (`S.observedAttributes`).
Investigate `__dde_attributes` key of the element. */ Investigate `__dde_attributes` key of the element. */
const [ name, value ]= detail; const [ name, value ]= detail;