mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-04-01 11:45:53 +02:00
🔤 🐛 ⚡ v0.9.1-alpha (#30)
* :tap: removed on.attributeChanged and static observedAttributes * ⚡ import optimalization * ⚡ scope.signal * 🔤 🐛 * ⚡ 🐛 registerReactivity and types * 🔤 * ⚡ * 🔤 * 🐛 Node in enviroment * ⚡ todos * ⚡ * ⚡ 🔤 * ⚡ lint * ⚡ memo * 🔤 🐛 memo * ⚡ 🔤 todomvc * 🐛 types * 🔤 p08 signal factory * 🔤 ⚡ types * ⚡ 🔤 lint * 🔤 * 🔤 * 🔤 * 🔤 * 📺
This commit is contained in:
parent
e1f321004d
commit
25d475ec04
@ -63,6 +63,7 @@ Creating reactive elements, components, and Web Components using the native
|
||||
- ☑️ **Lightweight** — ~10-15kB minified (original goal 10kB) with zero/minimal dependencies
|
||||
- ✅ **Declarative & functional approach** for clean, maintainable code
|
||||
- ✅ **Signals and events** for reactive UI
|
||||
- ✅ **Memoization for performance** — optimize rendering with intelligent caching
|
||||
- ✅ **Optional build-in signals** with support for custom reactive implementations
|
||||
- ✅ **Server-side rendering** support via [jsdom](https://github.com/jsdom/jsdom)
|
||||
- ✅ **TypeScript support** (work in progress)
|
||||
@ -122,3 +123,4 @@ Signals are the reactive backbone of Deka DOM Elements:
|
||||
- [potch/signals](https://github.com/potch/signals) - A small reactive signals library
|
||||
- [jaandrle/dollar_dom_component](https://github.com/jaandrle/dollar_dom_component) -
|
||||
Functional DOM components without JSX/virtual DOM
|
||||
- [mxjp/rvx: A signal based frontend framework](https://github.com/mxjp/rvx)
|
||||
|
@ -4,11 +4,13 @@ const files= [ "index", "index-with-signals" ];
|
||||
|
||||
$.api("")
|
||||
.command("main", "Build main files", { default: true })
|
||||
.action(async function main(){
|
||||
.option("--no-types", "Also generate d.ts files", false)
|
||||
.action(async function main({ types }){
|
||||
const regular = await build({
|
||||
files,
|
||||
filesOut,
|
||||
minify: "no",
|
||||
types,
|
||||
});
|
||||
const min = await build({
|
||||
files,
|
||||
@ -18,6 +20,7 @@ $.api("")
|
||||
return out.slice(0, idx)+".min"+out.slice(idx);
|
||||
},
|
||||
minify: "full",
|
||||
types,
|
||||
});
|
||||
return $.exit(regular + min);
|
||||
})
|
||||
|
@ -3,7 +3,7 @@ const css= echo.css`
|
||||
.info{ color: gray; }
|
||||
`;
|
||||
|
||||
export async function build({ files, filesOut, minify= "partial", iife= true }){
|
||||
export function build({ files, filesOut, minify= "partial", iife= true, types= true }){
|
||||
for(const file_root of files){
|
||||
const file= file_root+".js";
|
||||
echo(`Processing ${file} (minified: ${minify})`);
|
||||
@ -11,20 +11,22 @@ export async function build({ files, filesOut, minify= "partial", iife= true }){
|
||||
const esbuild_output= buildEsbuild({ file, out, minify });
|
||||
echoVariant(esbuild_output.stderr.split("\n")[1].trim());
|
||||
|
||||
const file_dts= file_root+".d.ts";
|
||||
const file_dts_out= filesOut(file_dts);
|
||||
echoVariant(file_dts_out, true);
|
||||
buildDts({
|
||||
bundle: out,
|
||||
entry: file_dts,
|
||||
});
|
||||
echoVariant(file_dts_out);
|
||||
if(types){
|
||||
const file_dts= file_root+".d.ts";
|
||||
const file_dts_out= filesOut(file_dts);
|
||||
echoVariant(file_dts_out, true);
|
||||
buildDts({
|
||||
bundle: out,
|
||||
entry: file_dts,
|
||||
});
|
||||
echoVariant(file_dts_out);
|
||||
}
|
||||
|
||||
if(iife) toIIFE(file, file_root);
|
||||
if(iife) toIIFE(file, file_root, types);
|
||||
}
|
||||
return 0;
|
||||
|
||||
async function toIIFE(file, file_root){
|
||||
function toIIFE(file, file_root, types){
|
||||
const fileMark= "iife";
|
||||
const name= "DDE";
|
||||
const out= filesOut(file_root+".js", fileMark);
|
||||
@ -36,6 +38,7 @@ export async function build({ files, filesOut, minify= "partial", iife= true }){
|
||||
const dde_output= buildEsbuild({ file, out, minify, params });
|
||||
echoVariant(`${out} (${name})`)
|
||||
|
||||
if(!types) return dde_output;
|
||||
const file_dts= file_root+".d.ts";
|
||||
const file_dts_out= filesOut(file_dts, fileMark);
|
||||
echoVariant(file_dts_out, true);
|
||||
|
10
bs/lint.sh
10
bs/lint.sh
@ -1,5 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eou pipefail
|
||||
npx editorconfig-checker -format gcc
|
||||
# if $1=vim -no-color
|
||||
one=${1:-''}
|
||||
additional=''
|
||||
[ "$one" = 'vim' ] && additional='-no-color'
|
||||
npx editorconfig-checker -format gcc ${additional}
|
||||
[ "$one" = 'vim' ] && additional='--reporter unix'
|
||||
npx jshint index.js src ${additional}
|
||||
[ "$one" = 'vim' ] && exit 0
|
||||
npx size-limit
|
||||
npx jshint index.js src
|
||||
|
80
dist/esm-with-signals.d.ts
vendored
80
dist/esm-with-signals.d.ts
vendored
@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
|
||||
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
|
||||
first_time?: boolean;
|
||||
};
|
||||
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
|
||||
export interface signal {
|
||||
_: Symbol;
|
||||
/**
|
||||
@ -63,7 +64,7 @@ export interface signal {
|
||||
* S.el(listS, list=> list.map(li=> el("li", li)));
|
||||
* ```
|
||||
* */
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
|
||||
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
|
||||
}
|
||||
export const signal: signal;
|
||||
@ -80,10 +81,11 @@ export type CustomElementTagNameMap = {
|
||||
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
|
||||
declare global {
|
||||
type ddeComponentAttributes = Record<any, any> | undefined;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
|
||||
type ddeString = string | Signal<string, {}>;
|
||||
type ddeStringable = ddeString | number | Signal<number, {}>;
|
||||
}
|
||||
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
|
||||
export type PascalCase = `${Capitalize<string>}${string}`;
|
||||
export type AttrsModified = {
|
||||
/**
|
||||
@ -145,6 +147,7 @@ export namespace el {
|
||||
host?: "this" | "parentElement";
|
||||
}, is_open?: boolean): Comment;
|
||||
}
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
|
||||
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
|
||||
export function el<A extends {
|
||||
textContent: ddeStringable;
|
||||
@ -157,7 +160,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
|
||||
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
|
||||
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
|
||||
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
|
||||
/** Simulate slots for ddeComponents */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
|
||||
/**
|
||||
@ -166,9 +168,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
|
||||
* @param body Body of the custom element
|
||||
* */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
@ -186,7 +188,7 @@ export interface On {
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
scope: Node | Function | Object;
|
||||
host: ddeElementAddon<any>;
|
||||
host: Host<SupportedElement>;
|
||||
custom_element: false | HTMLElement;
|
||||
prevent: boolean;
|
||||
};
|
||||
@ -202,7 +204,11 @@ export const scope: {
|
||||
* It can be also used to register Addon(s) (functions to be called when component is initized)
|
||||
* — `scope.host(on.connected(console.log))`.
|
||||
* */
|
||||
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
|
||||
host: Host<SupportedElement>;
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
signal: AbortSignal;
|
||||
state: Scope[];
|
||||
/** Adds new child scope. All attributes are inherited by default. */
|
||||
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
|
||||
@ -214,7 +220,6 @@ export const scope: {
|
||||
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
|
||||
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
|
||||
/**
|
||||
* This is used primarly for server side rendering. To be sure that all async operations
|
||||
* are finished before the page is sent to the client.
|
||||
@ -235,6 +240,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
|
||||
* ```
|
||||
* */
|
||||
export function queue(promise?: Promise<unknown>): Promise<unknown>;
|
||||
/**
|
||||
* Memoization utility for caching DOM elements to improve performance.
|
||||
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
|
||||
*
|
||||
* @param key - Unique identifier for the element (usually an ID or unique value)
|
||||
* @param generator - Function that creates the element
|
||||
* @returns The cached element if the key exists, otherwise the result of the generator function
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Within S.el for list rendering
|
||||
* S.el(itemsSignal, (items, memo) =>
|
||||
* el("ul").append(
|
||||
* ...items.map(item =>
|
||||
* memo(item.id, () => el(ItemComponent, item))
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
|
||||
/**
|
||||
* Memo namespace containing utility functions for memoization.
|
||||
*/
|
||||
export namespace memo {
|
||||
/**
|
||||
* Checks if an object is a memo scope.
|
||||
* @param obj - The object to check
|
||||
* @returns True if the object is a memo scope
|
||||
*/
|
||||
export function isScope(obj: any): boolean;
|
||||
/**
|
||||
* Creates a memoized function with optional cleanup support.
|
||||
*
|
||||
* @param fun - The function to memoize
|
||||
* @param options - Configuration options
|
||||
* @param options.signal - AbortSignal for cleanup
|
||||
* @param options.onlyLast - When true, only keeps the cache from the most recent call
|
||||
* @returns A memoized version of the function with a .clear() method
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const renderItems = memo.scope(function(items) {
|
||||
* return items.map(item =>
|
||||
* memo(item.id, () => el("div", item.name))
|
||||
* );
|
||||
* }, {
|
||||
* signal: controller.signal,
|
||||
* onlyLast: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function scope<F extends Function>(fun: F, options?: {
|
||||
signal?: AbortSignal;
|
||||
onlyLast?: boolean;
|
||||
}): F & {
|
||||
clear: () => void;
|
||||
};
|
||||
}
|
||||
/* TypeScript MEH */
|
||||
declare global {
|
||||
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;
|
||||
|
510
dist/esm-with-signals.js
vendored
510
dist/esm-with-signals.js
vendored
@ -32,8 +32,8 @@ function onAbort(signal2, listener) {
|
||||
};
|
||||
}
|
||||
function observedAttributes(instance, observedAttribute2) {
|
||||
const { observedAttributes: observedAttributes3 = [] } = instance.constructor;
|
||||
return observedAttributes3.reduce(function(out, name) {
|
||||
const { observedAttributes: observedAttributes2 = [] } = instance.constructor;
|
||||
return observedAttributes2.reduce(function(out, name) {
|
||||
out[kebabToCamel(name)] = observedAttribute2(instance, name);
|
||||
return out;
|
||||
}, {});
|
||||
@ -91,6 +91,7 @@ var enviroment = {
|
||||
setDeleteAttr,
|
||||
ssr: "",
|
||||
D: globalThis.document,
|
||||
N: globalThis.Node,
|
||||
F: globalThis.DocumentFragment,
|
||||
H: globalThis.HTMLElement,
|
||||
S: globalThis.SVGElement,
|
||||
@ -111,6 +112,215 @@ var evc = "dde:connected";
|
||||
var evd = "dde:disconnected";
|
||||
var eva = "dde:attributeChanged";
|
||||
|
||||
// src/events-observer.js
|
||||
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
|
||||
get() {
|
||||
return () => {
|
||||
};
|
||||
}
|
||||
});
|
||||
function connectionsChangesObserverConstructor() {
|
||||
const store = /* @__PURE__ */ new Map();
|
||||
let is_observing = false;
|
||||
const observerListener = (stop2) => function(mutations) {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "childList") continue;
|
||||
if (observerAdded(mutation.addedNodes, true)) {
|
||||
stop2();
|
||||
continue;
|
||||
}
|
||||
if (observerRemoved(mutation.removedNodes, true))
|
||||
stop2();
|
||||
}
|
||||
};
|
||||
const observer = new enviroment.M(observerListener(stop));
|
||||
return {
|
||||
/**
|
||||
* Creates an observer for a specific element
|
||||
* @param {Element} element - Element to observe
|
||||
* @returns {Function} Cleanup function
|
||||
*/
|
||||
observe(element) {
|
||||
const o = new enviroment.M(observerListener(() => {
|
||||
}));
|
||||
o.observe(element, { childList: true, subtree: true });
|
||||
return () => o.disconnect();
|
||||
},
|
||||
/**
|
||||
* Register a connection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for connection event
|
||||
*/
|
||||
onConnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.connected.has(listener)) return;
|
||||
listeners.connected.add(listener);
|
||||
listeners.length_c += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a connection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offConnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
if (!ls.connected.has(listener)) return;
|
||||
ls.connected.delete(listener);
|
||||
ls.length_c -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
},
|
||||
/**
|
||||
* Register a disconnection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for disconnection event
|
||||
*/
|
||||
onDisconnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.disconnected.has(listener)) return;
|
||||
listeners.disconnected.add(listener);
|
||||
listeners.length_d += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a disconnection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offDisconnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
ls.disconnected.delete(listener);
|
||||
ls.length_d -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
}
|
||||
};
|
||||
function cleanWhenOff(element, ls) {
|
||||
if (ls.length_c || ls.length_d)
|
||||
return;
|
||||
store.delete(element);
|
||||
stop();
|
||||
}
|
||||
function getElementStore(element) {
|
||||
if (store.has(element)) return store.get(element);
|
||||
const out = {
|
||||
connected: /* @__PURE__ */ new WeakSet(),
|
||||
length_c: 0,
|
||||
disconnected: /* @__PURE__ */ new WeakSet(),
|
||||
length_d: 0
|
||||
};
|
||||
store.set(element, out);
|
||||
return out;
|
||||
}
|
||||
function start() {
|
||||
if (is_observing) return;
|
||||
is_observing = true;
|
||||
observer.observe(enviroment.D.body, { childList: true, subtree: true });
|
||||
}
|
||||
function stop() {
|
||||
if (!is_observing || store.size) return;
|
||||
is_observing = false;
|
||||
observer.disconnect();
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
async function collectChildren(element) {
|
||||
if (store.size > 30)
|
||||
await requestIdle();
|
||||
const out = [];
|
||||
if (!isInstance(element, enviroment.N)) return out;
|
||||
for (const el of store.keys()) {
|
||||
if (el === element || !isInstance(el, enviroment.N)) continue;
|
||||
if (element.contains(el))
|
||||
out.push(el);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerAdded(addedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of addedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerAdded);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_c) continue;
|
||||
element.dispatchEvent(new Event(evc));
|
||||
ls.connected = /* @__PURE__ */ new WeakSet();
|
||||
ls.length_c = 0;
|
||||
if (!ls.length_d) store.delete(element);
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerRemoved(removedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of removedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerRemoved);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_d) continue;
|
||||
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function dispatchRemove(element) {
|
||||
return () => {
|
||||
if (element.isConnected) return;
|
||||
element.dispatchEvent(new Event(evd));
|
||||
store.delete(element);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
options = null;
|
||||
}
|
||||
if (!options) options = {};
|
||||
return function dispatch(element, ...d) {
|
||||
if (host) {
|
||||
d.unshift(element);
|
||||
element = typeof host === "function" ? host() : host;
|
||||
}
|
||||
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
|
||||
return element.dispatchEvent(event);
|
||||
};
|
||||
}
|
||||
function on(event, listener, options) {
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(event, listener, options);
|
||||
return element;
|
||||
};
|
||||
}
|
||||
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
|
||||
on.connected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evc, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
|
||||
if (c) c_ch_o.onConnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
};
|
||||
on.disconnected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evd, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
|
||||
if (c) c_ch_o.onDisconnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
};
|
||||
|
||||
// src/dom.js
|
||||
function queue(promise) {
|
||||
return enviroment.q(promise);
|
||||
@ -122,6 +332,7 @@ var scopes = [{
|
||||
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
|
||||
prevent: true
|
||||
}];
|
||||
var store_abort = /* @__PURE__ */ new WeakMap();
|
||||
var scope = {
|
||||
/**
|
||||
* Gets the current scope
|
||||
@ -138,6 +349,17 @@ var scope = {
|
||||
return this.current.host;
|
||||
},
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
get signal() {
|
||||
const { host } = this;
|
||||
if (store_abort.has(host)) return store_abort.get(host);
|
||||
const a = new AbortController();
|
||||
store_abort.set(host, a);
|
||||
host(on.disconnected(() => a.abort()));
|
||||
return a.signal;
|
||||
},
|
||||
/**
|
||||
* Prevents default behavior in the current scope
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
@ -380,172 +602,8 @@ function setDelete(obj, key, val) {
|
||||
return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
|
||||
// src/events-observer.js
|
||||
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
|
||||
get() {
|
||||
return () => {
|
||||
};
|
||||
}
|
||||
});
|
||||
function connectionsChangesObserverConstructor() {
|
||||
const store = /* @__PURE__ */ new Map();
|
||||
let is_observing = false;
|
||||
const observerListener = (stop2) => function(mutations) {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "childList") continue;
|
||||
if (observerAdded(mutation.addedNodes, true)) {
|
||||
stop2();
|
||||
continue;
|
||||
}
|
||||
if (observerRemoved(mutation.removedNodes, true))
|
||||
stop2();
|
||||
}
|
||||
};
|
||||
const observer = new enviroment.M(observerListener(stop));
|
||||
return {
|
||||
/**
|
||||
* Creates an observer for a specific element
|
||||
* @param {Element} element - Element to observe
|
||||
* @returns {Function} Cleanup function
|
||||
*/
|
||||
observe(element) {
|
||||
const o = new enviroment.M(observerListener(() => {
|
||||
}));
|
||||
o.observe(element, { childList: true, subtree: true });
|
||||
return () => o.disconnect();
|
||||
},
|
||||
/**
|
||||
* Register a connection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for connection event
|
||||
*/
|
||||
onConnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.connected.has(listener)) return;
|
||||
listeners.connected.add(listener);
|
||||
listeners.length_c += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a connection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offConnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
if (!ls.connected.has(listener)) return;
|
||||
ls.connected.delete(listener);
|
||||
ls.length_c -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
},
|
||||
/**
|
||||
* Register a disconnection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for disconnection event
|
||||
*/
|
||||
onDisconnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.disconnected.has(listener)) return;
|
||||
listeners.disconnected.add(listener);
|
||||
listeners.length_d += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a disconnection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offDisconnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
ls.disconnected.delete(listener);
|
||||
ls.length_d -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
}
|
||||
};
|
||||
function cleanWhenOff(element, ls) {
|
||||
if (ls.length_c || ls.length_d)
|
||||
return;
|
||||
store.delete(element);
|
||||
stop();
|
||||
}
|
||||
function getElementStore(element) {
|
||||
if (store.has(element)) return store.get(element);
|
||||
const out = {
|
||||
connected: /* @__PURE__ */ new WeakSet(),
|
||||
length_c: 0,
|
||||
disconnected: /* @__PURE__ */ new WeakSet(),
|
||||
length_d: 0
|
||||
};
|
||||
store.set(element, out);
|
||||
return out;
|
||||
}
|
||||
function start() {
|
||||
if (is_observing) return;
|
||||
is_observing = true;
|
||||
observer.observe(enviroment.D.body, { childList: true, subtree: true });
|
||||
}
|
||||
function stop() {
|
||||
if (!is_observing || store.size) return;
|
||||
is_observing = false;
|
||||
observer.disconnect();
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
async function collectChildren(element) {
|
||||
if (store.size > 30)
|
||||
await requestIdle();
|
||||
const out = [];
|
||||
if (!isInstance(element, Node)) return out;
|
||||
for (const el of store.keys()) {
|
||||
if (el === element || !isInstance(el, Node)) continue;
|
||||
if (element.contains(el))
|
||||
out.push(el);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerAdded(addedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of addedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerAdded);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_c) continue;
|
||||
element.dispatchEvent(new Event(evc));
|
||||
ls.connected = /* @__PURE__ */ new WeakSet();
|
||||
ls.length_c = 0;
|
||||
if (!ls.length_d) store.delete(element);
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerRemoved(removedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of removedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerRemoved);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_d) continue;
|
||||
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function dispatchRemove(element) {
|
||||
return () => {
|
||||
if (element.isConnected) return;
|
||||
element.dispatchEvent(new Event(evd));
|
||||
store.delete(element);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// src/customElement.js
|
||||
function customElementRender(target, render, props = observedAttributes2) {
|
||||
function customElementRender(target, render, props = {}) {
|
||||
const custom_element = target.host || target;
|
||||
scope.push({
|
||||
scope: custom_element,
|
||||
@ -586,81 +644,40 @@ function wrapMethod(obj, method, apply) {
|
||||
obj[method] = new Proxy(obj[method] || (() => {
|
||||
}), { apply });
|
||||
}
|
||||
function observedAttributes2(instance) {
|
||||
return observedAttributes(instance, (i, n) => i.getAttribute(n));
|
||||
}
|
||||
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
options = null;
|
||||
}
|
||||
if (!options) options = {};
|
||||
return function dispatch(element, ...d) {
|
||||
if (host) {
|
||||
d.unshift(element);
|
||||
element = typeof host === "function" ? host() : host;
|
||||
}
|
||||
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
|
||||
return element.dispatchEvent(event);
|
||||
};
|
||||
// src/memo.js
|
||||
var memoMark = "__dde_memo";
|
||||
var memo_scope = [];
|
||||
function memo(key, generator) {
|
||||
if (!memo_scope.length) return generator(key);
|
||||
const k = typeof key === "object" ? JSON.stringify(key) : key;
|
||||
const [{ cache, after }] = memo_scope;
|
||||
return after(k, hasOwn(cache, k) ? cache[k] : generator(key));
|
||||
}
|
||||
function on(event, listener, options) {
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(event, listener, options);
|
||||
return element;
|
||||
};
|
||||
}
|
||||
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
|
||||
on.connected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evc, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
|
||||
if (c) c_ch_o.onConnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
memo.isScope = function(obj) {
|
||||
return obj[memoMark];
|
||||
};
|
||||
on.disconnected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evd, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
|
||||
if (c) c_ch_o.onDisconnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
};
|
||||
var store_abort = /* @__PURE__ */ new WeakMap();
|
||||
on.disconnectedAsAbort = function(host) {
|
||||
if (store_abort.has(host)) return store_abort.get(host);
|
||||
const a = new AbortController();
|
||||
store_abort.set(host, a);
|
||||
host(on.disconnected(() => a.abort()));
|
||||
return a.signal;
|
||||
};
|
||||
var els_attribute_store = /* @__PURE__ */ new WeakSet();
|
||||
on.attributeChanged = function(listener, options) {
|
||||
if (typeof options !== "object")
|
||||
options = {};
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(eva, listener, options);
|
||||
if (element[keyLTE] || els_attribute_store.has(element))
|
||||
return element;
|
||||
if (!enviroment.M) return element;
|
||||
const observer = new enviroment.M(function(mutations) {
|
||||
for (const { attributeName, target } of mutations)
|
||||
target.dispatchEvent(
|
||||
new CustomEvent(eva, { detail: [attributeName, target.getAttribute(attributeName)] })
|
||||
);
|
||||
memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
|
||||
let cache = oCreate();
|
||||
function memoScope2(...args) {
|
||||
if (signal2 && signal2.aborted)
|
||||
return fun.apply(this, args);
|
||||
let cache_local = onlyLast ? cache : oCreate();
|
||||
memo_scope.unshift({
|
||||
cache,
|
||||
after(key, val) {
|
||||
return cache_local[key] = val;
|
||||
}
|
||||
});
|
||||
const c = onAbort(options.signal, () => observer.disconnect());
|
||||
if (c) observer.observe(element, { attributes: true });
|
||||
return element;
|
||||
};
|
||||
const out = fun.apply(this, args);
|
||||
memo_scope.shift();
|
||||
cache = cache_local;
|
||||
return out;
|
||||
}
|
||||
memoScope2[memoMark] = true;
|
||||
memoScope2.clear = () => cache = oCreate();
|
||||
if (signal2) signal2.addEventListener("abort", memoScope2.clear);
|
||||
return memoScope2;
|
||||
};
|
||||
|
||||
// src/signals-lib/helpers.js
|
||||
@ -776,25 +793,18 @@ signal.clear = function(...signals2) {
|
||||
}
|
||||
};
|
||||
var key_reactive = "__dde_reactive";
|
||||
function cache(store = oCreate()) {
|
||||
return (key, fun) => hasOwn(store, key) ? store[key] : store[key] = fun();
|
||||
}
|
||||
signal.el = function(s, map) {
|
||||
map = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
|
||||
const mark_start = createElement.mark({ type: "reactive", source: new Defined().compact }, true);
|
||||
const mark_end = mark_start.end;
|
||||
const out = enviroment.D.createDocumentFragment();
|
||||
out.append(mark_start, mark_end);
|
||||
const { current } = scope;
|
||||
let cache_shared = oCreate();
|
||||
const reRenderReactiveElement = (v) => {
|
||||
if (!mark_start.parentNode || !mark_end.parentNode)
|
||||
return removeSignalListener(s, reRenderReactiveElement);
|
||||
const memo = cache(cache_shared);
|
||||
cache_shared = oCreate();
|
||||
scope.push(current);
|
||||
let els = map(v, function useCache(key, fun) {
|
||||
return cache_shared[key] = memo(key, fun);
|
||||
});
|
||||
let els = map(v);
|
||||
scope.pop();
|
||||
if (!Array.isArray(els))
|
||||
els = [els];
|
||||
@ -814,7 +824,7 @@ signal.el = function(s, map) {
|
||||
current.host(on.disconnected(
|
||||
() => (
|
||||
/*! Clears cached elements for reactive element `S.el` */
|
||||
cache_shared = {}
|
||||
map.clear()
|
||||
)
|
||||
));
|
||||
return out;
|
||||
@ -846,7 +856,7 @@ var key_attributes = "__dde_attributes";
|
||||
signal.observedAttributes = function(element) {
|
||||
const store = element[key_attributes] = {};
|
||||
const attrs = observedAttributes(element, observedAttribute(store));
|
||||
on.attributeChanged(function attributeChangeToSignal({ detail }) {
|
||||
on(eva, function attributeChangeToSignal({ detail }) {
|
||||
/*! This maps attributes to signals (`S.observedAttributes`).
|
||||
Investigate `__dde_attributes` key of the element. */
|
||||
const [name, value] = detail;
|
||||
@ -997,7 +1007,7 @@ export {
|
||||
elementAttribute,
|
||||
isSignal,
|
||||
lifecyclesToEvents,
|
||||
observedAttributes2 as observedAttributes,
|
||||
memo,
|
||||
on,
|
||||
queue,
|
||||
registerReactivity,
|
||||
|
80
dist/esm-with-signals.min.d.ts
vendored
80
dist/esm-with-signals.min.d.ts
vendored
@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
|
||||
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
|
||||
first_time?: boolean;
|
||||
};
|
||||
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
|
||||
export interface signal {
|
||||
_: Symbol;
|
||||
/**
|
||||
@ -63,7 +64,7 @@ export interface signal {
|
||||
* S.el(listS, list=> list.map(li=> el("li", li)));
|
||||
* ```
|
||||
* */
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
|
||||
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
|
||||
}
|
||||
export const signal: signal;
|
||||
@ -80,10 +81,11 @@ export type CustomElementTagNameMap = {
|
||||
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
|
||||
declare global {
|
||||
type ddeComponentAttributes = Record<any, any> | undefined;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
|
||||
type ddeString = string | Signal<string, {}>;
|
||||
type ddeStringable = ddeString | number | Signal<number, {}>;
|
||||
}
|
||||
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
|
||||
export type PascalCase = `${Capitalize<string>}${string}`;
|
||||
export type AttrsModified = {
|
||||
/**
|
||||
@ -145,6 +147,7 @@ export namespace el {
|
||||
host?: "this" | "parentElement";
|
||||
}, is_open?: boolean): Comment;
|
||||
}
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
|
||||
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
|
||||
export function el<A extends {
|
||||
textContent: ddeStringable;
|
||||
@ -157,7 +160,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
|
||||
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
|
||||
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
|
||||
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
|
||||
/** Simulate slots for ddeComponents */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
|
||||
/**
|
||||
@ -166,9 +168,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
|
||||
* @param body Body of the custom element
|
||||
* */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
@ -186,7 +188,7 @@ export interface On {
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
scope: Node | Function | Object;
|
||||
host: ddeElementAddon<any>;
|
||||
host: Host<SupportedElement>;
|
||||
custom_element: false | HTMLElement;
|
||||
prevent: boolean;
|
||||
};
|
||||
@ -202,7 +204,11 @@ export const scope: {
|
||||
* It can be also used to register Addon(s) (functions to be called when component is initized)
|
||||
* — `scope.host(on.connected(console.log))`.
|
||||
* */
|
||||
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
|
||||
host: Host<SupportedElement>;
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
signal: AbortSignal;
|
||||
state: Scope[];
|
||||
/** Adds new child scope. All attributes are inherited by default. */
|
||||
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
|
||||
@ -214,7 +220,6 @@ export const scope: {
|
||||
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
|
||||
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
|
||||
/**
|
||||
* This is used primarly for server side rendering. To be sure that all async operations
|
||||
* are finished before the page is sent to the client.
|
||||
@ -235,6 +240,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
|
||||
* ```
|
||||
* */
|
||||
export function queue(promise?: Promise<unknown>): Promise<unknown>;
|
||||
/**
|
||||
* Memoization utility for caching DOM elements to improve performance.
|
||||
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
|
||||
*
|
||||
* @param key - Unique identifier for the element (usually an ID or unique value)
|
||||
* @param generator - Function that creates the element
|
||||
* @returns The cached element if the key exists, otherwise the result of the generator function
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Within S.el for list rendering
|
||||
* S.el(itemsSignal, (items, memo) =>
|
||||
* el("ul").append(
|
||||
* ...items.map(item =>
|
||||
* memo(item.id, () => el(ItemComponent, item))
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
|
||||
/**
|
||||
* Memo namespace containing utility functions for memoization.
|
||||
*/
|
||||
export namespace memo {
|
||||
/**
|
||||
* Checks if an object is a memo scope.
|
||||
* @param obj - The object to check
|
||||
* @returns True if the object is a memo scope
|
||||
*/
|
||||
export function isScope(obj: any): boolean;
|
||||
/**
|
||||
* Creates a memoized function with optional cleanup support.
|
||||
*
|
||||
* @param fun - The function to memoize
|
||||
* @param options - Configuration options
|
||||
* @param options.signal - AbortSignal for cleanup
|
||||
* @param options.onlyLast - When true, only keeps the cache from the most recent call
|
||||
* @returns A memoized version of the function with a .clear() method
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const renderItems = memo.scope(function(items) {
|
||||
* return items.map(item =>
|
||||
* memo(item.id, () => el("div", item.name))
|
||||
* );
|
||||
* }, {
|
||||
* signal: controller.signal,
|
||||
* onlyLast: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function scope<F extends Function>(fun: F, options?: {
|
||||
signal?: AbortSignal;
|
||||
onlyLast?: boolean;
|
||||
}): F & {
|
||||
clear: () => void;
|
||||
};
|
||||
}
|
||||
/* TypeScript MEH */
|
||||
declare global {
|
||||
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;
|
||||
|
8
dist/esm-with-signals.min.js
vendored
8
dist/esm-with-signals.min.js
vendored
File diff suppressed because one or more lines are too long
80
dist/esm.d.ts
vendored
80
dist/esm.d.ts
vendored
@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
|
||||
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
|
||||
first_time?: boolean;
|
||||
};
|
||||
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
|
||||
export interface signal {
|
||||
_: Symbol;
|
||||
/**
|
||||
@ -63,7 +64,7 @@ export interface signal {
|
||||
* S.el(listS, list=> list.map(li=> el("li", li)));
|
||||
* ```
|
||||
* */
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
|
||||
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
|
||||
}
|
||||
declare const signal: signal;
|
||||
@ -79,10 +80,11 @@ export type CustomElementTagNameMap = {
|
||||
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
|
||||
declare global {
|
||||
type ddeComponentAttributes = Record<any, any> | undefined;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
|
||||
type ddeString = string | Signal<string, {}>;
|
||||
type ddeStringable = ddeString | number | Signal<number, {}>;
|
||||
}
|
||||
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
|
||||
export type PascalCase = `${Capitalize<string>}${string}`;
|
||||
export type AttrsModified = {
|
||||
/**
|
||||
@ -144,6 +146,7 @@ export namespace el {
|
||||
host?: "this" | "parentElement";
|
||||
}, is_open?: boolean): Comment;
|
||||
}
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
|
||||
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
|
||||
export function el<A extends {
|
||||
textContent: ddeStringable;
|
||||
@ -156,7 +159,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
|
||||
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
|
||||
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
|
||||
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
|
||||
/** Simulate slots for ddeComponents */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
|
||||
/**
|
||||
@ -165,9 +167,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
|
||||
* @param body Body of the custom element
|
||||
* */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
@ -185,7 +187,7 @@ export interface On {
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
scope: Node | Function | Object;
|
||||
host: ddeElementAddon<any>;
|
||||
host: Host<SupportedElement>;
|
||||
custom_element: false | HTMLElement;
|
||||
prevent: boolean;
|
||||
};
|
||||
@ -201,7 +203,11 @@ export const scope: {
|
||||
* It can be also used to register Addon(s) (functions to be called when component is initized)
|
||||
* — `scope.host(on.connected(console.log))`.
|
||||
* */
|
||||
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
|
||||
host: Host<SupportedElement>;
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
signal: AbortSignal;
|
||||
state: Scope[];
|
||||
/** Adds new child scope. All attributes are inherited by default. */
|
||||
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
|
||||
@ -213,7 +219,6 @@ export const scope: {
|
||||
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
|
||||
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
|
||||
/**
|
||||
* This is used primarly for server side rendering. To be sure that all async operations
|
||||
* are finished before the page is sent to the client.
|
||||
@ -234,6 +239,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
|
||||
* ```
|
||||
* */
|
||||
export function queue(promise?: Promise<unknown>): Promise<unknown>;
|
||||
/**
|
||||
* Memoization utility for caching DOM elements to improve performance.
|
||||
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
|
||||
*
|
||||
* @param key - Unique identifier for the element (usually an ID or unique value)
|
||||
* @param generator - Function that creates the element
|
||||
* @returns The cached element if the key exists, otherwise the result of the generator function
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Within S.el for list rendering
|
||||
* S.el(itemsSignal, (items, memo) =>
|
||||
* el("ul").append(
|
||||
* ...items.map(item =>
|
||||
* memo(item.id, () => el(ItemComponent, item))
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
|
||||
/**
|
||||
* Memo namespace containing utility functions for memoization.
|
||||
*/
|
||||
export namespace memo {
|
||||
/**
|
||||
* Checks if an object is a memo scope.
|
||||
* @param obj - The object to check
|
||||
* @returns True if the object is a memo scope
|
||||
*/
|
||||
export function isScope(obj: any): boolean;
|
||||
/**
|
||||
* Creates a memoized function with optional cleanup support.
|
||||
*
|
||||
* @param fun - The function to memoize
|
||||
* @param options - Configuration options
|
||||
* @param options.signal - AbortSignal for cleanup
|
||||
* @param options.onlyLast - When true, only keeps the cache from the most recent call
|
||||
* @returns A memoized version of the function with a .clear() method
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const renderItems = memo.scope(function(items) {
|
||||
* return items.map(item =>
|
||||
* memo(item.id, () => el("div", item.name))
|
||||
* );
|
||||
* }, {
|
||||
* signal: controller.signal,
|
||||
* onlyLast: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function scope<F extends Function>(fun: F, options?: {
|
||||
signal?: AbortSignal;
|
||||
onlyLast?: boolean;
|
||||
}): F & {
|
||||
clear: () => void;
|
||||
};
|
||||
}
|
||||
/* TypeScript MEH */
|
||||
declare global {
|
||||
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;
|
||||
|
505
dist/esm.js
vendored
505
dist/esm.js
vendored
@ -1,4 +1,5 @@
|
||||
// src/helpers.js
|
||||
var hasOwn = (...a) => Object.prototype.hasOwnProperty.call(...a);
|
||||
function isUndef(value) {
|
||||
return typeof value === "undefined";
|
||||
}
|
||||
@ -8,6 +9,9 @@ function isInstance(obj, cls) {
|
||||
function isProtoFrom(obj, cls) {
|
||||
return Object.prototype.isPrototypeOf.call(cls, obj);
|
||||
}
|
||||
function oCreate(proto = null, p = {}) {
|
||||
return Object.create(proto, p);
|
||||
}
|
||||
function oAssign(...o) {
|
||||
return Object.assign(...o);
|
||||
}
|
||||
@ -21,16 +25,6 @@ function onAbort(signal, listener) {
|
||||
signal.removeEventListener("abort", listener);
|
||||
};
|
||||
}
|
||||
function observedAttributes(instance, observedAttribute) {
|
||||
const { observedAttributes: observedAttributes3 = [] } = instance.constructor;
|
||||
return observedAttributes3.reduce(function(out, name) {
|
||||
out[kebabToCamel(name)] = observedAttribute(instance, name);
|
||||
return out;
|
||||
}, {});
|
||||
}
|
||||
function kebabToCamel(name) {
|
||||
return name.replace(/-./g, (x) => x[1].toUpperCase());
|
||||
}
|
||||
|
||||
// src/signals-lib/common.js
|
||||
var signals_global = {
|
||||
@ -68,6 +62,7 @@ var enviroment = {
|
||||
setDeleteAttr,
|
||||
ssr: "",
|
||||
D: globalThis.document,
|
||||
N: globalThis.Node,
|
||||
F: globalThis.DocumentFragment,
|
||||
H: globalThis.HTMLElement,
|
||||
S: globalThis.SVGElement,
|
||||
@ -88,6 +83,215 @@ var evc = "dde:connected";
|
||||
var evd = "dde:disconnected";
|
||||
var eva = "dde:attributeChanged";
|
||||
|
||||
// src/events-observer.js
|
||||
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
|
||||
get() {
|
||||
return () => {
|
||||
};
|
||||
}
|
||||
});
|
||||
function connectionsChangesObserverConstructor() {
|
||||
const store = /* @__PURE__ */ new Map();
|
||||
let is_observing = false;
|
||||
const observerListener = (stop2) => function(mutations) {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "childList") continue;
|
||||
if (observerAdded(mutation.addedNodes, true)) {
|
||||
stop2();
|
||||
continue;
|
||||
}
|
||||
if (observerRemoved(mutation.removedNodes, true))
|
||||
stop2();
|
||||
}
|
||||
};
|
||||
const observer = new enviroment.M(observerListener(stop));
|
||||
return {
|
||||
/**
|
||||
* Creates an observer for a specific element
|
||||
* @param {Element} element - Element to observe
|
||||
* @returns {Function} Cleanup function
|
||||
*/
|
||||
observe(element) {
|
||||
const o = new enviroment.M(observerListener(() => {
|
||||
}));
|
||||
o.observe(element, { childList: true, subtree: true });
|
||||
return () => o.disconnect();
|
||||
},
|
||||
/**
|
||||
* Register a connection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for connection event
|
||||
*/
|
||||
onConnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.connected.has(listener)) return;
|
||||
listeners.connected.add(listener);
|
||||
listeners.length_c += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a connection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offConnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
if (!ls.connected.has(listener)) return;
|
||||
ls.connected.delete(listener);
|
||||
ls.length_c -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
},
|
||||
/**
|
||||
* Register a disconnection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for disconnection event
|
||||
*/
|
||||
onDisconnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.disconnected.has(listener)) return;
|
||||
listeners.disconnected.add(listener);
|
||||
listeners.length_d += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a disconnection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offDisconnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
ls.disconnected.delete(listener);
|
||||
ls.length_d -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
}
|
||||
};
|
||||
function cleanWhenOff(element, ls) {
|
||||
if (ls.length_c || ls.length_d)
|
||||
return;
|
||||
store.delete(element);
|
||||
stop();
|
||||
}
|
||||
function getElementStore(element) {
|
||||
if (store.has(element)) return store.get(element);
|
||||
const out = {
|
||||
connected: /* @__PURE__ */ new WeakSet(),
|
||||
length_c: 0,
|
||||
disconnected: /* @__PURE__ */ new WeakSet(),
|
||||
length_d: 0
|
||||
};
|
||||
store.set(element, out);
|
||||
return out;
|
||||
}
|
||||
function start() {
|
||||
if (is_observing) return;
|
||||
is_observing = true;
|
||||
observer.observe(enviroment.D.body, { childList: true, subtree: true });
|
||||
}
|
||||
function stop() {
|
||||
if (!is_observing || store.size) return;
|
||||
is_observing = false;
|
||||
observer.disconnect();
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
async function collectChildren(element) {
|
||||
if (store.size > 30)
|
||||
await requestIdle();
|
||||
const out = [];
|
||||
if (!isInstance(element, enviroment.N)) return out;
|
||||
for (const el of store.keys()) {
|
||||
if (el === element || !isInstance(el, enviroment.N)) continue;
|
||||
if (element.contains(el))
|
||||
out.push(el);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerAdded(addedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of addedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerAdded);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_c) continue;
|
||||
element.dispatchEvent(new Event(evc));
|
||||
ls.connected = /* @__PURE__ */ new WeakSet();
|
||||
ls.length_c = 0;
|
||||
if (!ls.length_d) store.delete(element);
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerRemoved(removedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of removedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerRemoved);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_d) continue;
|
||||
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function dispatchRemove(element) {
|
||||
return () => {
|
||||
if (element.isConnected) return;
|
||||
element.dispatchEvent(new Event(evd));
|
||||
store.delete(element);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
options = null;
|
||||
}
|
||||
if (!options) options = {};
|
||||
return function dispatch(element, ...d) {
|
||||
if (host) {
|
||||
d.unshift(element);
|
||||
element = typeof host === "function" ? host() : host;
|
||||
}
|
||||
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
|
||||
return element.dispatchEvent(event);
|
||||
};
|
||||
}
|
||||
function on(event, listener, options) {
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(event, listener, options);
|
||||
return element;
|
||||
};
|
||||
}
|
||||
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
|
||||
on.connected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evc, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
|
||||
if (c) c_ch_o.onConnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
};
|
||||
on.disconnected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evd, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
|
||||
if (c) c_ch_o.onDisconnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
};
|
||||
|
||||
// src/dom.js
|
||||
function queue(promise) {
|
||||
return enviroment.q(promise);
|
||||
@ -99,6 +303,7 @@ var scopes = [{
|
||||
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
|
||||
prevent: true
|
||||
}];
|
||||
var store_abort = /* @__PURE__ */ new WeakMap();
|
||||
var scope = {
|
||||
/**
|
||||
* Gets the current scope
|
||||
@ -115,6 +320,17 @@ var scope = {
|
||||
return this.current.host;
|
||||
},
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
get signal() {
|
||||
const { host } = this;
|
||||
if (store_abort.has(host)) return store_abort.get(host);
|
||||
const a = new AbortController();
|
||||
store_abort.set(host, a);
|
||||
host(on.disconnected(() => a.abort()));
|
||||
return a.signal;
|
||||
},
|
||||
/**
|
||||
* Prevents default behavior in the current scope
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
@ -357,172 +573,8 @@ function setDelete(obj, key, val) {
|
||||
return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
|
||||
// src/events-observer.js
|
||||
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
|
||||
get() {
|
||||
return () => {
|
||||
};
|
||||
}
|
||||
});
|
||||
function connectionsChangesObserverConstructor() {
|
||||
const store = /* @__PURE__ */ new Map();
|
||||
let is_observing = false;
|
||||
const observerListener = (stop2) => function(mutations) {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "childList") continue;
|
||||
if (observerAdded(mutation.addedNodes, true)) {
|
||||
stop2();
|
||||
continue;
|
||||
}
|
||||
if (observerRemoved(mutation.removedNodes, true))
|
||||
stop2();
|
||||
}
|
||||
};
|
||||
const observer = new enviroment.M(observerListener(stop));
|
||||
return {
|
||||
/**
|
||||
* Creates an observer for a specific element
|
||||
* @param {Element} element - Element to observe
|
||||
* @returns {Function} Cleanup function
|
||||
*/
|
||||
observe(element) {
|
||||
const o = new enviroment.M(observerListener(() => {
|
||||
}));
|
||||
o.observe(element, { childList: true, subtree: true });
|
||||
return () => o.disconnect();
|
||||
},
|
||||
/**
|
||||
* Register a connection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for connection event
|
||||
*/
|
||||
onConnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.connected.has(listener)) return;
|
||||
listeners.connected.add(listener);
|
||||
listeners.length_c += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a connection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offConnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
if (!ls.connected.has(listener)) return;
|
||||
ls.connected.delete(listener);
|
||||
ls.length_c -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
},
|
||||
/**
|
||||
* Register a disconnection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for disconnection event
|
||||
*/
|
||||
onDisconnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.disconnected.has(listener)) return;
|
||||
listeners.disconnected.add(listener);
|
||||
listeners.length_d += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a disconnection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offDisconnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
ls.disconnected.delete(listener);
|
||||
ls.length_d -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
}
|
||||
};
|
||||
function cleanWhenOff(element, ls) {
|
||||
if (ls.length_c || ls.length_d)
|
||||
return;
|
||||
store.delete(element);
|
||||
stop();
|
||||
}
|
||||
function getElementStore(element) {
|
||||
if (store.has(element)) return store.get(element);
|
||||
const out = {
|
||||
connected: /* @__PURE__ */ new WeakSet(),
|
||||
length_c: 0,
|
||||
disconnected: /* @__PURE__ */ new WeakSet(),
|
||||
length_d: 0
|
||||
};
|
||||
store.set(element, out);
|
||||
return out;
|
||||
}
|
||||
function start() {
|
||||
if (is_observing) return;
|
||||
is_observing = true;
|
||||
observer.observe(enviroment.D.body, { childList: true, subtree: true });
|
||||
}
|
||||
function stop() {
|
||||
if (!is_observing || store.size) return;
|
||||
is_observing = false;
|
||||
observer.disconnect();
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
async function collectChildren(element) {
|
||||
if (store.size > 30)
|
||||
await requestIdle();
|
||||
const out = [];
|
||||
if (!isInstance(element, Node)) return out;
|
||||
for (const el of store.keys()) {
|
||||
if (el === element || !isInstance(el, Node)) continue;
|
||||
if (element.contains(el))
|
||||
out.push(el);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerAdded(addedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of addedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerAdded);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_c) continue;
|
||||
element.dispatchEvent(new Event(evc));
|
||||
ls.connected = /* @__PURE__ */ new WeakSet();
|
||||
ls.length_c = 0;
|
||||
if (!ls.length_d) store.delete(element);
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerRemoved(removedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of removedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerRemoved);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_d) continue;
|
||||
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function dispatchRemove(element) {
|
||||
return () => {
|
||||
if (element.isConnected) return;
|
||||
element.dispatchEvent(new Event(evd));
|
||||
store.delete(element);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// src/customElement.js
|
||||
function customElementRender(target, render, props = observedAttributes2) {
|
||||
function customElementRender(target, render, props = {}) {
|
||||
const custom_element = target.host || target;
|
||||
scope.push({
|
||||
scope: custom_element,
|
||||
@ -563,81 +615,40 @@ function wrapMethod(obj, method, apply) {
|
||||
obj[method] = new Proxy(obj[method] || (() => {
|
||||
}), { apply });
|
||||
}
|
||||
function observedAttributes2(instance) {
|
||||
return observedAttributes(instance, (i, n) => i.getAttribute(n));
|
||||
}
|
||||
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
options = null;
|
||||
}
|
||||
if (!options) options = {};
|
||||
return function dispatch(element, ...d) {
|
||||
if (host) {
|
||||
d.unshift(element);
|
||||
element = typeof host === "function" ? host() : host;
|
||||
}
|
||||
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
|
||||
return element.dispatchEvent(event);
|
||||
};
|
||||
// src/memo.js
|
||||
var memoMark = "__dde_memo";
|
||||
var memo_scope = [];
|
||||
function memo(key, generator) {
|
||||
if (!memo_scope.length) return generator(key);
|
||||
const k = typeof key === "object" ? JSON.stringify(key) : key;
|
||||
const [{ cache, after }] = memo_scope;
|
||||
return after(k, hasOwn(cache, k) ? cache[k] : generator(key));
|
||||
}
|
||||
function on(event, listener, options) {
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(event, listener, options);
|
||||
return element;
|
||||
};
|
||||
}
|
||||
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
|
||||
on.connected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evc, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
|
||||
if (c) c_ch_o.onConnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
memo.isScope = function(obj) {
|
||||
return obj[memoMark];
|
||||
};
|
||||
on.disconnected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evd, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
|
||||
if (c) c_ch_o.onDisconnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
};
|
||||
var store_abort = /* @__PURE__ */ new WeakMap();
|
||||
on.disconnectedAsAbort = function(host) {
|
||||
if (store_abort.has(host)) return store_abort.get(host);
|
||||
const a = new AbortController();
|
||||
store_abort.set(host, a);
|
||||
host(on.disconnected(() => a.abort()));
|
||||
return a.signal;
|
||||
};
|
||||
var els_attribute_store = /* @__PURE__ */ new WeakSet();
|
||||
on.attributeChanged = function(listener, options) {
|
||||
if (typeof options !== "object")
|
||||
options = {};
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(eva, listener, options);
|
||||
if (element[keyLTE] || els_attribute_store.has(element))
|
||||
return element;
|
||||
if (!enviroment.M) return element;
|
||||
const observer = new enviroment.M(function(mutations) {
|
||||
for (const { attributeName, target } of mutations)
|
||||
target.dispatchEvent(
|
||||
new CustomEvent(eva, { detail: [attributeName, target.getAttribute(attributeName)] })
|
||||
);
|
||||
memo.scope = function memoScope(fun, { signal, onlyLast } = {}) {
|
||||
let cache = oCreate();
|
||||
function memoScope2(...args) {
|
||||
if (signal && signal.aborted)
|
||||
return fun.apply(this, args);
|
||||
let cache_local = onlyLast ? cache : oCreate();
|
||||
memo_scope.unshift({
|
||||
cache,
|
||||
after(key, val) {
|
||||
return cache_local[key] = val;
|
||||
}
|
||||
});
|
||||
const c = onAbort(options.signal, () => observer.disconnect());
|
||||
if (c) observer.observe(element, { attributes: true });
|
||||
return element;
|
||||
};
|
||||
const out = fun.apply(this, args);
|
||||
memo_scope.shift();
|
||||
cache = cache_local;
|
||||
return out;
|
||||
}
|
||||
memoScope2[memoMark] = true;
|
||||
memoScope2.clear = () => cache = oCreate();
|
||||
if (signal) signal.addEventListener("abort", memoScope2.clear);
|
||||
return memoScope2;
|
||||
};
|
||||
export {
|
||||
assign,
|
||||
@ -653,7 +664,7 @@ export {
|
||||
createElementNS as elNS,
|
||||
elementAttribute,
|
||||
lifecyclesToEvents,
|
||||
observedAttributes2 as observedAttributes,
|
||||
memo,
|
||||
on,
|
||||
queue,
|
||||
registerReactivity,
|
||||
|
80
dist/esm.min.d.ts
vendored
80
dist/esm.min.d.ts
vendored
@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
|
||||
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
|
||||
first_time?: boolean;
|
||||
};
|
||||
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
|
||||
export interface signal {
|
||||
_: Symbol;
|
||||
/**
|
||||
@ -63,7 +64,7 @@ export interface signal {
|
||||
* S.el(listS, list=> list.map(li=> el("li", li)));
|
||||
* ```
|
||||
* */
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
|
||||
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
|
||||
}
|
||||
declare const signal: signal;
|
||||
@ -79,10 +80,11 @@ export type CustomElementTagNameMap = {
|
||||
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
|
||||
declare global {
|
||||
type ddeComponentAttributes = Record<any, any> | undefined;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
|
||||
type ddeString = string | Signal<string, {}>;
|
||||
type ddeStringable = ddeString | number | Signal<number, {}>;
|
||||
}
|
||||
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
|
||||
export type PascalCase = `${Capitalize<string>}${string}`;
|
||||
export type AttrsModified = {
|
||||
/**
|
||||
@ -144,6 +146,7 @@ export namespace el {
|
||||
host?: "this" | "parentElement";
|
||||
}, is_open?: boolean): Comment;
|
||||
}
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
|
||||
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
|
||||
export function el<A extends {
|
||||
textContent: ddeStringable;
|
||||
@ -156,7 +159,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
|
||||
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
|
||||
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
|
||||
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
|
||||
/** Simulate slots for ddeComponents */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
|
||||
/**
|
||||
@ -165,9 +167,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
|
||||
* @param body Body of the custom element
|
||||
* */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
@ -185,7 +187,7 @@ export interface On {
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
scope: Node | Function | Object;
|
||||
host: ddeElementAddon<any>;
|
||||
host: Host<SupportedElement>;
|
||||
custom_element: false | HTMLElement;
|
||||
prevent: boolean;
|
||||
};
|
||||
@ -201,7 +203,11 @@ export const scope: {
|
||||
* It can be also used to register Addon(s) (functions to be called when component is initized)
|
||||
* — `scope.host(on.connected(console.log))`.
|
||||
* */
|
||||
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
|
||||
host: Host<SupportedElement>;
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
signal: AbortSignal;
|
||||
state: Scope[];
|
||||
/** Adds new child scope. All attributes are inherited by default. */
|
||||
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
|
||||
@ -213,7 +219,6 @@ export const scope: {
|
||||
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
|
||||
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
|
||||
/**
|
||||
* This is used primarly for server side rendering. To be sure that all async operations
|
||||
* are finished before the page is sent to the client.
|
||||
@ -234,6 +239,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
|
||||
* ```
|
||||
* */
|
||||
export function queue(promise?: Promise<unknown>): Promise<unknown>;
|
||||
/**
|
||||
* Memoization utility for caching DOM elements to improve performance.
|
||||
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
|
||||
*
|
||||
* @param key - Unique identifier for the element (usually an ID or unique value)
|
||||
* @param generator - Function that creates the element
|
||||
* @returns The cached element if the key exists, otherwise the result of the generator function
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Within S.el for list rendering
|
||||
* S.el(itemsSignal, (items, memo) =>
|
||||
* el("ul").append(
|
||||
* ...items.map(item =>
|
||||
* memo(item.id, () => el(ItemComponent, item))
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
|
||||
/**
|
||||
* Memo namespace containing utility functions for memoization.
|
||||
*/
|
||||
export namespace memo {
|
||||
/**
|
||||
* Checks if an object is a memo scope.
|
||||
* @param obj - The object to check
|
||||
* @returns True if the object is a memo scope
|
||||
*/
|
||||
export function isScope(obj: any): boolean;
|
||||
/**
|
||||
* Creates a memoized function with optional cleanup support.
|
||||
*
|
||||
* @param fun - The function to memoize
|
||||
* @param options - Configuration options
|
||||
* @param options.signal - AbortSignal for cleanup
|
||||
* @param options.onlyLast - When true, only keeps the cache from the most recent call
|
||||
* @returns A memoized version of the function with a .clear() method
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const renderItems = memo.scope(function(items) {
|
||||
* return items.map(item =>
|
||||
* memo(item.id, () => el("div", item.name))
|
||||
* );
|
||||
* }, {
|
||||
* signal: controller.signal,
|
||||
* onlyLast: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function scope<F extends Function>(fun: F, options?: {
|
||||
signal?: AbortSignal;
|
||||
onlyLast?: boolean;
|
||||
}): F & {
|
||||
clear: () => void;
|
||||
};
|
||||
}
|
||||
/* TypeScript MEH */
|
||||
declare global {
|
||||
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;
|
||||
|
2
dist/esm.min.js
vendored
2
dist/esm.min.js
vendored
File diff suppressed because one or more lines are too long
80
dist/iife-with-signals.d.ts
vendored
80
dist/iife-with-signals.d.ts
vendored
@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
|
||||
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
|
||||
first_time?: boolean;
|
||||
};
|
||||
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
|
||||
export interface signal {
|
||||
_: Symbol;
|
||||
/**
|
||||
@ -63,7 +64,7 @@ export interface signal {
|
||||
* S.el(listS, list=> list.map(li=> el("li", li)));
|
||||
* ```
|
||||
* */
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
|
||||
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
|
||||
}
|
||||
export const signal: signal;
|
||||
@ -80,10 +81,11 @@ export type CustomElementTagNameMap = {
|
||||
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
|
||||
declare global {
|
||||
type ddeComponentAttributes = Record<any, any> | undefined;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
|
||||
type ddeString = string | Signal<string, {}>;
|
||||
type ddeStringable = ddeString | number | Signal<number, {}>;
|
||||
}
|
||||
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
|
||||
export type PascalCase = `${Capitalize<string>}${string}`;
|
||||
export type AttrsModified = {
|
||||
/**
|
||||
@ -145,6 +147,7 @@ export namespace el {
|
||||
host?: "this" | "parentElement";
|
||||
}, is_open?: boolean): Comment;
|
||||
}
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
|
||||
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
|
||||
export function el<A extends {
|
||||
textContent: ddeStringable;
|
||||
@ -157,7 +160,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
|
||||
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
|
||||
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
|
||||
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
|
||||
/** Simulate slots for ddeComponents */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
|
||||
/**
|
||||
@ -166,9 +168,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
|
||||
* @param body Body of the custom element
|
||||
* */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
@ -186,7 +188,7 @@ export interface On {
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
scope: Node | Function | Object;
|
||||
host: ddeElementAddon<any>;
|
||||
host: Host<SupportedElement>;
|
||||
custom_element: false | HTMLElement;
|
||||
prevent: boolean;
|
||||
};
|
||||
@ -202,7 +204,11 @@ export const scope: {
|
||||
* It can be also used to register Addon(s) (functions to be called when component is initized)
|
||||
* — `scope.host(on.connected(console.log))`.
|
||||
* */
|
||||
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
|
||||
host: Host<SupportedElement>;
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
signal: AbortSignal;
|
||||
state: Scope[];
|
||||
/** Adds new child scope. All attributes are inherited by default. */
|
||||
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
|
||||
@ -214,7 +220,6 @@ export const scope: {
|
||||
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
|
||||
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
|
||||
/**
|
||||
* This is used primarly for server side rendering. To be sure that all async operations
|
||||
* are finished before the page is sent to the client.
|
||||
@ -235,6 +240,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
|
||||
* ```
|
||||
* */
|
||||
export function queue(promise?: Promise<unknown>): Promise<unknown>;
|
||||
/**
|
||||
* Memoization utility for caching DOM elements to improve performance.
|
||||
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
|
||||
*
|
||||
* @param key - Unique identifier for the element (usually an ID or unique value)
|
||||
* @param generator - Function that creates the element
|
||||
* @returns The cached element if the key exists, otherwise the result of the generator function
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Within S.el for list rendering
|
||||
* S.el(itemsSignal, (items, memo) =>
|
||||
* el("ul").append(
|
||||
* ...items.map(item =>
|
||||
* memo(item.id, () => el(ItemComponent, item))
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
|
||||
/**
|
||||
* Memo namespace containing utility functions for memoization.
|
||||
*/
|
||||
export namespace memo {
|
||||
/**
|
||||
* Checks if an object is a memo scope.
|
||||
* @param obj - The object to check
|
||||
* @returns True if the object is a memo scope
|
||||
*/
|
||||
export function isScope(obj: any): boolean;
|
||||
/**
|
||||
* Creates a memoized function with optional cleanup support.
|
||||
*
|
||||
* @param fun - The function to memoize
|
||||
* @param options - Configuration options
|
||||
* @param options.signal - AbortSignal for cleanup
|
||||
* @param options.onlyLast - When true, only keeps the cache from the most recent call
|
||||
* @returns A memoized version of the function with a .clear() method
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const renderItems = memo.scope(function(items) {
|
||||
* return items.map(item =>
|
||||
* memo(item.id, () => el("div", item.name))
|
||||
* );
|
||||
* }, {
|
||||
* signal: controller.signal,
|
||||
* onlyLast: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function scope<F extends Function>(fun: F, options?: {
|
||||
signal?: AbortSignal;
|
||||
onlyLast?: boolean;
|
||||
}): F & {
|
||||
clear: () => void;
|
||||
};
|
||||
}
|
||||
/* TypeScript MEH */
|
||||
declare global {
|
||||
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;
|
||||
|
510
dist/iife-with-signals.js
vendored
510
dist/iife-with-signals.js
vendored
@ -35,7 +35,7 @@ var DDE = (() => {
|
||||
elementAttribute: () => elementAttribute,
|
||||
isSignal: () => isSignal,
|
||||
lifecyclesToEvents: () => lifecyclesToEvents,
|
||||
observedAttributes: () => observedAttributes2,
|
||||
memo: () => memo,
|
||||
on: () => on,
|
||||
queue: () => queue,
|
||||
registerReactivity: () => registerReactivity,
|
||||
@ -78,8 +78,8 @@ var DDE = (() => {
|
||||
};
|
||||
}
|
||||
function observedAttributes(instance, observedAttribute2) {
|
||||
const { observedAttributes: observedAttributes3 = [] } = instance.constructor;
|
||||
return observedAttributes3.reduce(function(out, name) {
|
||||
const { observedAttributes: observedAttributes2 = [] } = instance.constructor;
|
||||
return observedAttributes2.reduce(function(out, name) {
|
||||
out[kebabToCamel(name)] = observedAttribute2(instance, name);
|
||||
return out;
|
||||
}, {});
|
||||
@ -137,6 +137,7 @@ var DDE = (() => {
|
||||
setDeleteAttr,
|
||||
ssr: "",
|
||||
D: globalThis.document,
|
||||
N: globalThis.Node,
|
||||
F: globalThis.DocumentFragment,
|
||||
H: globalThis.HTMLElement,
|
||||
S: globalThis.SVGElement,
|
||||
@ -157,6 +158,215 @@ var DDE = (() => {
|
||||
var evd = "dde:disconnected";
|
||||
var eva = "dde:attributeChanged";
|
||||
|
||||
// src/events-observer.js
|
||||
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
|
||||
get() {
|
||||
return () => {
|
||||
};
|
||||
}
|
||||
});
|
||||
function connectionsChangesObserverConstructor() {
|
||||
const store = /* @__PURE__ */ new Map();
|
||||
let is_observing = false;
|
||||
const observerListener = (stop2) => function(mutations) {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "childList") continue;
|
||||
if (observerAdded(mutation.addedNodes, true)) {
|
||||
stop2();
|
||||
continue;
|
||||
}
|
||||
if (observerRemoved(mutation.removedNodes, true))
|
||||
stop2();
|
||||
}
|
||||
};
|
||||
const observer = new enviroment.M(observerListener(stop));
|
||||
return {
|
||||
/**
|
||||
* Creates an observer for a specific element
|
||||
* @param {Element} element - Element to observe
|
||||
* @returns {Function} Cleanup function
|
||||
*/
|
||||
observe(element) {
|
||||
const o = new enviroment.M(observerListener(() => {
|
||||
}));
|
||||
o.observe(element, { childList: true, subtree: true });
|
||||
return () => o.disconnect();
|
||||
},
|
||||
/**
|
||||
* Register a connection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for connection event
|
||||
*/
|
||||
onConnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.connected.has(listener)) return;
|
||||
listeners.connected.add(listener);
|
||||
listeners.length_c += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a connection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offConnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
if (!ls.connected.has(listener)) return;
|
||||
ls.connected.delete(listener);
|
||||
ls.length_c -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
},
|
||||
/**
|
||||
* Register a disconnection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for disconnection event
|
||||
*/
|
||||
onDisconnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.disconnected.has(listener)) return;
|
||||
listeners.disconnected.add(listener);
|
||||
listeners.length_d += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a disconnection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offDisconnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
ls.disconnected.delete(listener);
|
||||
ls.length_d -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
}
|
||||
};
|
||||
function cleanWhenOff(element, ls) {
|
||||
if (ls.length_c || ls.length_d)
|
||||
return;
|
||||
store.delete(element);
|
||||
stop();
|
||||
}
|
||||
function getElementStore(element) {
|
||||
if (store.has(element)) return store.get(element);
|
||||
const out = {
|
||||
connected: /* @__PURE__ */ new WeakSet(),
|
||||
length_c: 0,
|
||||
disconnected: /* @__PURE__ */ new WeakSet(),
|
||||
length_d: 0
|
||||
};
|
||||
store.set(element, out);
|
||||
return out;
|
||||
}
|
||||
function start() {
|
||||
if (is_observing) return;
|
||||
is_observing = true;
|
||||
observer.observe(enviroment.D.body, { childList: true, subtree: true });
|
||||
}
|
||||
function stop() {
|
||||
if (!is_observing || store.size) return;
|
||||
is_observing = false;
|
||||
observer.disconnect();
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
async function collectChildren(element) {
|
||||
if (store.size > 30)
|
||||
await requestIdle();
|
||||
const out = [];
|
||||
if (!isInstance(element, enviroment.N)) return out;
|
||||
for (const el of store.keys()) {
|
||||
if (el === element || !isInstance(el, enviroment.N)) continue;
|
||||
if (element.contains(el))
|
||||
out.push(el);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerAdded(addedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of addedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerAdded);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_c) continue;
|
||||
element.dispatchEvent(new Event(evc));
|
||||
ls.connected = /* @__PURE__ */ new WeakSet();
|
||||
ls.length_c = 0;
|
||||
if (!ls.length_d) store.delete(element);
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerRemoved(removedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of removedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerRemoved);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_d) continue;
|
||||
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function dispatchRemove(element) {
|
||||
return () => {
|
||||
if (element.isConnected) return;
|
||||
element.dispatchEvent(new Event(evd));
|
||||
store.delete(element);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
options = null;
|
||||
}
|
||||
if (!options) options = {};
|
||||
return function dispatch(element, ...d) {
|
||||
if (host) {
|
||||
d.unshift(element);
|
||||
element = typeof host === "function" ? host() : host;
|
||||
}
|
||||
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
|
||||
return element.dispatchEvent(event);
|
||||
};
|
||||
}
|
||||
function on(event, listener, options) {
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(event, listener, options);
|
||||
return element;
|
||||
};
|
||||
}
|
||||
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
|
||||
on.connected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evc, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
|
||||
if (c) c_ch_o.onConnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
};
|
||||
on.disconnected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evd, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
|
||||
if (c) c_ch_o.onDisconnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
};
|
||||
|
||||
// src/dom.js
|
||||
function queue(promise) {
|
||||
return enviroment.q(promise);
|
||||
@ -168,6 +378,7 @@ var DDE = (() => {
|
||||
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
|
||||
prevent: true
|
||||
}];
|
||||
var store_abort = /* @__PURE__ */ new WeakMap();
|
||||
var scope = {
|
||||
/**
|
||||
* Gets the current scope
|
||||
@ -184,6 +395,17 @@ var DDE = (() => {
|
||||
return this.current.host;
|
||||
},
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
get signal() {
|
||||
const { host } = this;
|
||||
if (store_abort.has(host)) return store_abort.get(host);
|
||||
const a = new AbortController();
|
||||
store_abort.set(host, a);
|
||||
host(on.disconnected(() => a.abort()));
|
||||
return a.signal;
|
||||
},
|
||||
/**
|
||||
* Prevents default behavior in the current scope
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
@ -426,172 +648,8 @@ var DDE = (() => {
|
||||
return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
|
||||
// src/events-observer.js
|
||||
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
|
||||
get() {
|
||||
return () => {
|
||||
};
|
||||
}
|
||||
});
|
||||
function connectionsChangesObserverConstructor() {
|
||||
const store = /* @__PURE__ */ new Map();
|
||||
let is_observing = false;
|
||||
const observerListener = (stop2) => function(mutations) {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "childList") continue;
|
||||
if (observerAdded(mutation.addedNodes, true)) {
|
||||
stop2();
|
||||
continue;
|
||||
}
|
||||
if (observerRemoved(mutation.removedNodes, true))
|
||||
stop2();
|
||||
}
|
||||
};
|
||||
const observer = new enviroment.M(observerListener(stop));
|
||||
return {
|
||||
/**
|
||||
* Creates an observer for a specific element
|
||||
* @param {Element} element - Element to observe
|
||||
* @returns {Function} Cleanup function
|
||||
*/
|
||||
observe(element) {
|
||||
const o = new enviroment.M(observerListener(() => {
|
||||
}));
|
||||
o.observe(element, { childList: true, subtree: true });
|
||||
return () => o.disconnect();
|
||||
},
|
||||
/**
|
||||
* Register a connection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for connection event
|
||||
*/
|
||||
onConnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.connected.has(listener)) return;
|
||||
listeners.connected.add(listener);
|
||||
listeners.length_c += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a connection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offConnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
if (!ls.connected.has(listener)) return;
|
||||
ls.connected.delete(listener);
|
||||
ls.length_c -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
},
|
||||
/**
|
||||
* Register a disconnection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for disconnection event
|
||||
*/
|
||||
onDisconnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.disconnected.has(listener)) return;
|
||||
listeners.disconnected.add(listener);
|
||||
listeners.length_d += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a disconnection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offDisconnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
ls.disconnected.delete(listener);
|
||||
ls.length_d -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
}
|
||||
};
|
||||
function cleanWhenOff(element, ls) {
|
||||
if (ls.length_c || ls.length_d)
|
||||
return;
|
||||
store.delete(element);
|
||||
stop();
|
||||
}
|
||||
function getElementStore(element) {
|
||||
if (store.has(element)) return store.get(element);
|
||||
const out = {
|
||||
connected: /* @__PURE__ */ new WeakSet(),
|
||||
length_c: 0,
|
||||
disconnected: /* @__PURE__ */ new WeakSet(),
|
||||
length_d: 0
|
||||
};
|
||||
store.set(element, out);
|
||||
return out;
|
||||
}
|
||||
function start() {
|
||||
if (is_observing) return;
|
||||
is_observing = true;
|
||||
observer.observe(enviroment.D.body, { childList: true, subtree: true });
|
||||
}
|
||||
function stop() {
|
||||
if (!is_observing || store.size) return;
|
||||
is_observing = false;
|
||||
observer.disconnect();
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
async function collectChildren(element) {
|
||||
if (store.size > 30)
|
||||
await requestIdle();
|
||||
const out = [];
|
||||
if (!isInstance(element, Node)) return out;
|
||||
for (const el of store.keys()) {
|
||||
if (el === element || !isInstance(el, Node)) continue;
|
||||
if (element.contains(el))
|
||||
out.push(el);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerAdded(addedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of addedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerAdded);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_c) continue;
|
||||
element.dispatchEvent(new Event(evc));
|
||||
ls.connected = /* @__PURE__ */ new WeakSet();
|
||||
ls.length_c = 0;
|
||||
if (!ls.length_d) store.delete(element);
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerRemoved(removedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of removedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerRemoved);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_d) continue;
|
||||
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function dispatchRemove(element) {
|
||||
return () => {
|
||||
if (element.isConnected) return;
|
||||
element.dispatchEvent(new Event(evd));
|
||||
store.delete(element);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// src/customElement.js
|
||||
function customElementRender(target, render, props = observedAttributes2) {
|
||||
function customElementRender(target, render, props = {}) {
|
||||
const custom_element = target.host || target;
|
||||
scope.push({
|
||||
scope: custom_element,
|
||||
@ -632,81 +690,40 @@ var DDE = (() => {
|
||||
obj[method] = new Proxy(obj[method] || (() => {
|
||||
}), { apply });
|
||||
}
|
||||
function observedAttributes2(instance) {
|
||||
return observedAttributes(instance, (i, n) => i.getAttribute(n));
|
||||
}
|
||||
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
options = null;
|
||||
}
|
||||
if (!options) options = {};
|
||||
return function dispatch(element, ...d) {
|
||||
if (host) {
|
||||
d.unshift(element);
|
||||
element = typeof host === "function" ? host() : host;
|
||||
}
|
||||
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
|
||||
return element.dispatchEvent(event);
|
||||
};
|
||||
// src/memo.js
|
||||
var memoMark = "__dde_memo";
|
||||
var memo_scope = [];
|
||||
function memo(key, generator) {
|
||||
if (!memo_scope.length) return generator(key);
|
||||
const k = typeof key === "object" ? JSON.stringify(key) : key;
|
||||
const [{ cache, after }] = memo_scope;
|
||||
return after(k, hasOwn(cache, k) ? cache[k] : generator(key));
|
||||
}
|
||||
function on(event, listener, options) {
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(event, listener, options);
|
||||
return element;
|
||||
};
|
||||
}
|
||||
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
|
||||
on.connected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evc, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
|
||||
if (c) c_ch_o.onConnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
memo.isScope = function(obj) {
|
||||
return obj[memoMark];
|
||||
};
|
||||
on.disconnected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evd, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
|
||||
if (c) c_ch_o.onDisconnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
};
|
||||
var store_abort = /* @__PURE__ */ new WeakMap();
|
||||
on.disconnectedAsAbort = function(host) {
|
||||
if (store_abort.has(host)) return store_abort.get(host);
|
||||
const a = new AbortController();
|
||||
store_abort.set(host, a);
|
||||
host(on.disconnected(() => a.abort()));
|
||||
return a.signal;
|
||||
};
|
||||
var els_attribute_store = /* @__PURE__ */ new WeakSet();
|
||||
on.attributeChanged = function(listener, options) {
|
||||
if (typeof options !== "object")
|
||||
options = {};
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(eva, listener, options);
|
||||
if (element[keyLTE] || els_attribute_store.has(element))
|
||||
return element;
|
||||
if (!enviroment.M) return element;
|
||||
const observer = new enviroment.M(function(mutations) {
|
||||
for (const { attributeName, target } of mutations)
|
||||
target.dispatchEvent(
|
||||
new CustomEvent(eva, { detail: [attributeName, target.getAttribute(attributeName)] })
|
||||
);
|
||||
memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
|
||||
let cache = oCreate();
|
||||
function memoScope2(...args) {
|
||||
if (signal2 && signal2.aborted)
|
||||
return fun.apply(this, args);
|
||||
let cache_local = onlyLast ? cache : oCreate();
|
||||
memo_scope.unshift({
|
||||
cache,
|
||||
after(key, val) {
|
||||
return cache_local[key] = val;
|
||||
}
|
||||
});
|
||||
const c = onAbort(options.signal, () => observer.disconnect());
|
||||
if (c) observer.observe(element, { attributes: true });
|
||||
return element;
|
||||
};
|
||||
const out = fun.apply(this, args);
|
||||
memo_scope.shift();
|
||||
cache = cache_local;
|
||||
return out;
|
||||
}
|
||||
memoScope2[memoMark] = true;
|
||||
memoScope2.clear = () => cache = oCreate();
|
||||
if (signal2) signal2.addEventListener("abort", memoScope2.clear);
|
||||
return memoScope2;
|
||||
};
|
||||
|
||||
// src/signals-lib/helpers.js
|
||||
@ -822,25 +839,18 @@ var DDE = (() => {
|
||||
}
|
||||
};
|
||||
var key_reactive = "__dde_reactive";
|
||||
function cache(store = oCreate()) {
|
||||
return (key, fun) => hasOwn(store, key) ? store[key] : store[key] = fun();
|
||||
}
|
||||
signal.el = function(s, map) {
|
||||
map = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
|
||||
const mark_start = createElement.mark({ type: "reactive", source: new Defined().compact }, true);
|
||||
const mark_end = mark_start.end;
|
||||
const out = enviroment.D.createDocumentFragment();
|
||||
out.append(mark_start, mark_end);
|
||||
const { current } = scope;
|
||||
let cache_shared = oCreate();
|
||||
const reRenderReactiveElement = (v) => {
|
||||
if (!mark_start.parentNode || !mark_end.parentNode)
|
||||
return removeSignalListener(s, reRenderReactiveElement);
|
||||
const memo = cache(cache_shared);
|
||||
cache_shared = oCreate();
|
||||
scope.push(current);
|
||||
let els = map(v, function useCache(key, fun) {
|
||||
return cache_shared[key] = memo(key, fun);
|
||||
});
|
||||
let els = map(v);
|
||||
scope.pop();
|
||||
if (!Array.isArray(els))
|
||||
els = [els];
|
||||
@ -860,7 +870,7 @@ var DDE = (() => {
|
||||
current.host(on.disconnected(
|
||||
() => (
|
||||
/*! Clears cached elements for reactive element `S.el` */
|
||||
cache_shared = {}
|
||||
map.clear()
|
||||
)
|
||||
));
|
||||
return out;
|
||||
@ -892,7 +902,7 @@ var DDE = (() => {
|
||||
signal.observedAttributes = function(element) {
|
||||
const store = element[key_attributes] = {};
|
||||
const attrs = observedAttributes(element, observedAttribute(store));
|
||||
on.attributeChanged(function attributeChangeToSignal({ detail }) {
|
||||
on(eva, function attributeChangeToSignal({ detail }) {
|
||||
/*! This maps attributes to signals (`S.observedAttributes`).
|
||||
Investigate `__dde_attributes` key of the element. */
|
||||
const [name, value] = detail;
|
||||
|
80
dist/iife-with-signals.min.d.ts
vendored
80
dist/iife-with-signals.min.d.ts
vendored
@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
|
||||
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
|
||||
first_time?: boolean;
|
||||
};
|
||||
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
|
||||
export interface signal {
|
||||
_: Symbol;
|
||||
/**
|
||||
@ -63,7 +64,7 @@ export interface signal {
|
||||
* S.el(listS, list=> list.map(li=> el("li", li)));
|
||||
* ```
|
||||
* */
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
|
||||
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
|
||||
}
|
||||
export const signal: signal;
|
||||
@ -80,10 +81,11 @@ export type CustomElementTagNameMap = {
|
||||
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
|
||||
declare global {
|
||||
type ddeComponentAttributes = Record<any, any> | undefined;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
|
||||
type ddeString = string | Signal<string, {}>;
|
||||
type ddeStringable = ddeString | number | Signal<number, {}>;
|
||||
}
|
||||
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
|
||||
export type PascalCase = `${Capitalize<string>}${string}`;
|
||||
export type AttrsModified = {
|
||||
/**
|
||||
@ -145,6 +147,7 @@ export namespace el {
|
||||
host?: "this" | "parentElement";
|
||||
}, is_open?: boolean): Comment;
|
||||
}
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
|
||||
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
|
||||
export function el<A extends {
|
||||
textContent: ddeStringable;
|
||||
@ -157,7 +160,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
|
||||
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
|
||||
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
|
||||
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
|
||||
/** Simulate slots for ddeComponents */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
|
||||
/**
|
||||
@ -166,9 +168,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
|
||||
* @param body Body of the custom element
|
||||
* */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
@ -186,7 +188,7 @@ export interface On {
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
scope: Node | Function | Object;
|
||||
host: ddeElementAddon<any>;
|
||||
host: Host<SupportedElement>;
|
||||
custom_element: false | HTMLElement;
|
||||
prevent: boolean;
|
||||
};
|
||||
@ -202,7 +204,11 @@ export const scope: {
|
||||
* It can be also used to register Addon(s) (functions to be called when component is initized)
|
||||
* — `scope.host(on.connected(console.log))`.
|
||||
* */
|
||||
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
|
||||
host: Host<SupportedElement>;
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
signal: AbortSignal;
|
||||
state: Scope[];
|
||||
/** Adds new child scope. All attributes are inherited by default. */
|
||||
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
|
||||
@ -214,7 +220,6 @@ export const scope: {
|
||||
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
|
||||
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
|
||||
/**
|
||||
* This is used primarly for server side rendering. To be sure that all async operations
|
||||
* are finished before the page is sent to the client.
|
||||
@ -235,6 +240,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
|
||||
* ```
|
||||
* */
|
||||
export function queue(promise?: Promise<unknown>): Promise<unknown>;
|
||||
/**
|
||||
* Memoization utility for caching DOM elements to improve performance.
|
||||
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
|
||||
*
|
||||
* @param key - Unique identifier for the element (usually an ID or unique value)
|
||||
* @param generator - Function that creates the element
|
||||
* @returns The cached element if the key exists, otherwise the result of the generator function
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Within S.el for list rendering
|
||||
* S.el(itemsSignal, (items, memo) =>
|
||||
* el("ul").append(
|
||||
* ...items.map(item =>
|
||||
* memo(item.id, () => el(ItemComponent, item))
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
|
||||
/**
|
||||
* Memo namespace containing utility functions for memoization.
|
||||
*/
|
||||
export namespace memo {
|
||||
/**
|
||||
* Checks if an object is a memo scope.
|
||||
* @param obj - The object to check
|
||||
* @returns True if the object is a memo scope
|
||||
*/
|
||||
export function isScope(obj: any): boolean;
|
||||
/**
|
||||
* Creates a memoized function with optional cleanup support.
|
||||
*
|
||||
* @param fun - The function to memoize
|
||||
* @param options - Configuration options
|
||||
* @param options.signal - AbortSignal for cleanup
|
||||
* @param options.onlyLast - When true, only keeps the cache from the most recent call
|
||||
* @returns A memoized version of the function with a .clear() method
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const renderItems = memo.scope(function(items) {
|
||||
* return items.map(item =>
|
||||
* memo(item.id, () => el("div", item.name))
|
||||
* );
|
||||
* }, {
|
||||
* signal: controller.signal,
|
||||
* onlyLast: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function scope<F extends Function>(fun: F, options?: {
|
||||
signal?: AbortSignal;
|
||||
onlyLast?: boolean;
|
||||
}): F & {
|
||||
clear: () => void;
|
||||
};
|
||||
}
|
||||
/* TypeScript MEH */
|
||||
declare global {
|
||||
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;
|
||||
|
8
dist/iife-with-signals.min.js
vendored
8
dist/iife-with-signals.min.js
vendored
File diff suppressed because one or more lines are too long
80
dist/iife.d.ts
vendored
80
dist/iife.d.ts
vendored
@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
|
||||
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
|
||||
first_time?: boolean;
|
||||
};
|
||||
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
|
||||
export interface signal {
|
||||
_: Symbol;
|
||||
/**
|
||||
@ -63,7 +64,7 @@ export interface signal {
|
||||
* S.el(listS, list=> list.map(li=> el("li", li)));
|
||||
* ```
|
||||
* */
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
|
||||
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
|
||||
}
|
||||
declare const signal: signal;
|
||||
@ -79,10 +80,11 @@ export type CustomElementTagNameMap = {
|
||||
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
|
||||
declare global {
|
||||
type ddeComponentAttributes = Record<any, any> | undefined;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
|
||||
type ddeString = string | Signal<string, {}>;
|
||||
type ddeStringable = ddeString | number | Signal<number, {}>;
|
||||
}
|
||||
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
|
||||
export type PascalCase = `${Capitalize<string>}${string}`;
|
||||
export type AttrsModified = {
|
||||
/**
|
||||
@ -144,6 +146,7 @@ export namespace el {
|
||||
host?: "this" | "parentElement";
|
||||
}, is_open?: boolean): Comment;
|
||||
}
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
|
||||
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
|
||||
export function el<A extends {
|
||||
textContent: ddeStringable;
|
||||
@ -156,7 +159,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
|
||||
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
|
||||
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
|
||||
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
|
||||
/** Simulate slots for ddeComponents */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
|
||||
/**
|
||||
@ -165,9 +167,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
|
||||
* @param body Body of the custom element
|
||||
* */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
@ -185,7 +187,7 @@ export interface On {
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
scope: Node | Function | Object;
|
||||
host: ddeElementAddon<any>;
|
||||
host: Host<SupportedElement>;
|
||||
custom_element: false | HTMLElement;
|
||||
prevent: boolean;
|
||||
};
|
||||
@ -201,7 +203,11 @@ export const scope: {
|
||||
* It can be also used to register Addon(s) (functions to be called when component is initized)
|
||||
* — `scope.host(on.connected(console.log))`.
|
||||
* */
|
||||
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
|
||||
host: Host<SupportedElement>;
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
signal: AbortSignal;
|
||||
state: Scope[];
|
||||
/** Adds new child scope. All attributes are inherited by default. */
|
||||
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
|
||||
@ -213,7 +219,6 @@ export const scope: {
|
||||
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
|
||||
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
|
||||
/**
|
||||
* This is used primarly for server side rendering. To be sure that all async operations
|
||||
* are finished before the page is sent to the client.
|
||||
@ -234,6 +239,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
|
||||
* ```
|
||||
* */
|
||||
export function queue(promise?: Promise<unknown>): Promise<unknown>;
|
||||
/**
|
||||
* Memoization utility for caching DOM elements to improve performance.
|
||||
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
|
||||
*
|
||||
* @param key - Unique identifier for the element (usually an ID or unique value)
|
||||
* @param generator - Function that creates the element
|
||||
* @returns The cached element if the key exists, otherwise the result of the generator function
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Within S.el for list rendering
|
||||
* S.el(itemsSignal, (items, memo) =>
|
||||
* el("ul").append(
|
||||
* ...items.map(item =>
|
||||
* memo(item.id, () => el(ItemComponent, item))
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
|
||||
/**
|
||||
* Memo namespace containing utility functions for memoization.
|
||||
*/
|
||||
export namespace memo {
|
||||
/**
|
||||
* Checks if an object is a memo scope.
|
||||
* @param obj - The object to check
|
||||
* @returns True if the object is a memo scope
|
||||
*/
|
||||
export function isScope(obj: any): boolean;
|
||||
/**
|
||||
* Creates a memoized function with optional cleanup support.
|
||||
*
|
||||
* @param fun - The function to memoize
|
||||
* @param options - Configuration options
|
||||
* @param options.signal - AbortSignal for cleanup
|
||||
* @param options.onlyLast - When true, only keeps the cache from the most recent call
|
||||
* @returns A memoized version of the function with a .clear() method
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const renderItems = memo.scope(function(items) {
|
||||
* return items.map(item =>
|
||||
* memo(item.id, () => el("div", item.name))
|
||||
* );
|
||||
* }, {
|
||||
* signal: controller.signal,
|
||||
* onlyLast: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function scope<F extends Function>(fun: F, options?: {
|
||||
signal?: AbortSignal;
|
||||
onlyLast?: boolean;
|
||||
}): F & {
|
||||
clear: () => void;
|
||||
};
|
||||
}
|
||||
/* TypeScript MEH */
|
||||
declare global {
|
||||
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;
|
||||
|
505
dist/iife.js
vendored
505
dist/iife.js
vendored
@ -33,7 +33,7 @@ var DDE = (() => {
|
||||
elNS: () => createElementNS,
|
||||
elementAttribute: () => elementAttribute,
|
||||
lifecyclesToEvents: () => lifecyclesToEvents,
|
||||
observedAttributes: () => observedAttributes2,
|
||||
memo: () => memo,
|
||||
on: () => on,
|
||||
queue: () => queue,
|
||||
registerReactivity: () => registerReactivity,
|
||||
@ -42,6 +42,7 @@ var DDE = (() => {
|
||||
});
|
||||
|
||||
// src/helpers.js
|
||||
var hasOwn = (...a) => Object.prototype.hasOwnProperty.call(...a);
|
||||
function isUndef(value) {
|
||||
return typeof value === "undefined";
|
||||
}
|
||||
@ -51,6 +52,9 @@ var DDE = (() => {
|
||||
function isProtoFrom(obj, cls) {
|
||||
return Object.prototype.isPrototypeOf.call(cls, obj);
|
||||
}
|
||||
function oCreate(proto = null, p = {}) {
|
||||
return Object.create(proto, p);
|
||||
}
|
||||
function oAssign(...o) {
|
||||
return Object.assign(...o);
|
||||
}
|
||||
@ -64,16 +68,6 @@ var DDE = (() => {
|
||||
signal.removeEventListener("abort", listener);
|
||||
};
|
||||
}
|
||||
function observedAttributes(instance, observedAttribute) {
|
||||
const { observedAttributes: observedAttributes3 = [] } = instance.constructor;
|
||||
return observedAttributes3.reduce(function(out, name) {
|
||||
out[kebabToCamel(name)] = observedAttribute(instance, name);
|
||||
return out;
|
||||
}, {});
|
||||
}
|
||||
function kebabToCamel(name) {
|
||||
return name.replace(/-./g, (x) => x[1].toUpperCase());
|
||||
}
|
||||
|
||||
// src/signals-lib/common.js
|
||||
var signals_global = {
|
||||
@ -111,6 +105,7 @@ var DDE = (() => {
|
||||
setDeleteAttr,
|
||||
ssr: "",
|
||||
D: globalThis.document,
|
||||
N: globalThis.Node,
|
||||
F: globalThis.DocumentFragment,
|
||||
H: globalThis.HTMLElement,
|
||||
S: globalThis.SVGElement,
|
||||
@ -131,6 +126,215 @@ var DDE = (() => {
|
||||
var evd = "dde:disconnected";
|
||||
var eva = "dde:attributeChanged";
|
||||
|
||||
// src/events-observer.js
|
||||
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
|
||||
get() {
|
||||
return () => {
|
||||
};
|
||||
}
|
||||
});
|
||||
function connectionsChangesObserverConstructor() {
|
||||
const store = /* @__PURE__ */ new Map();
|
||||
let is_observing = false;
|
||||
const observerListener = (stop2) => function(mutations) {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "childList") continue;
|
||||
if (observerAdded(mutation.addedNodes, true)) {
|
||||
stop2();
|
||||
continue;
|
||||
}
|
||||
if (observerRemoved(mutation.removedNodes, true))
|
||||
stop2();
|
||||
}
|
||||
};
|
||||
const observer = new enviroment.M(observerListener(stop));
|
||||
return {
|
||||
/**
|
||||
* Creates an observer for a specific element
|
||||
* @param {Element} element - Element to observe
|
||||
* @returns {Function} Cleanup function
|
||||
*/
|
||||
observe(element) {
|
||||
const o = new enviroment.M(observerListener(() => {
|
||||
}));
|
||||
o.observe(element, { childList: true, subtree: true });
|
||||
return () => o.disconnect();
|
||||
},
|
||||
/**
|
||||
* Register a connection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for connection event
|
||||
*/
|
||||
onConnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.connected.has(listener)) return;
|
||||
listeners.connected.add(listener);
|
||||
listeners.length_c += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a connection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offConnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
if (!ls.connected.has(listener)) return;
|
||||
ls.connected.delete(listener);
|
||||
ls.length_c -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
},
|
||||
/**
|
||||
* Register a disconnection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for disconnection event
|
||||
*/
|
||||
onDisconnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.disconnected.has(listener)) return;
|
||||
listeners.disconnected.add(listener);
|
||||
listeners.length_d += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a disconnection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offDisconnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
ls.disconnected.delete(listener);
|
||||
ls.length_d -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
}
|
||||
};
|
||||
function cleanWhenOff(element, ls) {
|
||||
if (ls.length_c || ls.length_d)
|
||||
return;
|
||||
store.delete(element);
|
||||
stop();
|
||||
}
|
||||
function getElementStore(element) {
|
||||
if (store.has(element)) return store.get(element);
|
||||
const out = {
|
||||
connected: /* @__PURE__ */ new WeakSet(),
|
||||
length_c: 0,
|
||||
disconnected: /* @__PURE__ */ new WeakSet(),
|
||||
length_d: 0
|
||||
};
|
||||
store.set(element, out);
|
||||
return out;
|
||||
}
|
||||
function start() {
|
||||
if (is_observing) return;
|
||||
is_observing = true;
|
||||
observer.observe(enviroment.D.body, { childList: true, subtree: true });
|
||||
}
|
||||
function stop() {
|
||||
if (!is_observing || store.size) return;
|
||||
is_observing = false;
|
||||
observer.disconnect();
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
async function collectChildren(element) {
|
||||
if (store.size > 30)
|
||||
await requestIdle();
|
||||
const out = [];
|
||||
if (!isInstance(element, enviroment.N)) return out;
|
||||
for (const el of store.keys()) {
|
||||
if (el === element || !isInstance(el, enviroment.N)) continue;
|
||||
if (element.contains(el))
|
||||
out.push(el);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerAdded(addedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of addedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerAdded);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_c) continue;
|
||||
element.dispatchEvent(new Event(evc));
|
||||
ls.connected = /* @__PURE__ */ new WeakSet();
|
||||
ls.length_c = 0;
|
||||
if (!ls.length_d) store.delete(element);
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerRemoved(removedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of removedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerRemoved);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_d) continue;
|
||||
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function dispatchRemove(element) {
|
||||
return () => {
|
||||
if (element.isConnected) return;
|
||||
element.dispatchEvent(new Event(evd));
|
||||
store.delete(element);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
options = null;
|
||||
}
|
||||
if (!options) options = {};
|
||||
return function dispatch(element, ...d) {
|
||||
if (host) {
|
||||
d.unshift(element);
|
||||
element = typeof host === "function" ? host() : host;
|
||||
}
|
||||
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
|
||||
return element.dispatchEvent(event);
|
||||
};
|
||||
}
|
||||
function on(event, listener, options) {
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(event, listener, options);
|
||||
return element;
|
||||
};
|
||||
}
|
||||
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
|
||||
on.connected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evc, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
|
||||
if (c) c_ch_o.onConnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
};
|
||||
on.disconnected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evd, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
|
||||
if (c) c_ch_o.onDisconnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
};
|
||||
|
||||
// src/dom.js
|
||||
function queue(promise) {
|
||||
return enviroment.q(promise);
|
||||
@ -142,6 +346,7 @@ var DDE = (() => {
|
||||
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
|
||||
prevent: true
|
||||
}];
|
||||
var store_abort = /* @__PURE__ */ new WeakMap();
|
||||
var scope = {
|
||||
/**
|
||||
* Gets the current scope
|
||||
@ -158,6 +363,17 @@ var DDE = (() => {
|
||||
return this.current.host;
|
||||
},
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
get signal() {
|
||||
const { host } = this;
|
||||
if (store_abort.has(host)) return store_abort.get(host);
|
||||
const a = new AbortController();
|
||||
store_abort.set(host, a);
|
||||
host(on.disconnected(() => a.abort()));
|
||||
return a.signal;
|
||||
},
|
||||
/**
|
||||
* Prevents default behavior in the current scope
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
@ -400,172 +616,8 @@ var DDE = (() => {
|
||||
return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
|
||||
// src/events-observer.js
|
||||
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
|
||||
get() {
|
||||
return () => {
|
||||
};
|
||||
}
|
||||
});
|
||||
function connectionsChangesObserverConstructor() {
|
||||
const store = /* @__PURE__ */ new Map();
|
||||
let is_observing = false;
|
||||
const observerListener = (stop2) => function(mutations) {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "childList") continue;
|
||||
if (observerAdded(mutation.addedNodes, true)) {
|
||||
stop2();
|
||||
continue;
|
||||
}
|
||||
if (observerRemoved(mutation.removedNodes, true))
|
||||
stop2();
|
||||
}
|
||||
};
|
||||
const observer = new enviroment.M(observerListener(stop));
|
||||
return {
|
||||
/**
|
||||
* Creates an observer for a specific element
|
||||
* @param {Element} element - Element to observe
|
||||
* @returns {Function} Cleanup function
|
||||
*/
|
||||
observe(element) {
|
||||
const o = new enviroment.M(observerListener(() => {
|
||||
}));
|
||||
o.observe(element, { childList: true, subtree: true });
|
||||
return () => o.disconnect();
|
||||
},
|
||||
/**
|
||||
* Register a connection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for connection event
|
||||
*/
|
||||
onConnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.connected.has(listener)) return;
|
||||
listeners.connected.add(listener);
|
||||
listeners.length_c += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a connection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offConnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
if (!ls.connected.has(listener)) return;
|
||||
ls.connected.delete(listener);
|
||||
ls.length_c -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
},
|
||||
/**
|
||||
* Register a disconnection listener for an element
|
||||
* @param {Element} element - Element to watch
|
||||
* @param {Function} listener - Callback for disconnection event
|
||||
*/
|
||||
onDisconnected(element, listener) {
|
||||
start();
|
||||
const listeners = getElementStore(element);
|
||||
if (listeners.disconnected.has(listener)) return;
|
||||
listeners.disconnected.add(listener);
|
||||
listeners.length_d += 1;
|
||||
},
|
||||
/**
|
||||
* Unregister a disconnection listener
|
||||
* @param {Element} element - Element being watched
|
||||
* @param {Function} listener - Callback to remove
|
||||
*/
|
||||
offDisconnected(element, listener) {
|
||||
if (!store.has(element)) return;
|
||||
const ls = store.get(element);
|
||||
ls.disconnected.delete(listener);
|
||||
ls.length_d -= 1;
|
||||
cleanWhenOff(element, ls);
|
||||
}
|
||||
};
|
||||
function cleanWhenOff(element, ls) {
|
||||
if (ls.length_c || ls.length_d)
|
||||
return;
|
||||
store.delete(element);
|
||||
stop();
|
||||
}
|
||||
function getElementStore(element) {
|
||||
if (store.has(element)) return store.get(element);
|
||||
const out = {
|
||||
connected: /* @__PURE__ */ new WeakSet(),
|
||||
length_c: 0,
|
||||
disconnected: /* @__PURE__ */ new WeakSet(),
|
||||
length_d: 0
|
||||
};
|
||||
store.set(element, out);
|
||||
return out;
|
||||
}
|
||||
function start() {
|
||||
if (is_observing) return;
|
||||
is_observing = true;
|
||||
observer.observe(enviroment.D.body, { childList: true, subtree: true });
|
||||
}
|
||||
function stop() {
|
||||
if (!is_observing || store.size) return;
|
||||
is_observing = false;
|
||||
observer.disconnect();
|
||||
}
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
async function collectChildren(element) {
|
||||
if (store.size > 30)
|
||||
await requestIdle();
|
||||
const out = [];
|
||||
if (!isInstance(element, Node)) return out;
|
||||
for (const el of store.keys()) {
|
||||
if (el === element || !isInstance(el, Node)) continue;
|
||||
if (element.contains(el))
|
||||
out.push(el);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerAdded(addedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of addedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerAdded);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_c) continue;
|
||||
element.dispatchEvent(new Event(evc));
|
||||
ls.connected = /* @__PURE__ */ new WeakSet();
|
||||
ls.length_c = 0;
|
||||
if (!ls.length_d) store.delete(element);
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function observerRemoved(removedNodes, is_root) {
|
||||
let out = false;
|
||||
for (const element of removedNodes) {
|
||||
if (is_root) collectChildren(element).then(observerRemoved);
|
||||
if (!store.has(element)) continue;
|
||||
const ls = store.get(element);
|
||||
if (!ls.length_d) continue;
|
||||
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
|
||||
out = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function dispatchRemove(element) {
|
||||
return () => {
|
||||
if (element.isConnected) return;
|
||||
element.dispatchEvent(new Event(evd));
|
||||
store.delete(element);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// src/customElement.js
|
||||
function customElementRender(target, render, props = observedAttributes2) {
|
||||
function customElementRender(target, render, props = {}) {
|
||||
const custom_element = target.host || target;
|
||||
scope.push({
|
||||
scope: custom_element,
|
||||
@ -606,81 +658,40 @@ var DDE = (() => {
|
||||
obj[method] = new Proxy(obj[method] || (() => {
|
||||
}), { apply });
|
||||
}
|
||||
function observedAttributes2(instance) {
|
||||
return observedAttributes(instance, (i, n) => i.getAttribute(n));
|
||||
}
|
||||
|
||||
// src/events.js
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (typeof options === "function") {
|
||||
host = options;
|
||||
options = null;
|
||||
}
|
||||
if (!options) options = {};
|
||||
return function dispatch(element, ...d) {
|
||||
if (host) {
|
||||
d.unshift(element);
|
||||
element = typeof host === "function" ? host() : host;
|
||||
}
|
||||
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
|
||||
return element.dispatchEvent(event);
|
||||
};
|
||||
// src/memo.js
|
||||
var memoMark = "__dde_memo";
|
||||
var memo_scope = [];
|
||||
function memo(key, generator) {
|
||||
if (!memo_scope.length) return generator(key);
|
||||
const k = typeof key === "object" ? JSON.stringify(key) : key;
|
||||
const [{ cache, after }] = memo_scope;
|
||||
return after(k, hasOwn(cache, k) ? cache[k] : generator(key));
|
||||
}
|
||||
function on(event, listener, options) {
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(event, listener, options);
|
||||
return element;
|
||||
};
|
||||
}
|
||||
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
|
||||
on.connected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evc, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
|
||||
if (c) c_ch_o.onConnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
memo.isScope = function(obj) {
|
||||
return obj[memoMark];
|
||||
};
|
||||
on.disconnected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(evd, listener, options);
|
||||
if (element[keyLTE]) return element;
|
||||
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
|
||||
if (c) c_ch_o.onDisconnected(element, listener);
|
||||
return element;
|
||||
};
|
||||
};
|
||||
var store_abort = /* @__PURE__ */ new WeakMap();
|
||||
on.disconnectedAsAbort = function(host) {
|
||||
if (store_abort.has(host)) return store_abort.get(host);
|
||||
const a = new AbortController();
|
||||
store_abort.set(host, a);
|
||||
host(on.disconnected(() => a.abort()));
|
||||
return a.signal;
|
||||
};
|
||||
var els_attribute_store = /* @__PURE__ */ new WeakSet();
|
||||
on.attributeChanged = function(listener, options) {
|
||||
if (typeof options !== "object")
|
||||
options = {};
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(eva, listener, options);
|
||||
if (element[keyLTE] || els_attribute_store.has(element))
|
||||
return element;
|
||||
if (!enviroment.M) return element;
|
||||
const observer = new enviroment.M(function(mutations) {
|
||||
for (const { attributeName, target } of mutations)
|
||||
target.dispatchEvent(
|
||||
new CustomEvent(eva, { detail: [attributeName, target.getAttribute(attributeName)] })
|
||||
);
|
||||
memo.scope = function memoScope(fun, { signal, onlyLast } = {}) {
|
||||
let cache = oCreate();
|
||||
function memoScope2(...args) {
|
||||
if (signal && signal.aborted)
|
||||
return fun.apply(this, args);
|
||||
let cache_local = onlyLast ? cache : oCreate();
|
||||
memo_scope.unshift({
|
||||
cache,
|
||||
after(key, val) {
|
||||
return cache_local[key] = val;
|
||||
}
|
||||
});
|
||||
const c = onAbort(options.signal, () => observer.disconnect());
|
||||
if (c) observer.observe(element, { attributes: true });
|
||||
return element;
|
||||
};
|
||||
const out = fun.apply(this, args);
|
||||
memo_scope.shift();
|
||||
cache = cache_local;
|
||||
return out;
|
||||
}
|
||||
memoScope2[memoMark] = true;
|
||||
memoScope2.clear = () => cache = oCreate();
|
||||
if (signal) signal.addEventListener("abort", memoScope2.clear);
|
||||
return memoScope2;
|
||||
};
|
||||
return __toCommonJS(index_exports);
|
||||
})();
|
||||
|
80
dist/iife.min.d.ts
vendored
80
dist/iife.min.d.ts
vendored
@ -18,6 +18,7 @@ export type Actions<V> = Record<string | SymbolOnclear, Action<V>>;
|
||||
export type OnListenerOptions = Pick<AddEventListenerOptions, "signal"> & {
|
||||
first_time?: boolean;
|
||||
};
|
||||
export type SElement = Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
|
||||
export interface signal {
|
||||
_: Symbol;
|
||||
/**
|
||||
@ -63,7 +64,7 @@ export interface signal {
|
||||
* S.el(listS, list=> list.map(li=> el("li", li)));
|
||||
* ```
|
||||
* */
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => Element | Element[] | DocumentFragment): DocumentFragment;
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S) => SElement | SElement[]): DocumentFragment;
|
||||
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
|
||||
}
|
||||
declare const signal: signal;
|
||||
@ -79,10 +80,11 @@ export type CustomElementTagNameMap = {
|
||||
export type SupportedElement = HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | SVGElementTagNameMap[keyof SVGElementTagNameMap] | MathMLElementTagNameMap[keyof MathMLElementTagNameMap] | CustomElementTagNameMap[keyof CustomElementTagNameMap];
|
||||
declare global {
|
||||
type ddeComponentAttributes = Record<any, any> | undefined;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El) => any;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node> = (element: El, ...rest: any) => any;
|
||||
type ddeString = string | Signal<string, {}>;
|
||||
type ddeStringable = ddeString | number | Signal<number, {}>;
|
||||
}
|
||||
export type Host<EL extends SupportedElement> = (...addons: ddeElementAddon<EL>[]) => EL;
|
||||
export type PascalCase = `${Capitalize<string>}${string}`;
|
||||
export type AttrsModified = {
|
||||
/**
|
||||
@ -144,6 +146,7 @@ export namespace el {
|
||||
host?: "this" | "parentElement";
|
||||
}, is_open?: boolean): Comment;
|
||||
}
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement;
|
||||
export function el<A extends ddeComponentAttributes, EL extends SupportedElement | ddeDocumentFragment>(component: (attr: A, ...rest: any[]) => EL, attrs?: NoInfer<A>, ...addons: ddeElementAddon<EL>[]): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap] ? EL : (EL extends ddeDocumentFragment ? EL : ddeHTMLElement);
|
||||
export function el<A extends {
|
||||
textContent: ddeStringable;
|
||||
@ -156,7 +159,6 @@ export function elNS(namespace: "http://www.w3.org/1998/Math/MathML"): <TAG exte
|
||||
[key in keyof EL]: EL[key] | Signal<EL[key], {}> | string | number | boolean;
|
||||
}>, ...addons: ddeElementAddon<NoInfer<EL>>[]) => ddeMathMLElement;
|
||||
export function elNS(namespace: string): (tag_name: string, attrs?: string | ddeStringable | Record<string, any>, ...addons: ddeElementAddon<SupportedElement>[]) => SupportedElement;
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
|
||||
/** Simulate slots for ddeComponents */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(root: EL): EL;
|
||||
/**
|
||||
@ -165,9 +167,9 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(ro
|
||||
* @param body Body of the custom element
|
||||
* */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(el: HTMLElement, body: EL): EL;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, element: SupportedElement): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, host: Host<SupportedElement>): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options?: EventInit): (element: SupportedElement, data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, element: SupportedElement | (() => SupportedElement)): (data?: any) => void;
|
||||
declare function dispatchEvent$1(name: keyof DocumentEventMap | string, options: EventInit | null, host: Host<SupportedElement>): (data?: any) => void;
|
||||
export interface On {
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
<Event extends keyof DocumentEventMap, EE extends ddeElementAddon<SupportedElement> = ddeElementAddon<HTMLElement>>(type: Event, listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any, options?: AddEventListenerOptions): EE;
|
||||
@ -185,7 +187,7 @@ export interface On {
|
||||
export const on: On;
|
||||
export type Scope = {
|
||||
scope: Node | Function | Object;
|
||||
host: ddeElementAddon<any>;
|
||||
host: Host<SupportedElement>;
|
||||
custom_element: false | HTMLElement;
|
||||
prevent: boolean;
|
||||
};
|
||||
@ -201,7 +203,11 @@ export const scope: {
|
||||
* It can be also used to register Addon(s) (functions to be called when component is initized)
|
||||
* — `scope.host(on.connected(console.log))`.
|
||||
* */
|
||||
host: (...addons: ddeElementAddon<SupportedElement>[]) => HTMLElement;
|
||||
host: Host<SupportedElement>;
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
signal: AbortSignal;
|
||||
state: Scope[];
|
||||
/** Adds new child scope. All attributes are inherited by default. */
|
||||
push(scope?: Partial<Scope>): ReturnType<Array<Scope>["push"]>;
|
||||
@ -213,7 +219,6 @@ export const scope: {
|
||||
export function customElementRender<EL extends HTMLElement, P extends any = Record<string, string | Signal<string, {}>>>(target: ShadowRoot | EL, render: (props: P) => SupportedElement | DocumentFragment, props?: P | ((el: EL) => P)): EL;
|
||||
export function customElementWithDDE<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function lifecyclesToEvents<EL extends (new () => HTMLElement)>(custom_element: EL): EL;
|
||||
export function observedAttributes(custom_element: HTMLElement): Record<string, string>;
|
||||
/**
|
||||
* This is used primarly for server side rendering. To be sure that all async operations
|
||||
* are finished before the page is sent to the client.
|
||||
@ -234,6 +239,65 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
|
||||
* ```
|
||||
* */
|
||||
export function queue(promise?: Promise<unknown>): Promise<unknown>;
|
||||
/**
|
||||
* Memoization utility for caching DOM elements to improve performance.
|
||||
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
|
||||
*
|
||||
* @param key - Unique identifier for the element (usually an ID or unique value)
|
||||
* @param generator - Function that creates the element
|
||||
* @returns The cached element if the key exists, otherwise the result of the generator function
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Within S.el for list rendering
|
||||
* S.el(itemsSignal, (items, memo) =>
|
||||
* el("ul").append(
|
||||
* ...items.map(item =>
|
||||
* memo(item.id, () => el(ItemComponent, item))
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
|
||||
/**
|
||||
* Memo namespace containing utility functions for memoization.
|
||||
*/
|
||||
export namespace memo {
|
||||
/**
|
||||
* Checks if an object is a memo scope.
|
||||
* @param obj - The object to check
|
||||
* @returns True if the object is a memo scope
|
||||
*/
|
||||
export function isScope(obj: any): boolean;
|
||||
/**
|
||||
* Creates a memoized function with optional cleanup support.
|
||||
*
|
||||
* @param fun - The function to memoize
|
||||
* @param options - Configuration options
|
||||
* @param options.signal - AbortSignal for cleanup
|
||||
* @param options.onlyLast - When true, only keeps the cache from the most recent call
|
||||
* @returns A memoized version of the function with a .clear() method
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const renderItems = memo.scope(function(items) {
|
||||
* return items.map(item =>
|
||||
* memo(item.id, () => el("div", item.name))
|
||||
* );
|
||||
* }, {
|
||||
* signal: controller.signal,
|
||||
* onlyLast: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function scope<F extends Function>(fun: F, options?: {
|
||||
signal?: AbortSignal;
|
||||
onlyLast?: boolean;
|
||||
}): F & {
|
||||
clear: () => void;
|
||||
};
|
||||
}
|
||||
/* TypeScript MEH */
|
||||
declare global {
|
||||
type ddeAppend<el> = (...nodes: (Node | string)[]) => el;
|
||||
|
2
dist/iife.min.js
vendored
2
dist/iife.min.js
vendored
File diff suppressed because one or more lines are too long
@ -189,6 +189,7 @@ import { el } from "deka-dom-el";
|
||||
* */
|
||||
export function code({ id, src, content, language= "js", className= host.slice(1), page_id }){
|
||||
if(src) content= s.cat(src);
|
||||
content= normalizeIndentation(content);
|
||||
let dataJS;
|
||||
if(page_id){
|
||||
registerClientPart(page_id);
|
||||
@ -198,6 +199,10 @@ export function code({ id, src, content, language= "js", className= host.slice(1
|
||||
el("code", { className: "language-"+language, textContent: content.trim() })
|
||||
);
|
||||
}
|
||||
export function pre({ content }){
|
||||
content= normalizeIndentation(content);
|
||||
return el("pre").append(el("code", content.trim()));
|
||||
}
|
||||
let is_registered= {};
|
||||
/** @param {string} page_id */
|
||||
function registerClientPart(page_id){
|
||||
@ -207,33 +212,6 @@ function registerClientPart(page_id){
|
||||
document.head.append(
|
||||
// Use a newer version of Shiki with better performance
|
||||
el("script", { src: "https://cdn.jsdelivr.net/npm/shiki@0.14.3/dist/index.unpkg.iife.js", defer: true }),
|
||||
// Make sure we can match Flems styling in dark/light mode
|
||||
el("style", `
|
||||
/* Ensure CodeMirror and Shiki use the same font */
|
||||
.CodeMirror *, .shiki * {
|
||||
font-family: var(--font-mono) !important;
|
||||
}
|
||||
|
||||
/* Style Shiki's output to match our theme */
|
||||
.shiki {
|
||||
background-color: var(--shiki-color-background) !important;
|
||||
color: var(--shiki-color-text) !important;
|
||||
padding: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
/* Ensure Shiki code tokens use our CSS variables */
|
||||
.shiki .keyword { color: var(--shiki-token-keyword) !important; }
|
||||
.shiki .constant { color: var(--shiki-token-constant) !important; }
|
||||
.shiki .string { color: var(--shiki-token-string) !important; }
|
||||
.shiki .comment { color: var(--shiki-token-comment) !important; }
|
||||
.shiki .function { color: var(--shiki-token-function) !important; }
|
||||
.shiki .operator, .shiki .punctuation { color: var(--shiki-token-punctuation) !important; }
|
||||
.shiki .parameter { color: var(--shiki-token-parameter) !important; }
|
||||
.shiki .variable { color: var(--shiki-token-variable) !important; }
|
||||
.shiki .property { color: var(--shiki-token-property) !important; }
|
||||
`),
|
||||
);
|
||||
|
||||
registerClientFile(
|
||||
@ -245,3 +223,9 @@ function registerClientPart(page_id){
|
||||
|
||||
is_registered[page_id]= true;
|
||||
}
|
||||
/** @param {string} src */
|
||||
function normalizeIndentation(src){
|
||||
const lines= src.split("\n");
|
||||
const min_indent= Math.min(...lines.map(line=> line.search(/\S/)).filter(i=> i >= 0));
|
||||
return lines.map(line=> line.slice(min_indent)).join("\n");
|
||||
}
|
||||
|
@ -96,6 +96,18 @@ html[data-theme="light"] .cm-s-material .cm-error { color: #f44336 !important; }
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
${host}[data-variant=big]{
|
||||
height: 100vh;
|
||||
|
||||
main {
|
||||
flex-flow: column nowrap;
|
||||
flex-grow: 1;
|
||||
}
|
||||
main > * {
|
||||
width: 100%;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const dde_content= s.cat(new URL("../../dist/esm-with-signals.js", import.meta.url)).toString();
|
||||
@ -108,15 +120,16 @@ import { relative } from "node:path";
|
||||
* @param {object} attrs
|
||||
* @param {URL} attrs.src Example code file path
|
||||
* @param {"js"|"ts"|"html"|"css"} [attrs.language="js"] Language of the code
|
||||
* @param {"normal"|"big"} [attrs.variant="normal"] Size of the example
|
||||
* @param {string} attrs.page_id ID of the page
|
||||
* */
|
||||
export function example({ src, language= "js", page_id }){
|
||||
export function example({ src, language= "js", variant= "normal", page_id }){
|
||||
registerClientPart(page_id);
|
||||
const content= s.cat(src).toString()
|
||||
.replaceAll(/ from "deka-dom-el(\/signals)?";/g, ' from "./esm-with-signals.js";');
|
||||
const id= "code-example-"+generateCodeId(src);
|
||||
return el().append(
|
||||
el(code, { id, content, language, className: example.name }),
|
||||
el(code, { id, content, language, className: example.name }, el=> el.dataset.variant= variant),
|
||||
elCode({ id, content, extension: "."+language })
|
||||
);
|
||||
}
|
||||
|
@ -8,7 +8,8 @@ export class HTMLCustomElement extends HTMLElement{
|
||||
connectedCallback(){
|
||||
customElementRender(
|
||||
this.attachShadow({ mode: "open" }),
|
||||
ddeComponent
|
||||
ddeComponent,
|
||||
this
|
||||
);
|
||||
}
|
||||
set attr(value){ this.setAttribute("attr", value); }
|
||||
|
@ -2,7 +2,6 @@
|
||||
import {
|
||||
customElementRender,
|
||||
customElementWithDDE,
|
||||
observedAttributes,
|
||||
} from "deka-dom-el";
|
||||
/** @type {ddePublicElementTagNameMap} */
|
||||
import { S } from "deka-dom-el/signals";
|
||||
|
@ -9,7 +9,7 @@ export class HTMLCustomElement extends HTMLElement{
|
||||
// nice place to render custom element
|
||||
}
|
||||
attributeChangedCallback(name, oldValue, newValue){
|
||||
// listen to attribute changes (see `observedAttributes`)
|
||||
// listen to attribute changes (see `S.observedAttributes`)
|
||||
}
|
||||
disconnectedCallback(){
|
||||
// nice place to clean up
|
||||
|
@ -1,7 +1,6 @@
|
||||
import {
|
||||
customElementRender,
|
||||
customElementWithDDE,
|
||||
observedAttributes,
|
||||
el, on, scope,
|
||||
} from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
@ -9,7 +8,6 @@ export class HTMLCustomElement extends HTMLElement{
|
||||
static tagName= "custom-element";
|
||||
static observedAttributes= [ "attr" ];
|
||||
connectedCallback(){
|
||||
console.log(observedAttributes(this));
|
||||
customElementRender(
|
||||
this.attachShadow({ mode: "open" }),
|
||||
ddeComponent,
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Example of reactive element marker
|
||||
<!--<dde:mark type=\"reactive\" source=\"...\">-->
|
||||
<!--<dde:mark type="reactive" source="...">-->
|
||||
<!-- content that updates when signal changes -->
|
||||
<!--</dde:mark>-->
|
@ -24,7 +24,11 @@ document.body.append(
|
||||
);
|
||||
|
||||
import { chainableAppend } from "deka-dom-el";
|
||||
/** @param {keyof HTMLElementTagNameMap} tag */
|
||||
/**
|
||||
* @template {keyof HTMLElementTagNameMap} TAG
|
||||
* @param {TAG} tag
|
||||
* @returns {ddeHTMLElementTagNameMap[TAG] extends HTMLElement ? ddeHTMLElementTagNameMap[TAG] : ddeHTMLElement}
|
||||
* */
|
||||
const createElement= tag=> chainableAppend(document.createElement(tag));
|
||||
document.body.append(
|
||||
createElement("p").append(
|
||||
|
@ -8,12 +8,12 @@ button.disabled = true;
|
||||
const button2 = Object.assign(
|
||||
document.createElement('button'),
|
||||
{
|
||||
textContent: "Click me",
|
||||
className: "primary",
|
||||
disabled: true
|
||||
textContent: "Click me",
|
||||
className: "primary",
|
||||
disabled: true
|
||||
}
|
||||
);
|
||||
|
||||
// Add to DOM
|
||||
document.body.appendChild(button);
|
||||
document.body.appendChild(button2);
|
||||
document.body.append(button);
|
||||
document.body.append(button2);
|
||||
|
@ -2,14 +2,14 @@
|
||||
const div = document.createElement('div');
|
||||
const h1 = document.createElement('h1');
|
||||
h1.textContent = 'Title';
|
||||
div.appendChild(h1);
|
||||
div.append(h1);
|
||||
|
||||
const p = document.createElement('p');
|
||||
p.textContent = 'Paragraph';
|
||||
div.appendChild(p);
|
||||
div.append(p);
|
||||
|
||||
// appendChild doesn't return parent
|
||||
// append doesn't return parent
|
||||
// so chaining is not possible
|
||||
|
||||
// Add to DOM
|
||||
document.body.appendChild(div);
|
||||
document.body.append(div);
|
||||
|
19
docs/components/examples/events/dispatch.js
Normal file
19
docs/components/examples/events/dispatch.js
Normal 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))
|
||||
);
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { el, on } from "deka-dom-el";
|
||||
const paragraph= el("p", "See live-cycle events in console.",
|
||||
const paragraph= el("p", "See lifecycle events in console.",
|
||||
el=> log({ type: "dde:created", detail: el }),
|
||||
on.connected(log),
|
||||
on.disconnected(log),
|
||||
on.attributeChanged(log));
|
||||
);
|
||||
|
||||
document.body.append(
|
||||
paragraph,
|
||||
|
@ -6,9 +6,8 @@ let count = 0;
|
||||
button.addEventListener('click', () => {
|
||||
count++;
|
||||
document.querySelector('p').textContent =
|
||||
'Clicked ' + count + ' times';
|
||||
'Clicked ' + count + ' times';
|
||||
|
||||
if (count > 10) {
|
||||
button.disabled = true;
|
||||
}
|
||||
if (count > 10)
|
||||
button.disabled = true;
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { el, on } from "deka-dom-el";
|
||||
import { el } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
|
||||
// A HelloWorld component using the 3PS pattern
|
||||
@ -27,4 +27,4 @@ function HelloWorld({ emoji = "🚀" }) {
|
||||
// Use the component in your app
|
||||
document.body.append(
|
||||
el(HelloWorld, { emoji: "🎉" })
|
||||
);
|
||||
);
|
||||
|
@ -15,15 +15,15 @@ function HelloWorldComponent({ initial }){
|
||||
|
||||
return el().append(
|
||||
el("p", {
|
||||
textContent: S(() => `Hello World ${emoji().repeat(clicks())}`),
|
||||
textContent: S(() => `Hello World ${emoji.get().repeat(clicks.get())}`),
|
||||
className: "example",
|
||||
ariaLive: "polite", //OR ariaset: { live: "polite" },
|
||||
dataset: { example: "Example" }, //OR dataExample: "Example",
|
||||
}),
|
||||
el("button",
|
||||
{ textContent: "Fire", type: "button" },
|
||||
on("click", ()=> clicks(clicks() + 1)),
|
||||
on("keyup", ()=> clicks(clicks() - 2)),
|
||||
on("click", ()=> clicks.set(clicks.get() + 1)),
|
||||
on("keyup", ()=> clicks.set(clicks.get() - 2)),
|
||||
),
|
||||
el("select", null, onChange).append(
|
||||
el(OptionComponent, "🎉", isSelected),//OR { textContent: "🎉" }
|
||||
|
2
docs/components/examples/optimization/intro.js
Normal file
2
docs/components/examples/optimization/intro.js
Normal 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";
|
115
docs/components/examples/optimization/memo.js
Normal file
115
docs/components/examples/optimization/memo.js
Normal 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));
|
386
docs/components/examples/reallife/todomvc.js
Normal file
386
docs/components/examples/reallife/todomvc.js
Normal 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;
|
||||
}
|
@ -14,7 +14,7 @@ function component(){
|
||||
const textContent= S("Click to change text.");
|
||||
|
||||
const onclickChange= on("click", function redispatch(){
|
||||
textContent("Text changed! "+(new Date()).toString())
|
||||
textContent.set("Text changed! "+(new Date()).toString())
|
||||
});
|
||||
return el("p", textContent, onclickChange);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
@ -16,7 +16,9 @@ function Counter() {
|
||||
// THE HOST IS PROBABLY DIFFERENT THAN
|
||||
// YOU EXPECT AND SIGNAL MAY BE
|
||||
// UNEXPECTEDLY REMOVED!!!
|
||||
host().querySelector("button").disabled = count.get() >= 10;
|
||||
S.on(count, (count)=>
|
||||
host().querySelector("button").disabled = count >= 10
|
||||
);
|
||||
};
|
||||
setTimeout(()=> {
|
||||
// ok, BUT consider extract to separate function
|
||||
|
@ -1,6 +1,5 @@
|
||||
// Handling async data in SSR
|
||||
import { JSDOM } from "jsdom";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
import { register, queue } from "deka-dom-el/jsdom";
|
||||
|
||||
async function renderWithAsyncData() {
|
||||
@ -8,23 +7,7 @@ async function renderWithAsyncData() {
|
||||
const { el } = await register(dom);
|
||||
|
||||
// Create a component that fetches data
|
||||
function AsyncComponent() {
|
||||
const title= S("-");
|
||||
const description= S("-");
|
||||
|
||||
// Use the queue to track the async operation
|
||||
queue(fetch("https://api.example.com/data")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
title.set(data.title);
|
||||
description.set(data.description);
|
||||
}));
|
||||
|
||||
return el("div", { className: "async-content" }).append(
|
||||
el("h2", title),
|
||||
el("p", description)
|
||||
);
|
||||
}
|
||||
const { AsyncComponent } = await import("./components/AsyncComponent.js");
|
||||
|
||||
// Render the page
|
||||
dom.window.document.body.append(
|
||||
@ -41,3 +24,24 @@ async function renderWithAsyncData() {
|
||||
}
|
||||
|
||||
renderWithAsyncData();
|
||||
|
||||
// file: components/AsyncComponent.js
|
||||
import { el } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
function AsyncComponent() {
|
||||
const title= S("-");
|
||||
const description= S("-");
|
||||
|
||||
// Use the queue to track the async operation
|
||||
queue(fetch("https://api.example.com/data")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
title.set(data.title);
|
||||
description.set(data.description);
|
||||
}));
|
||||
|
||||
return el("div", { className: "async-content" }).append(
|
||||
el("h2", title),
|
||||
el("p", description)
|
||||
);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ async function renderPage() {
|
||||
const { el } = await register(dom);
|
||||
|
||||
// Create a simple header component
|
||||
// can be separated into a separate file and use `import { el } from "deka-dom-el"`
|
||||
function Header({ title }) {
|
||||
return el("header").append(
|
||||
el("h1", title),
|
||||
|
@ -17,6 +17,7 @@ async function renderPage() {
|
||||
const { el } = await register(dom);
|
||||
|
||||
// 4. Dynamically import page components
|
||||
// use `import { el } from "deka-dom-el"`
|
||||
const { Header } = await import("./components/Header.js");
|
||||
const { Content } = await import("./components/Content.js");
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Basic jsdom integration example
|
||||
import { JSDOM } from "jsdom";
|
||||
import { register, unregister, queue } from "deka-dom-el/jsdom.js";
|
||||
import { register, unregister, queue } from "deka-dom-el/jsdom";
|
||||
|
||||
// Create a jsdom instance
|
||||
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
|
||||
|
@ -12,7 +12,7 @@ async function buildSite() {
|
||||
];
|
||||
|
||||
// Create output directory
|
||||
mkdirSync("./dist", { recursive: true });
|
||||
mkdirSync("./dist/docs", { recursive: true });
|
||||
|
||||
// Build each page
|
||||
for (const page of pages) {
|
||||
@ -23,6 +23,7 @@ async function buildSite() {
|
||||
const { el } = await register(dom);
|
||||
|
||||
// Import the page component
|
||||
// use `import { el } from "deka-dom-el"`
|
||||
const { default: PageComponent } = await import(page.component);
|
||||
|
||||
// Render the page with its metadata
|
||||
@ -35,7 +36,7 @@ async function buildSite() {
|
||||
|
||||
// Write the HTML to a file
|
||||
const html = dom.serialize();
|
||||
writeFileSync(`./dist/${page.id}.html`, html);
|
||||
writeFileSync(`./dist/docs/${page.id}.html`, html);
|
||||
|
||||
console.log(`Built page: ${page.id}.html`);
|
||||
}
|
||||
|
@ -11,10 +11,6 @@ export function mnemonic(){
|
||||
el("code", "customElementWithDDE(<custom-element>)"),
|
||||
" — register <custom-element> to DDE library, see also `lifecyclesToEvents`, can be also used as decorator",
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "observedAttributes(<custom-element>)"),
|
||||
" — returns record of observed attributes (keys uses camelCase)",
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "S.observedAttributes(<custom-element>)"),
|
||||
" — returns record of observed attributes (keys uses camelCase and values are signals)",
|
||||
@ -32,4 +28,4 @@ export function mnemonic(){
|
||||
" — simulate slots for “dde”/functional components",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -16,15 +16,16 @@ export function mnemonic(){
|
||||
el("code", "dispatchEvent(<event>[, <options>])(element)"),
|
||||
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>]))")
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "dispatchEvent(<event>, <element>)([<detail>])"),
|
||||
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>))"), " or ",
|
||||
el("code", "<element>.dispatchEvent(new CustomEvent(<event>, { detail: <detail> }))")
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "dispatchEvent(<event>[, <options>])(<element>[, <detail>])"),
|
||||
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>] ))"), " or ",
|
||||
el("code", "<element>.dispatchEvent(new CustomEvent(<event>, { detail: <detail> }))")
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "dispatchEvent(<event>[, <options>], <host>)([<detail>])"),
|
||||
" — just ", el("code", "<host>().dispatchEvent(new Event(<event>[, <options>]))"), " or ",
|
||||
el("code", "<host>().dispatchEvent(new CustomEvent(<event>, { detail: <detail> }[, <options>] ))"),
|
||||
" (see scopes section of docs)"
|
||||
),
|
||||
);
|
||||
}
|
||||
|
15
docs/components/mnemonic/optimization-init.js
Normal file
15
docs/components/mnemonic/optimization-init.js
Normal 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)",
|
||||
),
|
||||
);
|
||||
}
|
@ -14,6 +14,10 @@ export function mnemonic(){
|
||||
el("li").append(
|
||||
el("code", "scope.host(...<addons>)"),
|
||||
" — use addons to current component",
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "scope.signal"),
|
||||
" — get AbortSignal that triggers when the element disconnects",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
39
docs/components/scrollTop.css.js
Normal file
39
docs/components/scrollTop.css.js
Normal 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;
|
||||
}
|
||||
}
|
||||
`;
|
14
docs/components/scrollTop.js.js
Normal file
14
docs/components/scrollTop.js.js
Normal 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" });
|
||||
}
|
||||
})
|
||||
}
|
@ -16,7 +16,6 @@ styles.css`
|
||||
SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
|
||||
--body-max-width: 40rem;
|
||||
--sidebar-width: 20rem;
|
||||
--header-height: 4rem;
|
||||
--border-radius: 0.375rem;
|
||||
|
||||
@ -73,6 +72,7 @@ styles.css`
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
tab-size: var(--tab-size, 2rem);
|
||||
}
|
||||
|
||||
/* Accessibility improvements */
|
||||
@ -215,8 +215,8 @@ body {
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
body {
|
||||
grid-template-rows: var(--header-height) 1fr;
|
||||
grid-template-columns: var(--sidebar-width) 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"sidebar content";
|
||||
@ -250,7 +250,7 @@ h2 {
|
||||
|
||||
/* Section headings with better visual hierarchy */
|
||||
body > main h3, body > main h4 {
|
||||
scroll-margin-top: calc(var(--header-height) + 1rem);
|
||||
scroll-margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* Make clickable heading links for better navigation */
|
||||
|
@ -36,9 +36,9 @@ export function page({ pkg, info }){
|
||||
el("h4", t`What Makes dd<el> Special`),
|
||||
el("ul").append(
|
||||
el("li", t`No build step required — use directly in the browser`),
|
||||
el("li", t`Lightweight core (~10–15kB minified) with zero dependencies`),
|
||||
el("li", t`Lightweight core (~10–15kB minified) without unnecessary dependencies (0 at now 😇)`),
|
||||
el("li", t`Natural DOM API — work with real DOM nodes, not abstractions`),
|
||||
el("li", t`Built-in reactivity with powerful signals system`),
|
||||
el("li", t`Built-in reactivity with simplified but powerful signals system`),
|
||||
el("li", t`Clean code organization with the 3PS pattern`)
|
||||
)
|
||||
),
|
||||
@ -67,7 +67,7 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
el("ol").append(
|
||||
el("li").append(...T`
|
||||
${el("strong", "Create State")}: Define your application's reactive data using signals
|
||||
${el("strong", "Create State")}: Define your application’s reactive data using signals
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Bind to Elements")}: Define how UI elements react to state changes
|
||||
@ -87,30 +87,44 @@ export function page({ pkg, info }){
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(...T`
|
||||
The 3PS pattern becomes especially powerful when combined with components, allowing you to create
|
||||
reusable pieces of UI with encapsulated state and behavior. You'll learn more about this in the
|
||||
reusable pieces of UI with encapsulated state and behavior. You’ll learn more about this in the
|
||||
following sections.
|
||||
`)
|
||||
),
|
||||
|
||||
el(h3, t`How to Use This Documentation`),
|
||||
el("p").append(...T`
|
||||
This guide will take you through dd<el>'s features step by step:
|
||||
This guide will take you through dd<el>’s features step by step:
|
||||
`),
|
||||
el("ol").append(
|
||||
el("li").append(...T`${el("strong", "Elements")} — Creating and manipulating DOM elements`),
|
||||
el("li").append(...T`${el("strong", "Events")} — Handling user interactions and lifecycle events`),
|
||||
el("li").append(...T`${el("strong", "Signals")} — Adding reactivity to your UI`),
|
||||
el("li").append(...T`${el("strong", "Scopes")} — Managing component lifecycles`),
|
||||
el("li").append(...T`${el("strong", "Custom Elements")} — Building web components`),
|
||||
el("li").append(...T`${el("strong", "Debugging")} — Tools to help you build and fix your apps`),
|
||||
el("li").append(...T`${el("strong", "Extensions")} — Integrating third-party functionalities`),
|
||||
el("li").append(...T`${el("strong", "Ireland Components")} —
|
||||
Creating interactive demos with server-side pre-rendering`),
|
||||
el("li").append(...T`${el("strong", "SSR")} — Server-side rendering with dd<el>`)
|
||||
el("ol", { start: 2 }).append(
|
||||
el("li").append(...T`${el("a", { href: "p02-elements.html" }).append(el("strong", "Elements"))} — Creating
|
||||
and manipulating DOM elements`),
|
||||
el("li").append(...T`${el("a", { href: "p03-events.html" }).append(el("strong", "Events and Addons"))} —
|
||||
Handling user interactions and lifecycle events`),
|
||||
el("li").append(...T`${el("a", { href: "p04-signals.html" }).append(el("strong", "Signals"))} — Adding
|
||||
reactivity to your UI`),
|
||||
el("li").append(...T`${el("a", { href: "p05-scopes.html" }).append(el("strong", "Scopes"))} — Managing
|
||||
component lifecycles`),
|
||||
el("li").append(...T`${el("a", { href: "p06-customElement.html" }).append(el("strong", "Web Components"))} —
|
||||
Building native custom elements`),
|
||||
el("li").append(...T`${el("a", { href: "p07-debugging.html" }).append(el("strong", "Debugging"))} — Tools to
|
||||
help you build and fix your apps`),
|
||||
el("li").append(...T`${el("a", { href: "p08-extensions.html" }).append(el("strong", "Extensions"))} —
|
||||
Integrating third-party functionalities`),
|
||||
el("li").append(...T`${el("a", { href: "p09-optimization.html" })
|
||||
.append(el("strong", "Performance Optimization"))} — Techniques for optimizing your applications`),
|
||||
el("li").append(...T`${el("a", { href: "p10-todomvc.html" }).append(el("strong", "TodoMVC"))} — A real-world
|
||||
application implementation`),
|
||||
el("li").append(...T`${el("a", { href: "p11-ssr.html" }).append(el("strong", "SSR"))} — Server-side
|
||||
rendering with dd<el>`),
|
||||
el("li").append(...T`${el("a", { href: "p12-ireland.html" }).append(el("strong", "Ireland Components"))} —
|
||||
Interactive demos with server-side pre-rendering`),
|
||||
el("li").append(...T`${el("a", { href: "p13-appendix.html" }).append(el("strong", "Appendix & Summary"))} —
|
||||
Comprehensive reference and best practices`),
|
||||
),
|
||||
el("p").append(...T`
|
||||
Each section builds on the previous ones, so we recommend following them in order.
|
||||
Let's get started with the basics of creating elements!
|
||||
Let’s get started with the basics of creating elements!
|
||||
`),
|
||||
);
|
||||
}
|
||||
|
@ -105,13 +105,8 @@ ${host_nav} a .nav-number {
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
${host_nav} {
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-right: none;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@ -121,14 +116,7 @@ ${host_nav} a .nav-number {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
${host_nav} a .nav-number {
|
||||
width: auto;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
${host_nav} a:first-child {
|
||||
margin-bottom: 0;
|
||||
margin-right: 0.5rem;
|
||||
min-width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
@ -3,6 +3,10 @@ import { el, simulateSlots } from "deka-dom-el";
|
||||
|
||||
import { header } from "./head.html.js";
|
||||
import { prevNext } from "../components/pageUtils.html.js";
|
||||
import { ireland } from "../components/ireland.html.js";
|
||||
import "../components/scrollTop.css.js";
|
||||
/** @param {string} url */
|
||||
const fileURL= url=> new URL(url, import.meta.url);
|
||||
|
||||
/** @param {Pick<import("../types.d.ts").PageAttrs, "pkg" | "info">} attrs */
|
||||
export function simplePage({ pkg, info }){
|
||||
@ -26,6 +30,9 @@ export function simplePage({ pkg, info }){
|
||||
|
||||
// Navigation between pages
|
||||
el(prevNext, info)
|
||||
)
|
||||
),
|
||||
|
||||
// Scroll to top button
|
||||
el(ireland, { src: fileURL("../components/scrollTop.js.js"), exportName: "scrollTop" })
|
||||
));
|
||||
}
|
||||
|
@ -54,13 +54,13 @@ export function page({ pkg, info }){
|
||||
dd<el> provides a simple yet powerful approach to element creation that is declarative, chainable,
|
||||
and maintains a clean syntax close to HTML structure.
|
||||
`),
|
||||
el("div", { class: "callout" }).append(
|
||||
el("div", { className: "callout" }).append(
|
||||
el("h4", t`dd<el> Elements: Key Benefits`),
|
||||
el("ul").append(
|
||||
el("li", t`Declarative element creation with intuitive property assignment`),
|
||||
el("li", t`Chainable methods for natural DOM tree construction`),
|
||||
el("li", t`Simplified component patterns for code reuse`),
|
||||
el("li", t`Normalized property/attribute handling across browsers`),
|
||||
el("li", t`Normalized declarative property/attribute handling across browsers`),
|
||||
el("li", t`Smart element return values for cleaner code flow`)
|
||||
)
|
||||
),
|
||||
@ -71,10 +71,10 @@ export function page({ pkg, info }){
|
||||
el("p").append(...T`
|
||||
In standard JavaScript, you create DOM elements using the
|
||||
${el("a", references.mdn_create).append(el("code", "document.createElement()"))} method
|
||||
and then set properties individually or with Object.assign():
|
||||
and then set properties individually or with ${el("code", "Object.assign()")}:
|
||||
`),
|
||||
el("div", { class: "illustration" }).append(
|
||||
el("div", { class: "comparison" }).append(
|
||||
el("div", { className: "illustration" }).append(
|
||||
el("div", { className: "comparison" }).append(
|
||||
el("div").append(
|
||||
el("h5", t`Native DOM API`),
|
||||
el(code, { src: fileURL("./components/examples/elements/native-dom-create.js"), page_id })
|
||||
@ -86,41 +86,45 @@ export function page({ pkg, info }){
|
||||
)
|
||||
),
|
||||
el("p").append(...T`
|
||||
The ${el("code", "el")} function provides a simple wrapper around ${el("code", "document.createElement")}
|
||||
The ${el("code", "el")} function provides a simple wrapper around ${el("code", "document.createElement")}
|
||||
with enhanced property assignment.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaCreateElement.js"), page_id }),
|
||||
|
||||
el(h3, t`Advanced Property Assignment`),
|
||||
el("p").append(...T`
|
||||
The ${el("code", "assign")} function is the heart of dd<el>'s element property handling. It is internally
|
||||
The ${el("code", "assign")} function is the heart of dd<el>’s element property handling. It is internally
|
||||
used to assign properties using the ${el("code", "el")} function. ${el("code", "assign")} provides
|
||||
intelligent assignment of both properties (IDL) and attributes:
|
||||
intelligent assignment of both ${el("a", { textContent: "properties (IDL)", ...references.mdn_idl })}
|
||||
and attributes:
|
||||
`),
|
||||
el("div", { class: "function-table" }).append(
|
||||
el("div", { className: "function-table" }).append(
|
||||
el("dl").append(
|
||||
el("dt", t`Property vs Attribute Priority`),
|
||||
el("dd", t`Prefers IDL properties, falls back to setAttribute() when no writable property exists`),
|
||||
|
||||
el("dt", t`Data and ARIA Attributes`),
|
||||
el("dd", t`Both dataset.* and data-* syntaxes supported (same for ARIA)`),
|
||||
el("dd").append(...T`Both ${el("code", "dataset")}.* and ${el("code", "data-")}* syntaxes supported
|
||||
(same for ${el("em", "ARIA")})`),
|
||||
|
||||
el("dt", t`Style Handling`),
|
||||
el("dd", t`Accepts string or object notation for style property`),
|
||||
el("dd").append(...T`Accepts string or object notation for ${el("code", "style")} property`),
|
||||
|
||||
el("dt", t`Class Management`),
|
||||
el("dd", t`Works with className, class, or classList object for toggling classes`),
|
||||
el("dd").append(...T`Works with ${el("code", "className")}, ${el("code", "class")}, or ${el("code",
|
||||
"classList")} object for toggling classes`),
|
||||
|
||||
el("dt", t`Force Modes`),
|
||||
el("dd", t`Use = prefix to force attribute mode, . prefix to force property mode`),
|
||||
el("dd").append(...T`Use ${el("code", "=")} prefix to force attribute mode, ${el("code", ".")} prefix to
|
||||
force property mode`),
|
||||
|
||||
el("dt", t`Attribute Removal`),
|
||||
el("dd", t`Pass undefined to remove a property or attribute`)
|
||||
el("dd").append(...T`Pass ${el("code", "undefined")} to remove a property or attribute`)
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaAssign.js"), page_id }),
|
||||
|
||||
el("div", { class: "note" }).append(
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(...T`
|
||||
You can explore standard HTML element properties in the MDN documentation for
|
||||
${el("a", { textContent: "HTMLElement", ...references.mdn_el })} (base class)
|
||||
@ -131,16 +135,16 @@ export function page({ pkg, info }){
|
||||
el(h3, t`Building DOM Trees with Chainable Methods`),
|
||||
el("p").append(...T`
|
||||
One of the most powerful features of dd<el> is its approach to building element trees.
|
||||
Unlike the native DOM API which doesn't return the parent after appendChild(), dd<el>'s
|
||||
append() always returns the parent element:
|
||||
Unlike the native DOM API which doesn’t return the parent after ${el("code", "append()")}, dd<el>’s
|
||||
${el("code", "append()")} always returns the parent element:
|
||||
`),
|
||||
el("div", { class: "illustration" }).append(
|
||||
el("div", { class: "comparison" }).append(
|
||||
el("div", { class: "bad-practice" }).append(
|
||||
el("div", { className: "illustration" }).append(
|
||||
el("div", { className: "comparison" }).append(
|
||||
el("div", { className: "bad-practice" }).append(
|
||||
el("h5", t`❌ Native DOM API`),
|
||||
el(code, { src: fileURL("./components/examples/elements/native-dom-tree.js"), page_id })
|
||||
),
|
||||
el("div", { class: "good-practice" }).append(
|
||||
el("div", { className: "good-practice" }).append(
|
||||
el("h5", t`✅ dd<el> Approach`),
|
||||
el(code, { src: fileURL("./components/examples/elements/dde-dom-tree.js"), page_id })
|
||||
)
|
||||
@ -148,7 +152,7 @@ export function page({ pkg, info }){
|
||||
),
|
||||
el("p").append(...T`
|
||||
This chainable pattern is much cleaner and easier to follow, especially for deeply nested elements.
|
||||
It also makes it simple to add multiple children to a parent element in a single fluent expression.
|
||||
It also makes it simple to add multiple children to a parent element in a single fluent expression.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/elements/dekaAppend.js"), page_id }),
|
||||
|
||||
@ -162,12 +166,17 @@ export function page({ pkg, info }){
|
||||
Component functions receive the properties object as their first argument, just like regular elements.
|
||||
This makes it easy to pass data down to components and create reusable UI fragments.
|
||||
`),
|
||||
el("div", { class: "tip" }).append(
|
||||
el("div", { className: "tip" }).append(
|
||||
el("p").append(...T`
|
||||
It's helpful to use naming conventions similar to native DOM elements for your components.
|
||||
This allows you to use ${el("a", { textContent: "destructuring assignment", ...references.mdn_destruct })}
|
||||
and keeps your code consistent with the DOM API.
|
||||
`)
|
||||
It’s helpful to use naming conventions similar to native DOM elements for your components.
|
||||
This allows you to keeps your code consistent with the DOM API.
|
||||
`),
|
||||
el("p").append(...T`
|
||||
Use ${el("a", { textContent: "destructuring assignment", ...references.mdn_destruct })}
|
||||
to extract the properties from the ${el("code", "props")} and pass them to the component element:
|
||||
${el("code", "function component({ className }){ return el(\"p\", { className }); }")} for make
|
||||
templates cleaner.
|
||||
`),
|
||||
),
|
||||
|
||||
el(h3, t`Working with SVG and Other Namespaces`),
|
||||
@ -194,7 +203,7 @@ export function page({ pkg, info }){
|
||||
from the props object for cleaner component code.
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Leverage chainable methods for better performance:")} Use chainable methods like
|
||||
${el("strong", "Leverage chainable methods for better performance:")} Use chainable methods
|
||||
${el("code", ".append()")} to build complex DOM trees for better performance and cleaner code.
|
||||
`),
|
||||
),
|
||||
|
@ -36,11 +36,6 @@ const references= {
|
||||
mdn_mutation: {
|
||||
href: "https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver",
|
||||
},
|
||||
/** Readding the element to the DOM fix by Vue */
|
||||
vue_fix: {
|
||||
title: t`Vue and Web Components, lifecycle implementation readding the element to the DOM`,
|
||||
href: "https://vuejs.org/guide/extras/web-components.html#lifecycle",
|
||||
}
|
||||
};
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
@ -48,11 +43,11 @@ export function page({ pkg, info }){
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(...T`
|
||||
Events are at the core of interactive web applications. dd<el> provides a clean, declarative approach to
|
||||
handling DOM events and extends this pattern with a powerful Addon system to incorporate additional
|
||||
handling DOM events and extends this pattern with a powerful Addon system to incorporate additional
|
||||
functionalities into your UI templates.
|
||||
`),
|
||||
el("div", { className: "callout" }).append(
|
||||
el("h4", t`Why dd<el>'s Event System and Addons Matters`),
|
||||
el("h4", t`Why dd<el>’s Event System and Addons Matters`),
|
||||
el("ul").append(
|
||||
el("li", t`Integrate event handling directly in element declarations`),
|
||||
el("li", t`Leverage lifecycle events for better component design`),
|
||||
@ -68,23 +63,23 @@ export function page({ pkg, info }){
|
||||
el("p").append(...T`
|
||||
In JavaScript you can listen to native DOM events using
|
||||
${el("a", references.mdn_listen).append(el("code", "element.addEventListener(type, listener, options)"))}.
|
||||
dd<el> provides an alternative approach with arguments ordered differently to better fit its declarative
|
||||
dd<el> provides an alternative approach with arguments ordered differently to better fit its declarative
|
||||
style:
|
||||
`),
|
||||
el("div", { className: "illustration" }).append(
|
||||
el("div", { className: "tabs" }).append(
|
||||
el("div", { className: "tab" }).append(
|
||||
el("h5", t`Native DOM API`),
|
||||
el(code, { content: `element.addEventListener('click', callback, options);`, page_id })
|
||||
el(code, { content: `element.addEventListener("click", callback, options);`, page_id })
|
||||
),
|
||||
el("div", { className: "tab" }).append(
|
||||
el("h5", t`dd<el> Approach`),
|
||||
el(code, { content: `on('click', callback, options)(element);`, page_id })
|
||||
el(code, { content: `on("click", callback, options)(element);`, page_id })
|
||||
)
|
||||
)
|
||||
),
|
||||
el("p").append(...T`
|
||||
The main benefit of dd<el>'s approach is that it works as an Addon, making it easy to integrate
|
||||
The main benefit of dd<el>’s approach is that it works as an Addon (see below), making it easy to integrate
|
||||
directly into element declarations.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/events/compare.js"), page_id }),
|
||||
@ -97,10 +92,14 @@ export function page({ pkg, info }){
|
||||
`)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/events/abortSignal.js"), page_id }),
|
||||
el("p").append(...T`
|
||||
This is the same for signals (see next section) and works well with scopes and library extendability (
|
||||
see scopes and extensions section).
|
||||
`),
|
||||
|
||||
el(h3, t`Three Ways to Handle Events`),
|
||||
el("div", { className: "tabs" }).append(
|
||||
el("div", { className: "tab", "data-tab": "html-attr" }).append(
|
||||
el("div", { className: "tab", dataTab: "html-attr" }).append(
|
||||
el("h4", t`HTML Attribute Style`),
|
||||
el(code, { src: fileURL("./components/examples/events/attribute-event.js"), page_id }),
|
||||
el("p").append(...T`
|
||||
@ -109,25 +108,25 @@ export function page({ pkg, info }){
|
||||
useful for SSR scenarios.
|
||||
`)
|
||||
),
|
||||
el("div", { className: "tab", "data-tab": "property" }).append(
|
||||
el("div", { className: "tab", dataTab: "property" }).append(
|
||||
el("h4", t`Property Assignment`),
|
||||
el(code, { src: fileURL("./components/examples/events/property-event.js"), page_id }),
|
||||
el("p", t`Assigns the event handler directly to the element's property.`)
|
||||
el("p", t`Assigns the event handler directly to the element’s property.`)
|
||||
),
|
||||
el("div", { className: "tab", "data-tab": "addon" }).append(
|
||||
el("div", { className: "tab", dataTab: "addon" }).append(
|
||||
el("h4", t`Addon Approach`),
|
||||
el(code, { src: fileURL("./components/examples/events/chain-event.js"), page_id }),
|
||||
el("p", t`Uses the addon pattern, see above.`)
|
||||
el("p", t`Uses the addon pattern (so adds the event listener to the element), see above.`)
|
||||
)
|
||||
),
|
||||
el("p").append(...T`
|
||||
For a deeper comparison of these approaches, see
|
||||
${el("a", { textContent: "WebReflection's detailed analysis", ...references.web_events })}.
|
||||
${el("a", { textContent: "WebReflection’s detailed analysis", ...references.web_events })}.
|
||||
`),
|
||||
|
||||
el(h3, t`Understanding Addons`),
|
||||
el("p").append(...T`
|
||||
Addons are a powerful pattern in dd<el> that extends beyond just event handling.
|
||||
Addons are a powerful pattern in dd<el> that extends beyond just event handling.
|
||||
An Addon is any function that accepts an HTML element as its first parameter.
|
||||
`),
|
||||
el("div", { className: "callout" }).append(
|
||||
@ -152,11 +151,12 @@ export function page({ pkg, info }){
|
||||
|
||||
el(h3, t`Lifecycle Events`),
|
||||
el("p").append(...T`
|
||||
Addons are called immediately when an element is created, even before it's connected to the live DOM.
|
||||
You can think of an Addon as an "oncreate" event handler.
|
||||
Addons are called immediately when an element is created, even before it’s connected to the live DOM.
|
||||
You can think of an Addon as an "oncreate" event handler.
|
||||
`),
|
||||
el("p").append(...T`
|
||||
dd<el> provides three additional lifecycle events that correspond to custom element lifecycle callbacks:
|
||||
dd<el> provides two additional lifecycle events that correspond to ${el("a", { textContent:
|
||||
"custom element", ...references.mdn_customElements })} lifecycle callbacks:
|
||||
`),
|
||||
el("div", { className: "function-table" }).append(
|
||||
el("dl").append(
|
||||
@ -165,31 +165,30 @@ export function page({ pkg, info }){
|
||||
|
||||
el("dt", t`on.disconnected(callback)`),
|
||||
el("dd", t`Fires when the element is removed from the DOM`),
|
||||
|
||||
el("dt", t`on.attributeChanged(callback, attributeName)`),
|
||||
el("dd", t`Fires when the specified attribute changes`)
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/events/live-cycle.js"), page_id }),
|
||||
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(...T`
|
||||
For regular elements (non-custom elements), dd<el> uses
|
||||
${el("a", references.mdn_mutation).append(el("code", "MutationObserver"), " | MDN")}
|
||||
internally to track lifecycle events.
|
||||
For regular elements (non-custom elements), dd<el> uses ${el("a",
|
||||
references.mdn_mutation).append(el("code", "MutationObserver"), " | MDN")} internally to track
|
||||
lifecycle events.
|
||||
`)
|
||||
),
|
||||
|
||||
el("div", { className: "warning" }).append(
|
||||
el("ul").append(
|
||||
el("li").append(...T`
|
||||
Always use ${el("code", "on.*")} functions, not ${el("code", "on('dde:*', ...)")}, for proper registration
|
||||
Always use ${el("code", "on.*")} functions as library must ensure proper (MutationObserver)
|
||||
registration, not ${el("code", "on('dde:*', ...)")}, even the native event system is used with event
|
||||
names prefixed with ${el("code", "dde:")}.
|
||||
`),
|
||||
el("li").append(...T`
|
||||
Use lifecycle events sparingly, as they require internal tracking
|
||||
`),
|
||||
el("li").append(...T`
|
||||
Leverage parent-child relationships: when a parent is removed, all children are also removed
|
||||
Leverage parent-child relationships: when a parent is removed, all children are also removed
|
||||
`),
|
||||
el("li").append(...T`
|
||||
…see section later in documentation regarding hosts elements
|
||||
@ -202,11 +201,11 @@ export function page({ pkg, info }){
|
||||
|
||||
el(h3, t`Dispatching Custom Events`),
|
||||
el("p").append(...T`
|
||||
This makes it easy to implement component communication through events,
|
||||
following standard web platform patterns. The curried approach allows for easy reuse
|
||||
of event dispatchers throughout your application.
|
||||
This makes it easy to implement component communication through events, following standard web platform
|
||||
patterns. The curried approach allows for easy reuse of event dispatchers throughout your application.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/events/compareDispatch.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/events/dispatch.js"), page_id }),
|
||||
|
||||
el(h3, t`Best Practices`),
|
||||
el("ol").append(
|
||||
@ -217,7 +216,8 @@ export function page({ pkg, info }){
|
||||
${el("strong", "Leverage lifecycle events")}: For component setup and teardown
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Delegate when possible")}: Add listeners to container elements when handling many similar elements
|
||||
${el("strong", "Delegate when possible")}: Add listeners to container elements when handling many
|
||||
similar elements
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Maintain consistency")}: Choose one event binding approach and stick with it
|
||||
|
@ -48,7 +48,7 @@ export function page({ pkg, info }){
|
||||
Signals provide a simple yet powerful way to create reactive applications with dd<el>. They handle the
|
||||
fundamental challenge of keeping your UI in sync with changing data in a declarative, efficient way.
|
||||
`),
|
||||
el("div", { class: "callout" }).append(
|
||||
el("div", { className: "callout" }).append(
|
||||
el("h4", t`What Makes Signals Special?`),
|
||||
el("ul").append(
|
||||
el("li", t`Fine-grained reactivity without complex state management`),
|
||||
@ -65,18 +65,18 @@ export function page({ pkg, info }){
|
||||
Signals organize your code into three distinct parts, following the
|
||||
${el("a", { textContent: t`3PS principle`, href: "./#h-3ps" })}:
|
||||
`),
|
||||
el("div", { class: "signal-diagram" }).append(
|
||||
el("div", { class: "signal-part" }).append(
|
||||
el("div", { className: "signal-diagram" }).append(
|
||||
el("div", { className: "signal-part" }).append(
|
||||
el("h4", t`PART 1: Create Signal`),
|
||||
el(code, { content: "const count = S(0);", page_id }),
|
||||
el("p", t`Define a reactive value that can be observed and changed`)
|
||||
el("p", t`Define a reactive value that can be observed and changed`)
|
||||
),
|
||||
el("div", { class: "signal-part" }).append(
|
||||
el("div", { className: "signal-part" }).append(
|
||||
el("h4", t`PART 2: React to Changes`),
|
||||
el(code, { content: "S.on(count, value => updateUI(value));", page_id }),
|
||||
el("p", t`Subscribe to signal changes with callbacks or effects`)
|
||||
),
|
||||
el("div", { class: "signal-part" }).append(
|
||||
el("div", { className: "signal-part" }).append(
|
||||
el("h4", t`PART 3: Update Signal`),
|
||||
el(code, { content: "count.set(count.get() + 1);", page_id }),
|
||||
el("p", t`Modify the signal value, which automatically triggers updates`)
|
||||
@ -84,26 +84,26 @@ export function page({ pkg, info }){
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/signals/signals.js"), page_id }),
|
||||
|
||||
el("div", { class: "note" }).append(
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(...T`
|
||||
Signals implement the ${el("a", { textContent: t`Publish–subscribe pattern`, ...references.wiki_pubsub })},
|
||||
a form of ${el("a", { textContent: t`Event-driven programming`, ...references.wiki_event_driven })}.
|
||||
This architecture allows different parts of your application to stay synchronized through a shared signal,
|
||||
without direct dependencies on each other. Compare for example with ${el("a", { textContent:
|
||||
t`fpubsub library`, ...references.fpubsub })}.
|
||||
Signals implement the ${el("a", { textContent: t`Publish–subscribe pattern`, ...references.wiki_pubsub
|
||||
})}, a form of ${el("a", { textContent: t`Event-driven programming`, ...references.wiki_event_driven
|
||||
})}. This architecture allows different parts of your application to stay synchronized through
|
||||
a shared signal, without direct dependencies on each other. Compare for example with ${el("a", {
|
||||
textContent: t`fpubsub library`, ...references.fpubsub })}.
|
||||
`)
|
||||
),
|
||||
|
||||
el(h3, t`Signal Essentials: Core API`),
|
||||
el("div", { class: "function-table" }).append(
|
||||
el("div", { className: "function-table" }).append(
|
||||
el("dl").append(
|
||||
el("dt", t`Creating a Signal`),
|
||||
el("dt", t`Creating a Signal`),
|
||||
el("dd", t`S(initialValue) → creates a signal with the given value`),
|
||||
|
||||
el("dt", t`Reading a Signal`),
|
||||
el("dt", t`Reading a Signal`),
|
||||
el("dd", t`signal.get() → returns the current value`),
|
||||
|
||||
el("dt", t`Writing to a Signal`),
|
||||
el("dt", t`Writing to a Signal`),
|
||||
el("dd", t`signal.set(newValue) → updates the value and notifies subscribers`),
|
||||
|
||||
el("dt", t`Subscribing to Changes`),
|
||||
@ -115,51 +115,53 @@ export function page({ pkg, info }){
|
||||
)
|
||||
),
|
||||
el("p").append(...T`
|
||||
Signals can be created with any type of value, but they work best with
|
||||
${el("a", { textContent: t`primitive types`, ...references.mdn_primitive })} like strings, numbers, and booleans.
|
||||
For complex data types like objects and arrays, you'll want to use Actions (covered below).
|
||||
Signals can be created with any type of value, but they work best with ${el("a", { textContent:
|
||||
t`primitive types`, ...references.mdn_primitive })} like strings, numbers, and booleans. For complex
|
||||
data types like objects and arrays, you’ll want to use Actions (covered below).
|
||||
`),
|
||||
|
||||
el(h3, t`Derived Signals: Computed Values`),
|
||||
el("p").append(...T`
|
||||
Computed values (also called derived signals) automatically update when their dependencies change.
|
||||
Create them by passing a function to S():
|
||||
Create them by passing ${el("strong", "a function")} to ${el("code", "S()")}:
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/signals/derived.js"), page_id }),
|
||||
el("p").append(...T`
|
||||
Derived signals are read-only - you can't call .set() on them. Their value is always computed
|
||||
from their dependencies. They're perfect for transforming or combining data from other signals.
|
||||
Derived signals are read-only - you can’t call ${el("code", ".set()")} on them. Their value is always
|
||||
computed from their dependencies. They’re perfect for transforming or combining data from other signals.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/signals/computations-abort.js"), page_id }),
|
||||
|
||||
el(h3, t`Signal Actions: For Complex State`),
|
||||
el("p").append(...T`
|
||||
When working with objects, arrays, or other complex data structures, Signal Actions provide
|
||||
a structured way to modify state while maintaining reactivity.
|
||||
When working with objects, arrays, or other complex data structures. Signal Actions provide
|
||||
a structured way to modify state while maintaining reactivity.
|
||||
`),
|
||||
el("div", { class: "illustration" }).append(
|
||||
el("div", { className: "illustration" }).append(
|
||||
el("h4", t`Actions vs. Direct Mutation`),
|
||||
el("div", { class: "comparison" }).append(
|
||||
el("div", { class: "good-practice" }).append(
|
||||
el("div", { className: "comparison" }).append(
|
||||
el("div", { className: "good-practice" }).append(
|
||||
el("h5", t`✅ With Actions`),
|
||||
el(code, { content: `const todos = S([], {
|
||||
add(text) {
|
||||
this.value.push(text);
|
||||
// Subscribers notified automatically
|
||||
}
|
||||
});
|
||||
|
||||
// Use the action
|
||||
S.action(todos, "add", "New todo");`, page_id })
|
||||
el(code, { content: `
|
||||
const todos = S([], {
|
||||
add(text) {
|
||||
this.value.push(text);
|
||||
// Subscribers notified automatically
|
||||
}
|
||||
});
|
||||
// Use the action
|
||||
S.action(todos, "add", "New todo");
|
||||
`, page_id })
|
||||
),
|
||||
el("div", { class: "bad-practice" }).append(
|
||||
el("div", { className: "bad-practice" }).append(
|
||||
el("h5", t`❌ Without Actions`),
|
||||
el(code, { content: `
|
||||
const todos = S([]);
|
||||
// Directly mutating the array
|
||||
const items = todos.get();
|
||||
items.push("New todo");
|
||||
// This WON'T trigger updates!`, page_id }))
|
||||
const todos = S([]);
|
||||
// Directly mutating the array
|
||||
const items = todos.get();
|
||||
items.push("New todo");
|
||||
// This WON’T trigger updates!
|
||||
`, page_id }))
|
||||
),
|
||||
),
|
||||
el("p").append(...T`
|
||||
@ -182,16 +184,19 @@ items.push("New todo");
|
||||
el("li", t`Act similar to reducers in other state management libraries`)
|
||||
),
|
||||
el("p").append(...T`
|
||||
Here's a more complete example of a todo list using signal actions:
|
||||
Here’s a more complete example of a todo list using signal actions:
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/signals/actions-todos.js"), page_id }),
|
||||
|
||||
el("div", { class: "tip" }).append(
|
||||
el("div", { className: "tip" }).append(
|
||||
el("p").append(...T`
|
||||
${el("strong", "Special Action Methods")}: Signal actions can implement special lifecycle hooks:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li", t`[S.symbols.onclear]() - Called when the signal is cleared. Use it to clean up resources.`),
|
||||
el("li").append(...T`
|
||||
${el("code", "[S.symbols.onclear]()")} - Called when the signal is cleared. Use it to clean up
|
||||
resources.
|
||||
`),
|
||||
)
|
||||
),
|
||||
|
||||
@ -200,39 +205,43 @@ items.push("New todo");
|
||||
Signals really shine when connected to your UI. dd<el> provides several ways to bind signals to DOM elements:
|
||||
`),
|
||||
|
||||
el("div", { class: "tabs" }).append(
|
||||
el("div", { class: "tab", "data-tab": "attributes" }).append(
|
||||
el("div", { className: "tabs" }).append(
|
||||
el("div", { className: "tab", dataTab: "attributes" }).append(
|
||||
el("h4", t`Reactive Attributes`),
|
||||
el("p", t`Bind signal values directly to element attributes, properties, or styles:`),
|
||||
el(code, { content: `// Create a signal
|
||||
const color = S("blue");
|
||||
el(code, { content: `
|
||||
// Create a signal
|
||||
const color = S("blue");
|
||||
|
||||
// Bind it to an element's style
|
||||
el("div", {
|
||||
style: {
|
||||
color, // Updates when signal changes
|
||||
fontWeight: S(() => color.get() === "red" ? "bold" : "normal")
|
||||
}
|
||||
}, "This text changes color");
|
||||
// Bind it to an element’s style
|
||||
el("div", {
|
||||
style: {
|
||||
color, // Updates when signal changes
|
||||
fontWeight: S(() => color.get() === "red" ? "bold" : "normal")
|
||||
}
|
||||
}, "This text changes color");
|
||||
|
||||
// Later:
|
||||
color.set("red"); // UI updates automatically`, page_id })
|
||||
// Later:
|
||||
color.set("red"); // UI updates automatically
|
||||
`, page_id }),
|
||||
),
|
||||
el("div", { class: "tab", "data-tab": "elements" }).append(
|
||||
el("div", { className: "tab", dataTab: "elements" }).append(
|
||||
el("h4", t`Reactive Elements`),
|
||||
el("p", t`Dynamically create or update elements based on signal values:`),
|
||||
el(code, { content: `// Create an array signal
|
||||
const items = S(["Apple", "Banana", "Cherry"]);
|
||||
el(code, { content: `
|
||||
// Create an array signal
|
||||
const items = S(["Apple", "Banana", "Cherry"]);
|
||||
|
||||
// Create a dynamic list that updates when items change
|
||||
el("ul").append(
|
||||
S.el(items, items =>
|
||||
items.map(item => el("li", item))
|
||||
)
|
||||
);
|
||||
// Create a dynamic list that updates when items change
|
||||
el("ul").append(
|
||||
S.el(items, items =>
|
||||
items.map(item => el("li", item))
|
||||
)
|
||||
);
|
||||
|
||||
// Later:
|
||||
S.action(items, "push", "Dragonfruit"); // List updates automatically`, page_id })
|
||||
// Later:
|
||||
S.action(items, "push", "Dragonfruit"); // List updates automatically
|
||||
`, page_id }),
|
||||
)
|
||||
),
|
||||
|
||||
@ -254,23 +263,24 @@ S.action(items, "push", "Dragonfruit"); // List updates automatically`, page_id
|
||||
`),
|
||||
el("ol").append(
|
||||
el("li").append(...T`
|
||||
${el("strong", "Keep signals small and focused")}: Use many small signals rather than a few large ones
|
||||
${el("strong", "Keep signals small and focused")}: Use many small signals rather than a few large ones
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Use derived signals for computations")}: Don't recompute values in multiple places
|
||||
${el("strong", "Use derived signals for computations")}: Don’t recompute values in multiple places
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Clean up signal subscriptions")}: Use AbortController or scope.host() to prevent memory leaks
|
||||
${el("strong", "Clean up signal subscriptions")}: Use AbortController (scope.host()) to prevent memory
|
||||
leaks
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Use actions for complex state")}: Don't directly mutate objects or arrays in signals
|
||||
${el("strong", "Use actions for complex state")}: Don’t directly mutate objects or arrays in signals
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Avoid infinite loops")}: Be careful when one signal updates another in a subscription
|
||||
${el("strong", "Avoid infinite loops")}: Be careful when one signal updates another in a subscription
|
||||
`)
|
||||
),
|
||||
|
||||
el("div", { class: "troubleshooting" }).append(
|
||||
el("div", { className: "troubleshooting" }).append(
|
||||
el("h4", t`Common Signal Pitfalls`),
|
||||
el("dl").append(
|
||||
el("dt", t`UI not updating when array/object changes`),
|
||||
|
@ -10,7 +10,7 @@ import { simplePage } from "./layout/simplePage.html.js";
|
||||
import { example } from "./components/example.html.js";
|
||||
import { h3 } from "./components/pageUtils.html.js";
|
||||
import { mnemonic } from "./components/mnemonic/scopes-init.js";
|
||||
import { code } from "./components/code.html.js";
|
||||
import { code, pre } from "./components/code.html.js";
|
||||
/** @param {string} url */
|
||||
const fileURL= url=> new URL(url, import.meta.url);
|
||||
const references= {
|
||||
@ -31,7 +31,7 @@ export function page({ pkg, info }){
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(...T`
|
||||
For state-less components we can use functions as UI components (see “Elements” page). But in real life,
|
||||
we may need to handle the component's life-cycle and provide JavaScript the way to properly use
|
||||
we may need to handle the component’s life-cycle and provide JavaScript the way to properly use
|
||||
the ${el("a", { textContent: t`Garbage collection`, ...references.garbage_collection })}.
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/scopes/intro.js"), page_id }),
|
||||
@ -40,56 +40,56 @@ export function page({ pkg, info }){
|
||||
el(h3, t`Understanding Host Elements and Scopes`),
|
||||
el("p").append(...T`
|
||||
The ${el("strong", "host")} is the name for the element representing the component. This is typically the
|
||||
element returned by a function. To get a reference, you can use ${el("code", "scope.host()")}. To apply addons,
|
||||
element returned by a function. To get a reference, you can use ${el("code", "scope.host()")}. To apply addons,
|
||||
just use ${el("code", "scope.host(...<addons>)")}.
|
||||
`),
|
||||
el("p").append(...T`
|
||||
Scopes are primarily needed when signals are used in DOM templates (with ${el("code", "el")}, ${el("code",
|
||||
"assign")}, or ${el("code", "S.el")}). They provide a way for automatically removing signal listeners
|
||||
"assign")}, or ${el("code", "S.el")}). They provide a way for automatically removing signal listeners
|
||||
and cleaning up unused signals when components are removed from the DOM.
|
||||
`),
|
||||
el("div", { className: "illustration" }).append(
|
||||
el("h4", t`Component Anatomy`),
|
||||
el("pre").append(el("code", `
|
||||
// 1. Component scope created
|
||||
el(MyComponent);
|
||||
el(pre, { content: `
|
||||
// 1. Component scope created
|
||||
el(MyComponent);
|
||||
|
||||
function MyComponent() {
|
||||
// 2. access the host element
|
||||
const { host } = scope;
|
||||
function MyComponent() {
|
||||
// 2. access the host element
|
||||
const { host } = scope;
|
||||
|
||||
// 3. Add behavior to host
|
||||
host(
|
||||
on.click(handleClick)
|
||||
);
|
||||
// 3. Add behavior to host
|
||||
host(
|
||||
on.click(handleClick)
|
||||
);
|
||||
|
||||
// 4. Return the host element
|
||||
return el("div", {
|
||||
className: "my-component"
|
||||
}).append(
|
||||
el("h2", "Title"),
|
||||
el("p", "Content")
|
||||
);
|
||||
}
|
||||
`.trim()))
|
||||
// 4. Return the host element
|
||||
return el("div", {
|
||||
className: "my-component"
|
||||
}).append(
|
||||
el("h2", "Title"),
|
||||
el("p", "Content")
|
||||
);
|
||||
}
|
||||
` })
|
||||
),
|
||||
el("div", { className: "function-table" }).append(
|
||||
el("h4", t`scope.host()`),
|
||||
el("dl").append(
|
||||
el("dt", t`When called with no arguments`),
|
||||
el("dd", t`Returns a reference to the host element (the root element of your component)`),
|
||||
el("dd", t`Returns a reference to the host element (the root element of your component)`),
|
||||
|
||||
el("dt", t`When called with addons/callbacks`),
|
||||
el("dd", t`Applies the addons to the host element and returns the host element`)
|
||||
el("dd", t`Applies the addons to the host element (and returns the host element)`)
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/scopes/scopes-and-hosts.js"), page_id }),
|
||||
|
||||
el("div", { className: "tip" }).append(
|
||||
el("p").append(...T`
|
||||
${el("strong", "Best Practice:")} Always capture the host reference at the beginning of your component
|
||||
function using ${el("code", "const { host } = scope")} to avoid scope-related issues, especially with
|
||||
asynchronous code.
|
||||
${el("strong", "Best Practice:")} Always capture the host reference (or other scope related values) at
|
||||
the beginning of your component function using ${el("code", "const { host } = scope")} to avoid
|
||||
scope-related issues, especially with ${el("em", "asynchronous code")}.
|
||||
`),
|
||||
el("p").append(...T`
|
||||
If you are interested in the implementation details, see Class-Based Components section.
|
||||
@ -112,31 +112,32 @@ function MyComponent() {
|
||||
`),
|
||||
el("div", { className: "illustration" }).append(
|
||||
el("h4", t`Lifecycle Flow`),
|
||||
el("pre").append(el("code", `
|
||||
1. Component created → scope established
|
||||
2. Component add<el> to DOM → connected event
|
||||
3. Component interactions happen
|
||||
4. Component removed from DOM → disconnected event
|
||||
5. Automatic cleanup of:
|
||||
- Event listeners
|
||||
- Signal subscriptions
|
||||
- Custom cleanup code
|
||||
`))
|
||||
el(pre, { content: `
|
||||
1. Component created → scope established
|
||||
2. Component added to DOM → connected event
|
||||
3. Component interactions happen
|
||||
4. Component removed from DOM → disconnected event
|
||||
5. Automatic cleanup of:
|
||||
- Event listeners (browser)
|
||||
- Signal subscriptions (dd<el> and browser)
|
||||
- Custom cleanup code (dd<el> and user)
|
||||
` })
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/scopes/cleaning.js"), page_id }),
|
||||
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(...T`
|
||||
In this example, when you click "Remove", the component is removed from the DOM, and all its associated
|
||||
resources are automatically cleaned up, including the signal subscription that updates the text content.
|
||||
This happens because the library internally registers a disconnected event handler on the host element.
|
||||
resources are automatically cleaned up, including ${el("em",
|
||||
"the signal subscription that updates the text content")}. This happens because the library
|
||||
internally registers a disconnected event handler on the host element.
|
||||
`)
|
||||
),
|
||||
|
||||
el(h3, t`Declarative vs Imperative Components`),
|
||||
el("p").append(...T`
|
||||
The library DOM API and signals work best when used declaratively. It means you split your app's logic
|
||||
into three parts as introduced in ${el("a", { textContent: "Signals", ...references.signals })}.
|
||||
The library DOM API and signals work best when used declaratively. It means you split your app’s logic
|
||||
into three parts as introduced in ${el("a", { textContent: "Signals (3PS)", ...references.signals })}.
|
||||
`),
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(...T`
|
||||
@ -145,17 +146,17 @@ function MyComponent() {
|
||||
`)
|
||||
),
|
||||
el("div", { className: "tabs" }).append(
|
||||
el("div", { className: "tab", "data-tab": "declarative" }).append(
|
||||
el("div", { className: "tab", dataTab: "declarative" }).append(
|
||||
el("h4", t`✅ Declarative Approach`),
|
||||
el("p", t`Define what your UI should look like based on state:`),
|
||||
el(code, { src: fileURL("./components/examples/scopes/declarative.js"), page_id })
|
||||
),
|
||||
el("div", { className: "tab", "data-tab": "imperative" }).append(
|
||||
el("div", { className: "tab", dataTab: "imperative" }).append(
|
||||
el("h4", t`⚠️ Imperative Approach`),
|
||||
el("p", t`Manually update the DOM in response to events:`),
|
||||
el(code, { src: fileURL("./components/examples/scopes/imperative.js"), page_id })
|
||||
),
|
||||
el("div", { className: "tab", "data-tab": "mixed" }).append(
|
||||
el("div", { className: "tab", dataTab: "mixed" }).append(
|
||||
el("h4", t`❌ Mixed Approach`),
|
||||
el("p", t`This approach should be avoided:`),
|
||||
el(code, { src: fileURL("./components/examples/scopes/mixed.js"), page_id })
|
||||
@ -171,7 +172,8 @@ function MyComponent() {
|
||||
${el("strong", "Define signals as constants:")} ${el("code", "const counter = S(0);")}
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Prefer declarative patterns:")} Use signals to drive UI updates rather than manual DOM manipulation
|
||||
${el("strong", "Prefer declarative patterns:")} Use signals to drive UI updates rather than manual DOM
|
||||
manipulation
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Keep components focused:")} Each component should do one thing well
|
||||
@ -195,7 +197,7 @@ function MyComponent() {
|
||||
el("dd", t`Use arrow functions or .bind() to preserve context`),
|
||||
|
||||
el("dt", t`Mixing declarative and imperative styles`),
|
||||
el("dd", t`Choose one approach and be consistent throughout a component`)
|
||||
el("dd", t`Choose one approach and be consistent throughout a component(s)`)
|
||||
)
|
||||
),
|
||||
|
||||
|
@ -10,7 +10,7 @@ import { simplePage } from "./layout/simplePage.html.js";
|
||||
import { example } from "./components/example.html.js";
|
||||
import { h3 } from "./components/pageUtils.html.js";
|
||||
import { mnemonic } from "./components/mnemonic/customElement-init.js";
|
||||
import { code } from "./components/code.html.js";
|
||||
import { code, pre } from "./components/code.html.js";
|
||||
/** @param {string} url */
|
||||
const fileURL= url=> new URL(url, import.meta.url);
|
||||
const references= {
|
||||
@ -49,6 +49,11 @@ const references= {
|
||||
title: t`Everything you need to know about Shadow DOM (github repo praveenpuglia/shadow-dom-in-depth)`,
|
||||
href: "https://github.com/praveenpuglia/shadow-dom-in-depth",
|
||||
},
|
||||
/** Decorators */
|
||||
decorators: {
|
||||
title: t`JavaScript Decorators: An In-depth Guide`,
|
||||
href: "https://www.sitepoint.com/javascript-decorators-what-they-are/",
|
||||
}
|
||||
};
|
||||
/** @param {import("./types.d.ts").PageAttrs} attrs */
|
||||
export function page({ pkg, info }){
|
||||
@ -56,7 +61,7 @@ export function page({ pkg, info }){
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(...T`
|
||||
dd<el> pairs powerfully with ${el("a", references.mdn_web_components).append(el("strong", t`Web
|
||||
Components`))} to create reusable, encapsulated custom elements with all the benefits of dd<el>'s
|
||||
Components`))} to create reusable, encapsulated custom elements with all the benefits of dd<el>’s
|
||||
declarative DOM construction and reactivity system.
|
||||
`),
|
||||
el("div", { className: "callout" }).append(
|
||||
@ -66,14 +71,13 @@ export function page({ pkg, info }){
|
||||
el("li", t`Reactive attribute updates through signals`),
|
||||
el("li", t`Simplified event handling with the same events API`),
|
||||
el("li", t`Clean component lifecycle management`),
|
||||
el("li", t`Improved code organization with scopes`)
|
||||
)
|
||||
),
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/customElement/intro.js"), page_id }),
|
||||
|
||||
el(h3, t`Getting Started: Web Components Basics`),
|
||||
el("p").append(...T`
|
||||
Web Components are a set of standard browser APIs that let you create custom HTML elements with
|
||||
Web Components are a set of standard browser APIs that let you create custom HTML elements with
|
||||
encapsulated functionality. They consist of three main technologies:
|
||||
`),
|
||||
el("ul").append(
|
||||
@ -81,14 +85,15 @@ export function page({ pkg, info }){
|
||||
${el("strong", "Custom Elements:")} Create your own HTML tags with JS-defined behavior
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Shadow DOM:")} Encapsulate styles and markup within a component
|
||||
${el("strong", "Shadow DOM:")} Encapsulate styles and markup within a component
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "HTML Templates:")} Define reusable markup structures
|
||||
${el("strong", "HTML Templates:")} Define reusable markup structures (${el("em",
|
||||
"the dd<el> replaces this part")})
|
||||
`)
|
||||
),
|
||||
el("p").append(...T`
|
||||
Let's start with a basic Custom Element example without dd<el> to establish the foundation:
|
||||
Let’s start with a basic Custom Element example without dd<el> to establish the foundation:
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/customElement/native-basic.js"), page_id }),
|
||||
|
||||
@ -103,19 +108,20 @@ export function page({ pkg, info }){
|
||||
|
||||
el(h3, t`dd<el> Integration: Step 1 - Event Handling`),
|
||||
el("p").append(...T`
|
||||
The first step in integrating dd<el> with Web Components is enabling dd<el>'s event system to work with your
|
||||
The first step in integrating dd<el> with Web Components is enabling dd<el>’s event system to work with your
|
||||
Custom Elements. This is done with ${el("code", "customElementWithDDE")}, which makes your Custom Element
|
||||
compatible with dd<el>'s event handling.
|
||||
compatible with dd<el>’s event handling. (${el("em").append(...T`Notice that customElementWithDDE is
|
||||
actually`)} ${el("a", { textContent: "decorator", ...references.decorators })})
|
||||
`),
|
||||
el("div", { className: "function-table" }).append(
|
||||
el("h4", t`customElementWithDDE`),
|
||||
el("dl").append(
|
||||
el("dt", t`Purpose`),
|
||||
el("dd", t`Enables dd<el>'s event system to work with your Custom Element`),
|
||||
el("dd", t`Enables dd<el>’s event system to work with your Custom Element`),
|
||||
el("dt", t`Usage`),
|
||||
el("dd", t`customElementWithDDE(YourElementClass)`),
|
||||
el("dt", t`Benefits`),
|
||||
el("dd", t`Allows using on.connected(), on.disconnected(), etc. with your element`)
|
||||
el("dd", t`Allows using on.connected(), on.disconnected() or S.observedAttributes().`)
|
||||
)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/customElement/customElementWithDDE.js"), page_id }),
|
||||
@ -123,26 +129,27 @@ export function page({ pkg, info }){
|
||||
el("div", { className: "tip" }).append(
|
||||
el("p").append(...T`
|
||||
${el("strong", "Key Point:")} The ${el("code", "customElementWithDDE")} function adds event dispatching
|
||||
to your Custom Element lifecycle methods, making them work seamlessly with dd<el>'s event system.
|
||||
to your Custom Element lifecycle methods, making them work seamlessly with dd<el>’s event system.
|
||||
`)
|
||||
),
|
||||
|
||||
el(h3, t`dd<el> Integration: Step 2 - Rendering Components`),
|
||||
el("p").append(...T`
|
||||
The next step is to use dd<el>'s component rendering within your Custom Element. This is done with
|
||||
The next step is to use dd<el>’s component rendering within your Custom Element. This is done with
|
||||
${el("code", "customElementRender")}, which connects your dd<el> component function to the Custom Element.
|
||||
`),
|
||||
el("div", { className: "function-table" }).append(
|
||||
el("h4", t`customElementRender`),
|
||||
el("dl").append(
|
||||
el("dt", t`Purpose`),
|
||||
el("dd", t`Connects a dd<el> component function to a Custom Element`),
|
||||
el("dd", t`Connects a dd<el> component function to a Custom Element`),
|
||||
el("dt", t`Parameters`),
|
||||
el("dd").append(
|
||||
el("ol").append(
|
||||
el("li", t`Target (usually this or this.shadowRoot)`),
|
||||
el("li", t`Component function that returns a DOM tree`),
|
||||
el("li", t`Optional: Attributes transformer function (default or S.observedAttributes)`)
|
||||
el("li", t`Component function that returns a DOM tree`),
|
||||
el("li", t`Optional: Attributes transformer function (empty by default or
|
||||
S.observedAttributes)`)
|
||||
)
|
||||
),
|
||||
el("dt", t`Returns`),
|
||||
@ -153,7 +160,7 @@ export function page({ pkg, info }){
|
||||
|
||||
el("div", { className: "note" }).append(
|
||||
el("p").append(...T`
|
||||
In this example, we're using Shadow DOM (${el("code", "this.attachShadow()")}) for encapsulation,
|
||||
In this example, we’re using Shadow DOM (${el("code", "this.attachShadow()")}) for encapsulation,
|
||||
but you can also render directly to the element with ${el("code", "customElementRender(this, ...)")}.
|
||||
`)
|
||||
),
|
||||
@ -161,7 +168,7 @@ export function page({ pkg, info }){
|
||||
el(h3, t`Reactive Web Components with Signals`),
|
||||
el("p").append(...T`
|
||||
One of the most powerful features of integrating dd<el> with Web Components is connecting HTML attributes
|
||||
to dd<el>'s reactive signals system. This creates truly reactive custom elements.
|
||||
to dd<el>’s reactive signals system. This creates truly reactive custom elements.
|
||||
`),
|
||||
el("div", { className: "tip" }).append(
|
||||
el("p").append(...T`
|
||||
@ -169,7 +176,8 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
el("ol").append(
|
||||
el("li").append(...T`
|
||||
${el("code", "observedAttributes")} - Passes attributes as regular values (static)
|
||||
Using standard attribute access (${el("code", "this.getAttribute(<name>)")}) - Passes attributes as
|
||||
regular values (static)
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("code", "S.observedAttributes")} - Transforms attributes into signals (reactive)
|
||||
@ -177,8 +185,8 @@ export function page({ pkg, info }){
|
||||
)
|
||||
),
|
||||
el("p").append(...T`
|
||||
Using ${el("code", "S.observedAttributes")} creates a reactive connection between your element's attributes
|
||||
and its internal rendering. When attributes change, your component automatically updates!
|
||||
Using the ${el("code", "S.observedAttributes")} creates a reactive connection between your element’s
|
||||
attributes and its internal rendering. When attributes change, your component automatically updates!
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/customElement/observedAttributes.js"), page_id }),
|
||||
|
||||
@ -186,31 +194,32 @@ export function page({ pkg, info }){
|
||||
el("h4", t`How S.observedAttributes Works`),
|
||||
el("ol").append(
|
||||
el("li", t`Takes each attribute listed in static observedAttributes`),
|
||||
el("li", t`Creates a dd<el> signal for each one`),
|
||||
el("li", t`Creates a dd<el> signal for each one`),
|
||||
el("li", t`Automatically updates these signals when attributes change`),
|
||||
el("li", t`Passes the signals to your component function`),
|
||||
el("li", t`In opposite, updates of signals trigger attribute changes`),
|
||||
el("li", t`Your component reacts to changes through signal subscriptions`)
|
||||
)
|
||||
),
|
||||
|
||||
el(h3, t`Working with Shadow DOM`),
|
||||
el("p").append(...T`
|
||||
Shadow DOM provides encapsulation for your component's styles and markup. When using dd<el> with Shadow DOM,
|
||||
Shadow DOM provides encapsulation for your component’s styles and markup. When using dd<el> with Shadow DOM,
|
||||
you get the best of both worlds: encapsulation plus declarative DOM creation.
|
||||
`),
|
||||
el("div", { className: "illustration" }).append(
|
||||
el("h4", t`Shadow DOM Encapsulation`),
|
||||
el("pre").append(el("code", `
|
||||
<my-custom-element>
|
||||
┌─────────────────────────┐
|
||||
#shadow-root
|
||||
el(pre, { content: `
|
||||
<my-custom-element>
|
||||
┌─────────────────────────┐
|
||||
#shadow-root
|
||||
|
||||
Created with dd<el>
|
||||
┌──────────────────┐
|
||||
<div>
|
||||
<h2>Title</h2>
|
||||
<p>Content</p>
|
||||
`))
|
||||
Created with dd<el>
|
||||
┌──────────────────┐
|
||||
<div>
|
||||
<h2>Title</h2>
|
||||
<p>Content</p>
|
||||
` })
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/customElement/shadowRoot.js"), page_id }),
|
||||
|
||||
@ -232,7 +241,7 @@ export function page({ pkg, info }){
|
||||
el("dt", t`Purpose`),
|
||||
el("dd", t`Provides slot functionality when you cannot/do not want to use shadow DOM`),
|
||||
el("dt", t`Parameters`),
|
||||
el("dd", t`A mapping object of slot names to DOM elements`)
|
||||
el("dd", t`A mapping object of slot names to DOM elements`)
|
||||
)
|
||||
),
|
||||
|
||||
@ -264,7 +273,7 @@ export function page({ pkg, info }){
|
||||
el("dt", t`Events not firing properly`),
|
||||
el("dd", t`Make sure you called customElementWithDDE before defining the element`),
|
||||
el("dt", t`Attributes not updating`),
|
||||
el("dd", t`Check that you've properly listed them in static observedAttributes`),
|
||||
el("dd", t`Check that you’ve properly listed them in static observedAttributes`),
|
||||
el("dt", t`Component not rendering`),
|
||||
el("dd", t`Verify customElementRender is called in connectedCallback, not constructor`)
|
||||
)
|
||||
|
@ -19,7 +19,7 @@ export function page({ pkg, info }){
|
||||
return el(simplePage, { info, pkg }).append(
|
||||
el("p").append(...T`
|
||||
Debugging is an essential part of application development. This guide provides techniques
|
||||
and best practices for debugging applications built with dd<el>, with a focus on signals.
|
||||
and best practices for debugging applications built with dd<el>, with a focus on signals.
|
||||
`),
|
||||
|
||||
el(h3, t`Debugging signals`),
|
||||
@ -30,42 +30,40 @@ export function page({ pkg, info }){
|
||||
|
||||
el("h4", t`Inspecting signal values`),
|
||||
el("p").append(...T`
|
||||
The simplest way to debug a signal is to log its current value by calling the get method:
|
||||
The simplest way to debug a signal is to log its current value by calling the get method:
|
||||
`),
|
||||
el(code, { content: `
|
||||
const signal = S(0);
|
||||
console.log('Current value:', signal.get());
|
||||
// without triggering updates
|
||||
console.log('Current value:', signal.valueOf());
|
||||
`, page_id }),
|
||||
const signal = S(0);
|
||||
console.log('Current value:', signal.get());
|
||||
// without triggering updates
|
||||
console.log('Current value:', signal.valueOf());
|
||||
`, page_id }),
|
||||
el("p").append(...T`
|
||||
You can also monitor signal changes by adding a listener:
|
||||
You can also monitor signal changes by adding a listener:
|
||||
`),
|
||||
el(code, {
|
||||
content:
|
||||
"// Log every time the signal changes\nS.on(signal, value => console.log('Signal changed:', value));",
|
||||
page_id }),
|
||||
el(code, { content: `
|
||||
// Log every time the signal changes
|
||||
S.on(signal, value => console.log('Signal changed:', value));
|
||||
`, page_id }),
|
||||
|
||||
el("h4", t`Debugging derived signals`),
|
||||
el("p").append(...T`
|
||||
With derived signals (created with ${el("code", "S(() => computation))")}), debugging is a bit more complex
|
||||
because the value depends on other signals. To understand why a derived signal isn't updating correctly:
|
||||
With derived signals (created with ${el("code", "S(() => computation))")}), debugging is a bit more complex
|
||||
because the value depends on other signals. To understand why a derived signal isn’t updating correctly:
|
||||
`),
|
||||
el("ol").append(
|
||||
el("li", t`Check that all dependency signals are updating correctly`),
|
||||
el("li", t`Add logging inside the computation function to see when it runs`),
|
||||
el("li", t`Add logging/debugger inside the computation function to see when it runs`),
|
||||
el("li", t`Verify that the computation function actually accesses the signal values with .get()`)
|
||||
),
|
||||
el(example, { src: fileURL("./components/examples/debugging/consoleLog.js"), page_id }),
|
||||
|
||||
el(h3, t`Common signal debugging issues`),
|
||||
el("h4", t`Signal updates not triggering UI changes`),
|
||||
el("p").append(...T`
|
||||
If signal updates aren't reflected in the UI, check:
|
||||
`),
|
||||
el("p", t`If signal updates aren’t reflected in the UI, check:`),
|
||||
el("ul").append(
|
||||
el("li", t`That you're using signal.set() to update the value, not modifying objects/arrays directly`),
|
||||
el("li", t`For mutable objects, ensure you're using actions or making proper copies before updating`),
|
||||
el("li", t`That you’re using signal.set() to update the value, not modifying objects/arrays directly`),
|
||||
el("li", t`For mutable objects, ensure you’re using actions or making proper copies before updating`),
|
||||
el("li", t`That the signal is actually connected to the DOM element (check your S.el or attribute binding code)`)
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/debugging/mutations.js"), page_id }),
|
||||
@ -77,12 +75,10 @@ console.log('Current value:', signal.valueOf());
|
||||
`),
|
||||
|
||||
el("h4", t`Performance issues with frequently updating signals`),
|
||||
el("p").append(...T`
|
||||
If you notice performance issues with signals that update very frequently:
|
||||
`),
|
||||
el("p", t`If you notice performance issues with signals that update very frequently:`),
|
||||
el("ul").append(
|
||||
el("li", t`Consider debouncing or throttling signal updates`),
|
||||
el("li", t`Make sure derived signals don't perform expensive calculations unnecessarily`),
|
||||
el("li", t`Make sure derived signals don’t perform expensive calculations unnecessarily`),
|
||||
el("li", t`Keep signal computations focused and minimal`)
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/debugging/debouncing.js"), page_id }),
|
||||
@ -96,13 +92,13 @@ console.log('Current value:', signal.valueOf());
|
||||
el("p").append(...T`
|
||||
dd<el> marks components in the DOM with special comment nodes to help you identify component boundaries.
|
||||
Components created with ${el("code", "el(ComponentFunction)")} are marked with comment nodes
|
||||
${el("code", "<!--<dde:mark type=\"component\" name=\"MyComponent\" host=\"parentElement\"/>-->")} and
|
||||
${el("code", `<!--<dde:mark type="component" name="MyComponent" host="parentElement"/>-->`)} and
|
||||
includes:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li", "type - Identifies the type of marker (\"component\", \"reactive\", or \"later\")"),
|
||||
el("li", "name - The name of the component function"),
|
||||
el("li", "host - Indicates whether the host is \"this\" (for DocumentFragments) or \"parentElement\""),
|
||||
el("li", t`type - Identifies the type of marker ("component", "reactive", or "later")`),
|
||||
el("li", t`name - The name of the component function`),
|
||||
el("li", t`host - Indicates whether the host is "this" (for DocumentFragments) or "parentElement"`),
|
||||
),
|
||||
|
||||
el("h4", t`Finding reactive elements in the DOM`),
|
||||
@ -111,9 +107,9 @@ console.log('Current value:', signal.valueOf());
|
||||
that are automatically updated when signal values change. These elements are wrapped in special
|
||||
comment nodes for debugging (to be true they are also used internally, so please do not edit them by hand):
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/debugging/dom-reactive-mark.js"), page_id }),
|
||||
el(code, { src: fileURL("./components/examples/debugging/dom-reactive-mark.html"), page_id }),
|
||||
el("p").append(...T`
|
||||
This is particularly useful when debugging why a reactive section isn't updating as expected.
|
||||
This is particularly useful when debugging why a reactive section isn’t updating as expected.
|
||||
You can inspect the elements between the comment nodes to see their current state and the
|
||||
signal connections through \`__dde_reactive\` of the host element.
|
||||
`),
|
||||
@ -124,48 +120,49 @@ console.log('Current value:', signal.valueOf());
|
||||
`),
|
||||
el("p").append(...T`
|
||||
${el("code", "<element>.__dde_reactive")} - An array property on DOM elements that tracks signal-to-element
|
||||
relationships. This allows you to quickly identify which elements are reactive and what signals they're
|
||||
bound to. Each entry in the array contains:
|
||||
relationships. This allows you to quickly identify which elements are reactive and what signals they’re
|
||||
bound to. Each entry in the array contains:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li", "A pair of signal and listener function: [signal, listener]"),
|
||||
el("li", "Additional context information about the element or attribute"),
|
||||
el("li", "Automatically managed by signal.el(), signal.observedAttributes(), and processReactiveAttribute()")
|
||||
el("li", t`A pair of signal and listener function: [signal, listener]`),
|
||||
el("li", t`Additional context information about the element or attribute`),
|
||||
el("li", t`Automatically managed by signal.el(), signal.observedAttributes(), and processReactiveAttribute()`)
|
||||
),
|
||||
el("p").append(...T`
|
||||
These properties make it easier to understand the reactive structure of your application when inspecting elements.
|
||||
These properties make it easier to understand the reactive structure of your application when inspecting
|
||||
elements.
|
||||
`),
|
||||
el(example, { src: fileURL("./components/examples/signals/debugging-dom.js"), page_id }),
|
||||
|
||||
el("h4", t`Examining signal connections`),
|
||||
el("p").append(...T`
|
||||
${el("code", "<signal>.__dde_signal")} - A Symbol property used to identify and store the internal state of
|
||||
${el("code", "<signal>.__dde_signal")} - A Symbol property used to identify and store the internal state of
|
||||
signal objects. It contains the following information:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li", "listeners: A Set of functions called when the signal value changes"),
|
||||
el("li", "actions: Custom actions that can be performed on the signal"),
|
||||
el("li", "onclear: Functions to run when the signal is cleared"),
|
||||
el("li", "host: Reference to the host element/scope"),
|
||||
el("li", "defined: Stack trace information for debugging"),
|
||||
el("li", "readonly: Boolean flag indicating if the signal is read-only")
|
||||
el("li", t`listeners: A Set of functions called when the signal value changes`),
|
||||
el("li", t`actions: Custom actions that can be performed on the signal`),
|
||||
el("li", t`onclear: Functions to run when the signal is cleared`),
|
||||
el("li", t`host: Reference to the host element/scope`),
|
||||
el("li", t`defined: Stack trace information for debugging`),
|
||||
el("li", t`readonly: Boolean flag indicating if the signal is read-only`)
|
||||
),
|
||||
el("p").append(...T`
|
||||
…to determine the current value of the signal, call ${el("code", "signal.valueOf()")}.
|
||||
`),
|
||||
el("p").append(...T`
|
||||
You can inspect (host) element relationships and bindings with signals in the DevTools console using
|
||||
${el("code", "$0.__dde_reactive")} (for currently selected element). In the console you will see a list of
|
||||
${el("code", "[ [ signal, listener ], element, property ]")}, where:
|
||||
${el("code", "$0.__dde_reactive")} (for currently selected element). In the console you will see a list of
|
||||
${el("code", `[ [ signal, listener ], element, property ]`)}, where:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li", "signal — the signal triggering the changes"),
|
||||
el("li", "listener — the listener function (this is an internal function for dd<el>)"),
|
||||
el("li", "element — the DOM element that is bound to the signal"),
|
||||
el("li", "property — the attribute or property name which is changing based on the signal"),
|
||||
el("li", t`signal — the signal triggering the changes`),
|
||||
el("li", t`listener — the listener function (this is an internal function for dd<el>)`),
|
||||
el("li", t`element — the DOM element that is bound to the signal`),
|
||||
el("li", t`property — the attribute or property name which is changing based on the signal`),
|
||||
),
|
||||
el("p").append(...T`
|
||||
…the structure of \`__dde_reactive\` utilizes the browser's behavior of packing the first field,
|
||||
…the structure of \`__dde_reactive\` utilizes the browser’s behavior of packing the first field,
|
||||
so you can see the element and property that changes in the console right away.
|
||||
`),
|
||||
|
||||
|
@ -25,8 +25,8 @@ export function page({ pkg, info }){
|
||||
el(h3, t`DOM Element Extensions with Addons`),
|
||||
el("p").append(...T`
|
||||
The primary method for extending DOM elements in dd<el> is through the Addon pattern.
|
||||
Addons are functions that take an element and applying some functionality to it. This pattern enables a
|
||||
clean, functional approach to element enhancement.
|
||||
Addons are functions that take an element and applying some functionality to it. This pattern enables
|
||||
a clean, functional approach to element enhancement.
|
||||
`),
|
||||
el("div", { className: "callout" }).append(
|
||||
el("h4", t`What are Addons?`),
|
||||
@ -34,61 +34,60 @@ export function page({ pkg, info }){
|
||||
Addons are simply functions with the signature: (element) => void. They:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li", t`Accept a DOM element as input`),
|
||||
el("li", t`Accept a DOM element as input`),
|
||||
el("li", t`Apply some behavior, property, or attribute to the element`),
|
||||
)
|
||||
),
|
||||
el(code, { content: `
|
||||
// Basic structure of an addon
|
||||
function myAddon(config) {
|
||||
return function(element) {
|
||||
// Apply functionality to element
|
||||
element.dataset.myAddon = config.option;
|
||||
};
|
||||
}
|
||||
// Basic structure of an addon
|
||||
function myAddon(config) {
|
||||
return function(element) {
|
||||
// Apply functionality to element
|
||||
element.dataset.myAddon = config.option;
|
||||
};
|
||||
}
|
||||
|
||||
// Using an addon
|
||||
el("div", { id: "example" }, myAddon({ option: "value" }));
|
||||
`.trim(), page_id }),
|
||||
// Using an addon
|
||||
el("div", { id: "example" }, myAddon({ option: "value" }));
|
||||
`, page_id }),
|
||||
|
||||
el(h3, t`Resource Cleanup with Abort Signals`),
|
||||
el("p").append(...T`
|
||||
When extending elements with functionality that uses resources like event listeners, timers,
|
||||
or external subscriptions, it's critical to clean up these resources when the element is removed
|
||||
or external subscriptions, it’s critical to clean up these resources when the element is removed
|
||||
from the DOM. dd<el> provides utilities for this through AbortSignal integration.
|
||||
`),
|
||||
el("div", { className: "tip" }).append(
|
||||
el("p").append(...T`
|
||||
The ${el("code", "on.disconnectedAsAbort")} utility creates an AbortSignal that automatically
|
||||
triggers when an element is disconnected from the DOM, making cleanup much easier to manage.
|
||||
The ${el("code", "scope.signal")} property creates an AbortSignal that automatically
|
||||
triggers when an element is disconnected from the DOM, making cleanup much easier to manage.
|
||||
`)
|
||||
),
|
||||
el(code, { content: `
|
||||
// Third-party library addon with proper cleanup
|
||||
function externalLibraryAddon(config, signal) {
|
||||
return function(element) {
|
||||
// Initialize the third-party library
|
||||
const instance = new ExternalLibrary(element, config);
|
||||
// Third-party library addon with proper cleanup
|
||||
function externalLibraryAddon(config, signal) {
|
||||
return function(element) {
|
||||
// Initialize the third-party library
|
||||
const instance = new ExternalLibrary(element, config);
|
||||
|
||||
// Set up cleanup when the element is removed
|
||||
signal.addEventListener('abort', () => {
|
||||
instance.destroy();
|
||||
});
|
||||
// Set up cleanup when the element is removed
|
||||
signal.addEventListener('abort', () => {
|
||||
instance.destroy();
|
||||
});
|
||||
|
||||
return element;
|
||||
};
|
||||
}
|
||||
// dde component
|
||||
function Component(){
|
||||
const { host }= scope;
|
||||
const signal= on.disconnectedAsAbort(host);
|
||||
return el("div", null, externalLibraryAddon({ option: "value" }, signal));
|
||||
}
|
||||
`.trim(), page_id }),
|
||||
return element;
|
||||
};
|
||||
}
|
||||
// dde component
|
||||
function Component(){
|
||||
const { signal }= scope;
|
||||
return el("div", null, externalLibraryAddon({ option: "value" }, signal));
|
||||
}
|
||||
`, page_id }),
|
||||
|
||||
el(h3, t`Building Library-Independent Extensions`),
|
||||
el("p").append(...T`
|
||||
When creating extensions, it's a good practice to make them as library-independent as possible.
|
||||
When creating extensions, it’s a good practice to make them as library-independent as possible.
|
||||
This approach enables better interoperability and future-proofing.
|
||||
`),
|
||||
el("div", { className: "illustration" }).append(
|
||||
@ -97,37 +96,37 @@ function Component(){
|
||||
el("div", { className: "tab" }).append(
|
||||
el("h5", t`✅ Library-Independent`),
|
||||
el(code, { content: `
|
||||
function enhancementElement({ signal, ...config }) {
|
||||
// do something
|
||||
return function(element) {
|
||||
// do something
|
||||
signal.addEventListener('abort', () => {
|
||||
// do cleanup
|
||||
});
|
||||
};
|
||||
}
|
||||
`.trim(), page_id })
|
||||
function enhancementElement({ signal, ...config }) {
|
||||
// do something
|
||||
return function(element) {
|
||||
// do something
|
||||
signal.addEventListener('abort', () => {
|
||||
// do cleanup
|
||||
});
|
||||
};
|
||||
}
|
||||
`, page_id })
|
||||
),
|
||||
el("div", { className: "tab" }).append(
|
||||
el("h5", t`⚠️ Library-Dependent`),
|
||||
el(code, { content: `
|
||||
// Tightly coupled to dd<el>
|
||||
function enhancementElement(config) {
|
||||
return function(element) {
|
||||
// do something
|
||||
on.disconnected(()=> {
|
||||
// do cleanup
|
||||
})(element);
|
||||
};
|
||||
}
|
||||
`.trim(), page_id })
|
||||
// Tightly coupled to dd<el>
|
||||
function enhancementElement(config) {
|
||||
return function(element) {
|
||||
// do something
|
||||
on.disconnected(()=> {
|
||||
// do cleanup
|
||||
})(element);
|
||||
};
|
||||
}
|
||||
`, page_id })
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
el(h3, t`Signal Extensions and Future Compatibility`),
|
||||
el(h3, t`Signal Extensions and Factory Patterns`),
|
||||
el("p").append(...T`
|
||||
Unlike DOM elements, signal functionality in dd<el> currently lacks a standardized
|
||||
Unlike DOM elements, signal functionality in dd<el> currently lacks a standardized
|
||||
way to create library-independent extensions. This is because signals are implemented
|
||||
differently across libraries.
|
||||
`),
|
||||
@ -139,35 +138,99 @@ function enhancementElement(config) {
|
||||
native signals without breaking changes when they become available.
|
||||
`)
|
||||
),
|
||||
|
||||
el("h4", t`The Signal Factory Pattern`),
|
||||
el("p").append(...T`
|
||||
For now, when extending signals functionality, focus on clear interfaces and isolation to make
|
||||
A powerful approach for extending signal functionality is the "Signal Factory" pattern.
|
||||
This approach encapsulates specific behavior in a function that creates and configures a signal.
|
||||
`),
|
||||
el(code, { content: `
|
||||
/**
|
||||
* Creates a signal for managing route state
|
||||
*
|
||||
* @param {typeof S} signal - The signal constructor
|
||||
*/
|
||||
function routerSignal(signal){
|
||||
const initial = location.hash.replace("#", "") || "all";
|
||||
return signal(initial, {
|
||||
/**
|
||||
* Set the current route
|
||||
* @param {"all"|"active"|"completed"} hash - The route to set
|
||||
*/
|
||||
set(hash){
|
||||
location.hash = hash;
|
||||
this.value = hash;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Usage
|
||||
const pageS = routerSignal(S);
|
||||
|
||||
// Update URL hash and signal value in one operation
|
||||
S.action(pageS, "set", "active");
|
||||
|
||||
// React to signal changes in the UI
|
||||
el("nav").append(
|
||||
el("a", {
|
||||
href: "#",
|
||||
className: S(()=> pageS.get() === "all" ? "selected" : ""),
|
||||
textContent: "All"
|
||||
})
|
||||
);
|
||||
`, page_id }),
|
||||
|
||||
el("div", { className: "callout" }).append(
|
||||
el("h4", t`Benefits of Signal Factories`),
|
||||
el("ul").append(
|
||||
el("li", t`Encapsulate related behavior in a single, reusable function`),
|
||||
el("li", t`Create domain-specific signals with custom actions`),
|
||||
el("li", t`Improve maintainability by centralizing similar logic`),
|
||||
el("li", t`Enable better testability by accepting the signal constructor as a parameter`),
|
||||
el("li", t`Create a clear semantic boundary around related state operations`)
|
||||
)
|
||||
),
|
||||
|
||||
el("p").append(...T`
|
||||
Note how the factory accepts the signal constructor as a parameter, making it easier to test
|
||||
and potentially migrate to different signal implementations in the future.
|
||||
`),
|
||||
|
||||
el("h4", t`Other Signal Extension Approaches`),
|
||||
el("p").append(...T`
|
||||
For simpler cases, you can also extend signals with clear interfaces and isolation to make
|
||||
future migration easier.
|
||||
`),
|
||||
el(code, { content: `
|
||||
// Signal extension with clear interface
|
||||
function createEnhancedSignal(initialValue) {
|
||||
const signal = S(initialValue);
|
||||
// Signal extension with clear interface
|
||||
function createEnhancedSignal(initialValue) {
|
||||
const signal = S(initialValue);
|
||||
|
||||
// Extension functionality
|
||||
const increment = () => signal.set(signal.get() + 1);
|
||||
const decrement = () => signal.set(signal.get() - 1);
|
||||
// Extension functionality
|
||||
const increment = () => signal.set(signal.get() + 1);
|
||||
const decrement = () => signal.set(signal.get() - 1);
|
||||
|
||||
// Return the original signal with added methods
|
||||
return Object.assign(signal, {
|
||||
increment,
|
||||
decrement
|
||||
});
|
||||
}
|
||||
// Return the original signal with added methods
|
||||
return { signal, increment, decrement };
|
||||
}
|
||||
|
||||
// Usage
|
||||
const counter = createEnhancedSignal(0);
|
||||
el("button")({ onclick: () => counter.increment() }, "Increment");
|
||||
el("div", S.text\`Count: \${counter}\`);
|
||||
`.trim(), page_id }),
|
||||
// Usage
|
||||
const counter = createEnhancedSignal(0);
|
||||
el("button", { textContent: "Increment", onclick: () => counter.increment() });
|
||||
el("div", S.text\`Count: \${counter}\`);
|
||||
`, page_id }),
|
||||
|
||||
el("div", { className: "tip" }).append(
|
||||
el("p").append(...T`
|
||||
When designing signal extensions, consider creating specialized signals for common patterns like:
|
||||
forms, API requests, persistence, animations, or routing. These can significantly reduce
|
||||
boilerplate code in your applications.
|
||||
`)
|
||||
),
|
||||
|
||||
el(h3, t`Using Signals Independently`),
|
||||
el("p").append(...T`
|
||||
While signals are tightly integrated with DDE's DOM elements, you can also use them independently.
|
||||
While signals are tightly integrated with DDE’s DOM elements, you can also use them independently.
|
||||
This can be useful when you need reactivity in non-UI code or want to integrate with other libraries.
|
||||
`),
|
||||
el("p").append(...T`
|
||||
@ -175,32 +238,34 @@ el("div", S.text\`Count: \${counter}\`);
|
||||
`),
|
||||
el("ol").append(
|
||||
el("li").append(...T`
|
||||
${el("strong", "Standard import")}: ${el("code", "import { S } from \"deka-dom-el/signals\";")}
|
||||
— This automatically registers signals with DDE's DOM reactivity system
|
||||
${el("strong", "Standard import")}: ${el("code", `import { S } from "deka-dom-el/signals";`)}
|
||||
— This automatically registers signals with DDE’s DOM reactivity system
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Independent import")}: ${el("code", "import { S } from \"deka-dom-el/src/signals-lib\";")}
|
||||
${el("strong", "Independent import")}: ${el("code", `import { S } from "deka-dom-el/src/signals-lib";`)}
|
||||
— This gives you just the signal system without DOM integration
|
||||
`)
|
||||
),
|
||||
el(code, { content: `// Independent signals without DOM integration
|
||||
import { signal as S, isSignal } from "deka-dom-el/src/signals-lib";
|
||||
el(code, { content: `
|
||||
// Independent signals without DOM integration
|
||||
import { signal, isSignal } from "deka-dom-el/src/signals-lib";
|
||||
|
||||
// Create and use signals as usual
|
||||
const count = S(0);
|
||||
const doubled = S(() => count.get() * 2);
|
||||
// Create and use signals as usual
|
||||
const count = signal(0);
|
||||
const doubled = signal(() => count.get() * 2);
|
||||
|
||||
// Subscribe to changes
|
||||
S.on(count, value => console.log(value));
|
||||
// Subscribe to changes
|
||||
signal.on(count, value => console.log(value));
|
||||
|
||||
// Update signal value
|
||||
count.set(5); // Logs: 5
|
||||
console.log(doubled.get()); // 10`, page_id }),
|
||||
// Update signal value
|
||||
count.set(5); // Logs: 5
|
||||
console.log(doubled.get()); // 10
|
||||
`, page_id }),
|
||||
el("p").append(...T`
|
||||
The independent signals API includes all core functionality (${el("code", "S()")}, ${el("code", "S.on()")},
|
||||
${el("code", "S.action()")}).
|
||||
`),
|
||||
el("div", { class: "callout" }).append(
|
||||
el("div", { className: "callout" }).append(
|
||||
el("h4", t`When to Use Independent Signals`),
|
||||
el("ul").append(
|
||||
el("li", t`For non-UI state management in your application`),
|
||||
@ -213,12 +278,16 @@ console.log(doubled.get()); // 10`, page_id }),
|
||||
el("ol").append(
|
||||
el("li").append(...T`
|
||||
${el("strong", "Use AbortSignals for cleanup:")} Always implement proper resource cleanup with
|
||||
${el("code", "on.disconnectedAsAbort")} or similar mechanisms
|
||||
${el("code", "scope.signal")} or similar mechanisms
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Separate core logic from library adaptation:")} Make your core functionality work
|
||||
with standard DOM APIs when possible
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Use signal factories for common patterns:")} Create reusable signal factories that encapsulate
|
||||
domain-specific behavior and state logic
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Document clearly:")} Provide clear documentation on how your extension works
|
||||
and what resources it uses
|
||||
@ -247,8 +316,11 @@ console.log(doubled.get()); // 10`, page_id }),
|
||||
el("dt", t`Mutating element prototypes`),
|
||||
el("dd", t`Prefer compositional approaches with addons over modifying element prototypes`),
|
||||
|
||||
el("dt", t`Duplicating similar signal logic across components`),
|
||||
el("dd", t`Use signal factories to encapsulate and reuse related signal behavior`),
|
||||
|
||||
el("dt", t`Complex initialization in addons`),
|
||||
el("dd", t`Split complex logic into a separate initialization function that the addon can call`)
|
||||
el("dd", t`Split complex logic into a separate initialization function that the addon can call`)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
387
docs/p09-optimization.html.js
Normal file
387
docs/p09-optimization.html.js
Normal 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 haven’t 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 aren’t 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),
|
||||
);
|
||||
}
|
@ -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
630
docs/p10-todomvc.html.js
Normal 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.
|
||||
`),
|
||||
);
|
||||
}
|
@ -26,11 +26,25 @@ export function page({ pkg, info }){
|
||||
`)
|
||||
),
|
||||
el("p").append(...T`
|
||||
dd<el> isn't limited to browser environments. Thanks to its flexible architecture,
|
||||
dd<el> isn’t limited to browser environments. Thanks to its flexible architecture,
|
||||
it can be used for server-side rendering (SSR) to generate static HTML files.
|
||||
This is achieved through integration with for example ${el("a", { href: "https://github.com/tmpvar/jsdom",
|
||||
textContent: "jsdom" })}, a JavaScript implementation of web standards for Node.js.
|
||||
textContent: "jsdom" })}, a JavaScript implementation of web standards for Node.js.
|
||||
`),
|
||||
el("p").append(...T`
|
||||
Additionally, you might consider using these alternative solutions:
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li").append(...T`
|
||||
${el("a", { href: "https://github.com/capricorn86/happy-dom", textContent: "happy-dom" })} —
|
||||
A JavaScript implementation of a web browser without its graphical user interface that’s faster than jsdom
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("a", { href: "https://github.com/WebReflection/linkedom", textContent: "linkedom" })} —
|
||||
A lightweight DOM implementation specifically designed for SSR with significantly better performance
|
||||
than jsdom
|
||||
`),
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/ssr/intro.js"), page_id }),
|
||||
|
||||
el(h3, t`Why Server-Side Rendering?`),
|
||||
@ -48,40 +62,40 @@ export function page({ pkg, info }){
|
||||
el(h3, t`How jsdom Integration Works`),
|
||||
el("p").append(...T`
|
||||
The jsdom export in dd<el> provides the necessary tools to use the library in Node.js
|
||||
by integrating with jsdom. Here's what it does:
|
||||
by integrating with jsdom. Here’s what it does:
|
||||
`),
|
||||
el("ol").append(
|
||||
el("li", t`Creates a virtual DOM environment in Node.js using jsdom`),
|
||||
el("li", t`Creates a virtual DOM environment in Node.js using jsdom`),
|
||||
el("li", t`Registers DOM globals like HTMLElement, document, etc. for dd<el> to use`),
|
||||
el("li", t`Sets an SSR flag in the environment to enable SSR-specific behaviors`),
|
||||
el("li", t`Provides a promise queue system for managing async operations during rendering`),
|
||||
el("li", t`Sets an SSR flag in the environment to enable SSR-specific behaviors`),
|
||||
el("li", t`Provides a promise queue system for managing async operations during rendering`),
|
||||
el("li", t`Handles DOM property/attribute mapping differences between browsers and jsdom`)
|
||||
),
|
||||
el(code, { src: fileURL("./components/examples/ssr/start.js"), page_id }),
|
||||
|
||||
el(h3, t`Basic SSR Example`),
|
||||
el("p").append(...T`
|
||||
Here's a simple example of how to use dd<el> for server-side rendering in a Node.js script:
|
||||
Here’s a simple example of how to use dd<el> for server-side rendering in a Node.js script:
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/ssr/basic-example.js"), page_id }),
|
||||
|
||||
el(h3, t`Building a Static Site Generator`),
|
||||
el(h3, t`Building a Static Site Generator`),
|
||||
el("p").append(...T`
|
||||
You can build a complete static site generator with dd<el>. In fact, this documentation site
|
||||
is built using dd<el> for server-side rendering! Here's how the documentation build process works:
|
||||
You can build a complete static site generator with dd<el>. In fact, this documentation site
|
||||
is built using dd<el> for server-side rendering! Here’s how the documentation build process works:
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/ssr/static-site-generator.js"), page_id }),
|
||||
|
||||
el(h3, t`Working with Async Content in SSR`),
|
||||
el("p").append(...T`
|
||||
The jsdom export includes a queue system to handle asynchronous operations during rendering.
|
||||
The jsdom export includes a queue system to handle asynchronous operations during rendering.
|
||||
This is crucial for components that fetch data or perform other async tasks.
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/ssr/async-data.js"), page_id }),
|
||||
|
||||
el(h3, t`Working with Dynamic Imports for SSR`),
|
||||
el("p").append(...T`
|
||||
When structuring server-side rendering code, a crucial pattern to follow is using dynamic imports
|
||||
When structuring server-side rendering code, a crucial pattern to follow is using dynamic imports
|
||||
for both the deka-dom-el/jsdom module and your page components.
|
||||
`),
|
||||
el("p").append(...T`
|
||||
@ -94,7 +108,7 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
el("li").append(...T`
|
||||
${el("strong", "Environment registration timing:")} The jsdom module auto-registers the DOM environment
|
||||
when imported, which must happen ${el("em", "after")} you've created your JSDOM instance and
|
||||
when imported, which must happen ${el("em", "after")} you’ve created your JSDOM instance and
|
||||
${el("em", "before")} you import your components using ${el("code", "import { el } from \"deka-dom-el\";")}.
|
||||
`),
|
||||
el("li").append(...T`
|
||||
@ -113,9 +127,9 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
el("ul").append(
|
||||
el("li", t`Browser-specific APIs like window.localStorage are not available in jsdom by default`),
|
||||
el("li", t`Event listeners added during SSR won't be functional in the final HTML unless hydrated on the client`),
|
||||
el("li", t`Event listeners added during SSR won’t be functional in the final HTML unless hydrated on the client`),
|
||||
el("li", t`Some DOM features may behave differently in jsdom compared to real browsers`),
|
||||
el("li", t`For large sites, you may need to optimize memory usage by creating a new jsdom instance for each page`)
|
||||
el("li", t`For large sites, you may need to optimize memory usage by creating a new jsdom instance for each page`)
|
||||
),
|
||||
el("p").append(...T`
|
||||
For advanced SSR applications, consider implementing hydration on the client-side to restore
|
||||
@ -124,7 +138,7 @@ export function page({ pkg, info }){
|
||||
|
||||
el(h3, t`Real Example: How This Documentation is Built`),
|
||||
el("p").append(...T`
|
||||
This documentation site itself is built using dd<el>'s SSR capabilities.
|
||||
This documentation site itself is built using dd<el>’s SSR capabilities.
|
||||
The build process collects all page components, renders them with jsdom, and outputs static HTML files.
|
||||
`),
|
||||
el(code, { src: fileURL("./components/examples/ssr/static-site-generator.js"), page_id }),
|
335
docs/p12-ireland.html.js
Normal file
335
docs/p12-ireland.html.js
Normal 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 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 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 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.
|
||||
`)
|
||||
);
|
||||
}
|
383
docs/p13-appendix.html.js
Normal file
383
docs/p13-appendix.html.js
Normal 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
82
index.d.ts
vendored
@ -7,10 +7,11 @@ type SupportedElement=
|
||||
| CustomElementTagNameMap[keyof CustomElementTagNameMap]
|
||||
declare global {
|
||||
type ddeComponentAttributes= Record<any, any> | undefined;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node>= (element: El)=> any;
|
||||
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node>= (element: El, ...rest: any)=> any;
|
||||
type ddeString= string | ddeSignal<string, {}>
|
||||
type ddeStringable= ddeString | number | ddeSignal<number, {}>
|
||||
}
|
||||
type Host<EL extends SupportedElement>= (...addons: ddeElementAddon<EL>[])=> EL;
|
||||
type PascalCase= `${Capitalize<string>}${string}`;
|
||||
type AttrsModified= {
|
||||
/**
|
||||
@ -84,6 +85,7 @@ export namespace el {
|
||||
is_open?: boolean
|
||||
): Comment;
|
||||
}
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL | ddeHTMLElement
|
||||
|
||||
export function el<
|
||||
A extends ddeComponentAttributes,
|
||||
@ -155,7 +157,6 @@ export function elNS(
|
||||
)=> SupportedElement
|
||||
export { elNS as createElementNS }
|
||||
|
||||
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
|
||||
/** Simulate slots for ddeComponents */
|
||||
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
|
||||
root: EL,
|
||||
@ -170,14 +171,14 @@ export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
|
||||
body: EL,
|
||||
): EL
|
||||
|
||||
export function dispatchEvent(name: keyof DocumentEventMap | string, element: SupportedElement):
|
||||
export function dispatchEvent(name: keyof DocumentEventMap | string, host: Host<SupportedElement>):
|
||||
(data?: any)=> void;
|
||||
export function dispatchEvent(name: keyof DocumentEventMap | string, options?: EventInit):
|
||||
(element: SupportedElement, data?: any)=> void;
|
||||
export function dispatchEvent(
|
||||
name: keyof DocumentEventMap | string,
|
||||
options: EventInit | null,
|
||||
element: SupportedElement | (()=> SupportedElement)
|
||||
host: Host<SupportedElement>
|
||||
): (data?: any)=> void;
|
||||
interface On{
|
||||
/** Listens to the DOM event. See {@link Document.addEventListener} */
|
||||
@ -225,7 +226,7 @@ export const on: On;
|
||||
|
||||
type Scope= {
|
||||
scope: Node | Function | Object,
|
||||
host: ddeElementAddon<any>,
|
||||
host: Host<SupportedElement>,
|
||||
custom_element: false | HTMLElement,
|
||||
prevent: boolean
|
||||
};
|
||||
@ -241,7 +242,12 @@ export const scope: {
|
||||
* It can be also used to register Addon(s) (functions to be called when component is initized)
|
||||
* — `scope.host(on.connected(console.log))`.
|
||||
* */
|
||||
host: (...addons: ddeElementAddon<SupportedElement>[])=> HTMLElement,
|
||||
host: Host<SupportedElement>,
|
||||
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
signal: AbortSignal,
|
||||
|
||||
state: Scope[],
|
||||
/** Adds new child scope. All attributes are inherited by default. */
|
||||
@ -262,7 +268,6 @@ export function customElementRender<
|
||||
): EL
|
||||
export function customElementWithDDE<EL extends (new ()=> HTMLElement)>(custom_element: EL): EL
|
||||
export function lifecyclesToEvents<EL extends (new ()=> HTMLElement)>(custom_element: EL): EL
|
||||
export function observedAttributes(custom_element: HTMLElement): Record<string, string>
|
||||
|
||||
/**
|
||||
* This is used primarly for server side rendering. To be sure that all async operations
|
||||
@ -285,6 +290,69 @@ export function observedAttributes(custom_element: HTMLElement): Record<string,
|
||||
* */
|
||||
export function queue(promise?: Promise<unknown>): Promise<unknown>;
|
||||
|
||||
/**
|
||||
* Memoization utility for caching DOM elements to improve performance.
|
||||
* Used to prevent unnecessary recreation of elements when rendering lists or complex components.
|
||||
*
|
||||
* @param key - Unique identifier for the element (usually an ID or unique value)
|
||||
* @param generator - Function that creates the element
|
||||
* @returns The cached element if the key exists, otherwise the result of the generator function
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Within S.el for list rendering
|
||||
* S.el(itemsSignal, (items, memo) =>
|
||||
* el("ul").append(
|
||||
* ...items.map(item =>
|
||||
* memo(item.id, () => el(ItemComponent, item))
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function memo<T>(key: string | number | object, generator: (key: any) => T): T;
|
||||
|
||||
/**
|
||||
* Memo namespace containing utility functions for memoization.
|
||||
*/
|
||||
export namespace memo {
|
||||
/**
|
||||
* Checks if an object is a memo scope.
|
||||
* @param obj - The object to check
|
||||
* @returns True if the object is a memo scope
|
||||
*/
|
||||
export function isScope(obj: any): boolean;
|
||||
|
||||
/**
|
||||
* Creates a memoized function with optional cleanup support.
|
||||
*
|
||||
* @param fun - The function to memoize
|
||||
* @param options - Configuration options
|
||||
* @param options.signal - AbortSignal for cleanup
|
||||
* @param options.onlyLast - When true, only keeps the cache from the most recent call
|
||||
* @returns A memoized version of the function with a .clear() method
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const renderItems = memo.scope(function(items) {
|
||||
* return items.map(item =>
|
||||
* memo(item.id, () => el("div", item.name))
|
||||
* );
|
||||
* }, {
|
||||
* signal: controller.signal,
|
||||
* onlyLast: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function scope<F extends Function>(
|
||||
fun: F,
|
||||
options?: {
|
||||
signal?: AbortSignal;
|
||||
onlyLast?: boolean;
|
||||
}
|
||||
): F & { clear: () => void };
|
||||
}
|
||||
|
||||
/* TypeScript MEH */
|
||||
declare global{
|
||||
type ddeAppend<el>= (...nodes: (Node | string)[])=> el;
|
||||
|
2
index.js
2
index.js
@ -1,3 +1,5 @@
|
||||
export * from "./src/dom.js";
|
||||
export * from "./src/customElement.js";
|
||||
export * from "./src/events.js";
|
||||
export { registerReactivity } from "./src/signals-lib/common.js";
|
||||
export { memo } from "./src/memo.js";
|
||||
|
1
jsdom.d.ts
vendored
1
jsdom.d.ts
vendored
@ -3,6 +3,7 @@ export * from "./index.d";
|
||||
type JSDOM= {
|
||||
window: Window,
|
||||
document: Document,
|
||||
Node: typeof Node,
|
||||
HTMLElement: typeof HTMLElement,
|
||||
SVGElement: typeof SVGElement,
|
||||
DocumentFragment: typeof DocumentFragment,
|
||||
|
5
jsdom.js
5
jsdom.js
@ -1,4 +1,3 @@
|
||||
//TODO: https://www.npmjs.com/package/html-element
|
||||
import { enviroment as env } from './src/dom-common.js';
|
||||
env.ssr= " ssr";
|
||||
|
||||
@ -17,7 +16,9 @@ env.setDeleteAttr= function(obj, prop, value){
|
||||
if(value) return obj.setAttribute(prop, "");
|
||||
obj.removeAttribute(prop);
|
||||
};
|
||||
const keys= { H: "HTMLElement", S: "SVGElement", F: "DocumentFragment", D: "document" };
|
||||
const keys= {
|
||||
N: "Node", H: "HTMLElement", S: "SVGElement", F: "DocumentFragment", D: "document", M: "MutationObserver",
|
||||
};
|
||||
let env_bk= {};
|
||||
let dom_last;
|
||||
|
||||
|
254
package-lock.json
generated
254
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "deka-dom-el",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.1-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "deka-dom-el",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.1-alpha",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@size-limit/preset-small-lib": "~11.2",
|
||||
@ -23,14 +23,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.3.tgz",
|
||||
"integrity": "sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.1.tgz",
|
||||
"integrity": "sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^2.1.1",
|
||||
"@csstools/css-color-parser": "^3.0.7",
|
||||
"@csstools/css-calc": "^2.1.2",
|
||||
"@csstools/css-color-parser": "^3.0.8",
|
||||
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||
"@csstools/css-tokenizer": "^3.0.3",
|
||||
"lru-cache": "^10.4.3"
|
||||
@ -152,9 +152,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
|
||||
"integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -169,9 +169,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
|
||||
"integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -186,9 +186,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -203,9 +203,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -220,9 +220,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -237,9 +237,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -254,9 +254,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -271,9 +271,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -288,9 +288,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
|
||||
"integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -305,9 +305,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -322,9 +322,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
|
||||
"integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -339,9 +339,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
|
||||
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
|
||||
"integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@ -356,9 +356,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
|
||||
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
|
||||
"integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@ -373,9 +373,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
|
||||
"integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -390,9 +390,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
|
||||
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
|
||||
"integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@ -407,9 +407,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
|
||||
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
|
||||
"integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@ -424,9 +424,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -441,9 +441,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -458,9 +458,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -475,9 +475,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -492,9 +492,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -509,9 +509,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -526,9 +526,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
|
||||
"integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -543,9 +543,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
|
||||
"integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -560,9 +560,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
|
||||
"integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -698,9 +698,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
|
||||
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
|
||||
"version": "22.13.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
|
||||
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1024,13 +1024,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cssstyle": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz",
|
||||
"integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.0.tgz",
|
||||
"integrity": "sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^2.8.2",
|
||||
"@asamuzakjp/css-color": "^3.1.1",
|
||||
"rrweb-cssom": "^0.8.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -1276,9 +1276,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
||||
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
|
||||
"integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@ -1289,31 +1289,31 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.0",
|
||||
"@esbuild/android-arm": "0.25.0",
|
||||
"@esbuild/android-arm64": "0.25.0",
|
||||
"@esbuild/android-x64": "0.25.0",
|
||||
"@esbuild/darwin-arm64": "0.25.0",
|
||||
"@esbuild/darwin-x64": "0.25.0",
|
||||
"@esbuild/freebsd-arm64": "0.25.0",
|
||||
"@esbuild/freebsd-x64": "0.25.0",
|
||||
"@esbuild/linux-arm": "0.25.0",
|
||||
"@esbuild/linux-arm64": "0.25.0",
|
||||
"@esbuild/linux-ia32": "0.25.0",
|
||||
"@esbuild/linux-loong64": "0.25.0",
|
||||
"@esbuild/linux-mips64el": "0.25.0",
|
||||
"@esbuild/linux-ppc64": "0.25.0",
|
||||
"@esbuild/linux-riscv64": "0.25.0",
|
||||
"@esbuild/linux-s390x": "0.25.0",
|
||||
"@esbuild/linux-x64": "0.25.0",
|
||||
"@esbuild/netbsd-arm64": "0.25.0",
|
||||
"@esbuild/netbsd-x64": "0.25.0",
|
||||
"@esbuild/openbsd-arm64": "0.25.0",
|
||||
"@esbuild/openbsd-x64": "0.25.0",
|
||||
"@esbuild/sunos-x64": "0.25.0",
|
||||
"@esbuild/win32-arm64": "0.25.0",
|
||||
"@esbuild/win32-ia32": "0.25.0",
|
||||
"@esbuild/win32-x64": "0.25.0"
|
||||
"@esbuild/aix-ppc64": "0.25.1",
|
||||
"@esbuild/android-arm": "0.25.1",
|
||||
"@esbuild/android-arm64": "0.25.1",
|
||||
"@esbuild/android-x64": "0.25.1",
|
||||
"@esbuild/darwin-arm64": "0.25.1",
|
||||
"@esbuild/darwin-x64": "0.25.1",
|
||||
"@esbuild/freebsd-arm64": "0.25.1",
|
||||
"@esbuild/freebsd-x64": "0.25.1",
|
||||
"@esbuild/linux-arm": "0.25.1",
|
||||
"@esbuild/linux-arm64": "0.25.1",
|
||||
"@esbuild/linux-ia32": "0.25.1",
|
||||
"@esbuild/linux-loong64": "0.25.1",
|
||||
"@esbuild/linux-mips64el": "0.25.1",
|
||||
"@esbuild/linux-ppc64": "0.25.1",
|
||||
"@esbuild/linux-riscv64": "0.25.1",
|
||||
"@esbuild/linux-s390x": "0.25.1",
|
||||
"@esbuild/linux-x64": "0.25.1",
|
||||
"@esbuild/netbsd-arm64": "0.25.1",
|
||||
"@esbuild/netbsd-x64": "0.25.1",
|
||||
"@esbuild/openbsd-arm64": "0.25.1",
|
||||
"@esbuild/openbsd-x64": "0.25.1",
|
||||
"@esbuild/sunos-x64": "0.25.1",
|
||||
"@esbuild/win32-arm64": "0.25.1",
|
||||
"@esbuild/win32-ia32": "0.25.1",
|
||||
"@esbuild/win32-x64": "0.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
@ -2095,9 +2095,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.2.tgz",
|
||||
"integrity": "sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g==",
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.3.tgz",
|
||||
"integrity": "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -2738,22 +2738,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "6.1.82",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.82.tgz",
|
||||
"integrity": "sha512-KCTjNL9F7j8MzxgfTgjT+v21oYH38OidFty7dH00maWANAI2IsLw2AnThtTJi9HKALHZKQQWnNebYheadacD+g==",
|
||||
"version": "6.1.84",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.84.tgz",
|
||||
"integrity": "sha512-aRGIbCIF3teodtUFAYSdQONVmDRy21REM3o6JnqWn5ZkQBJJ4gHxhw6OfwQ+WkSAi3ASamrS4N4nyazWx6uTYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^6.1.82"
|
||||
"tldts-core": "^6.1.84"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "6.1.82",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.82.tgz",
|
||||
"integrity": "sha512-Jabl32m21tt/d/PbDO88R43F8aY98Piiz6BVH9ShUlOAiiAELhEqwrAmBocjAqnCfoUeIsRU+h3IEzZd318F3w==",
|
||||
"version": "6.1.84",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.84.tgz",
|
||||
"integrity": "sha512-NaQa1W76W2aCGjXybvnMYzGSM4x8fvG2AN/pla7qxcg0ZHbooOPhA8kctmOZUDfZyhDL27OGNbwAeig8P4p1vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "deka-dom-el",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.1-alpha",
|
||||
"description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.",
|
||||
"author": "Jan Andrle <andrle.jan@centrum.cz>",
|
||||
"license": "MIT",
|
||||
@ -36,7 +36,8 @@
|
||||
"files": [
|
||||
"*.js",
|
||||
"*.d.ts",
|
||||
"src"
|
||||
"src",
|
||||
"tsconfig.json"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
3
signals.d.ts
vendored
3
signals.d.ts
vendored
@ -12,6 +12,7 @@ type Action<V>= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typ
|
||||
type SymbolOnclear= symbol;
|
||||
type Actions<V>= Record<string | SymbolOnclear, Action<V>>;
|
||||
type OnListenerOptions= Pick<AddEventListenerOptions, "signal"> & { first_time?: boolean };
|
||||
type SElement= Node | Element | DocumentFragment | ddeHTMLElement | ddeSVGElement | ddeDocumentFragment;
|
||||
interface signal{
|
||||
_: Symbol
|
||||
/**
|
||||
@ -61,7 +62,7 @@ interface signal{
|
||||
* S.el(listS, list=> list.map(li=> el("li", li)));
|
||||
* ```
|
||||
* */
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S)=> Element | Element[] | DocumentFragment): DocumentFragment;
|
||||
el<S extends any>(signal: Signal<S, any>, el: (v: S)=> SElement | SElement[]): DocumentFragment;
|
||||
|
||||
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
|
||||
}
|
||||
|
@ -7,10 +7,10 @@ import { c_ch_o } from "./events-observer.js";
|
||||
*
|
||||
* @param {Element|ShadowRoot} target - The custom element or shadow root to render into
|
||||
* @param {Function} render - The render function that returns content
|
||||
* @param {Function|Object} [props=observedAttributes] - Props to pass to the render function
|
||||
* @param {Function|Object} [props= {}] - Props to pass to the render function
|
||||
* @returns {Node} The rendered content
|
||||
*/
|
||||
export function customElementRender(target, render, props= observedAttributes){
|
||||
export function customElementRender(target, render, props= {}){
|
||||
const custom_element= target.host || target;
|
||||
scope.push({
|
||||
scope: custom_element,
|
||||
@ -69,15 +69,3 @@ export { lifecyclesToEvents as customElementWithDDE };
|
||||
function wrapMethod(obj, method, apply){
|
||||
obj[method]= new Proxy(obj[method] || (()=> {}), { apply });
|
||||
}
|
||||
|
||||
import { observedAttributes as oA } from "./helpers.js";
|
||||
|
||||
/**
|
||||
* Gets observed attributes for a custom element
|
||||
*
|
||||
* @param {Element} instance - Custom element instance
|
||||
* @returns {Object} Object mapping camelCase attribute names to their values
|
||||
*/
|
||||
export function observedAttributes(instance){
|
||||
return oA(instance, (i, n)=> i.getAttribute(n));
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ export const enviroment= {
|
||||
setDeleteAttr,
|
||||
ssr: "",
|
||||
D: globalThis.document,
|
||||
N: globalThis.Node,
|
||||
F: globalThis.DocumentFragment,
|
||||
H: globalThis.HTMLElement,
|
||||
S: globalThis.SVGElement,
|
||||
|
16
src/dom.js
16
src/dom.js
@ -1,6 +1,7 @@
|
||||
import { signals } from "./signals-lib/common.js";
|
||||
import { enviroment as env } from './dom-common.js';
|
||||
import { isInstance, isUndef, oAssign } from "./helpers.js";
|
||||
import { on } from "./events.js";
|
||||
|
||||
/**
|
||||
* Queues a promise, this is helpful for crossplatform components (on server side we can wait for all registered
|
||||
@ -19,6 +20,8 @@ const scopes= [ {
|
||||
host: c=> c ? c(env.D.body) : env.D.body,
|
||||
prevent: true,
|
||||
} ];
|
||||
/** Store for disconnect abort controllers */
|
||||
const store_abort= new WeakMap();
|
||||
/**
|
||||
* Scope management utility for tracking component hierarchies
|
||||
*/
|
||||
@ -35,6 +38,19 @@ export const scope= {
|
||||
*/
|
||||
get host(){ return this.current.host; },
|
||||
|
||||
/**
|
||||
* Creates/gets an AbortController that triggers when the element disconnects
|
||||
* */
|
||||
get signal(){
|
||||
const { host }= this;
|
||||
if(store_abort.has(host)) return store_abort.get(host);
|
||||
|
||||
const a= new AbortController();
|
||||
store_abort.set(host, a);
|
||||
host(on.disconnected(()=> a.abort()));
|
||||
return a.signal;
|
||||
},
|
||||
|
||||
/**
|
||||
* Prevents default behavior in the current scope
|
||||
* @returns {Object} Current scope context
|
||||
|
@ -167,9 +167,9 @@ function connectionsChangesObserverConstructor(){
|
||||
if(store.size > 30)//TODO?: limit
|
||||
await requestIdle();
|
||||
const out= [];
|
||||
if(!isInstance(element, Node)) return out;
|
||||
if(!isInstance(element, env.N)) return out;
|
||||
for(const el of store.keys()){
|
||||
if(el===element || !isInstance(el, Node)) continue;
|
||||
if(el===element || !isInstance(el, env.N)) continue;
|
||||
if(element.contains(el))
|
||||
out.push(el);
|
||||
}
|
||||
@ -214,6 +214,7 @@ function connectionsChangesObserverConstructor(){
|
||||
|
||||
const ls= store.get(element);
|
||||
if(!ls.length_d) continue;
|
||||
// support for S.el, see https://vuejs.org/guide/extras/web-components.html#lifecycle
|
||||
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
|
||||
out= true;
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
export { registerReactivity } from './signals-lib/common.js';
|
||||
import { enviroment as env, keyLTE, evc, evd, eva } from './dom-common.js';
|
||||
import { keyLTE, evc, evd } from './dom-common.js';
|
||||
import { oAssign, onAbort } from './helpers.js';
|
||||
|
||||
/**
|
||||
@ -48,7 +47,6 @@ import { c_ch_o } from "./events-observer.js";
|
||||
const lifeOptions= obj=> oAssign({}, typeof obj==="object" ? obj : null, { once: true });
|
||||
|
||||
//TODO: cleanUp when event before abort?
|
||||
//TODO: docs (e.g.) https://nolanlawson.com/2024/01/13/web-component-gotcha-constructor-vs-connectedcallback/
|
||||
|
||||
/**
|
||||
* Creates a function to register connected lifecycle event listeners
|
||||
@ -88,53 +86,3 @@ on.disconnected= function(listener, options){
|
||||
return element;
|
||||
};
|
||||
};
|
||||
|
||||
/** Store for disconnect abort controllers */
|
||||
const store_abort= new WeakMap();
|
||||
|
||||
/**
|
||||
* Creates an AbortController that triggers when the element disconnects
|
||||
*
|
||||
* @param {Function} host - Host element or function taking an element
|
||||
* @returns {AbortSignal} AbortSignal that aborts on disconnect
|
||||
*/
|
||||
on.disconnectedAsAbort= function(host){
|
||||
if(store_abort.has(host)) return store_abort.get(host);
|
||||
|
||||
const a= new AbortController();
|
||||
store_abort.set(host, a);
|
||||
host(on.disconnected(()=> a.abort()));
|
||||
return a.signal;
|
||||
};
|
||||
|
||||
/** Store for elements with attribute observers */
|
||||
const els_attribute_store= new WeakSet();
|
||||
|
||||
/**
|
||||
* Creates a function to register attribute change event listeners
|
||||
*
|
||||
* @param {Function} listener - Event handler
|
||||
* @param {Object} [options] - Event listener options
|
||||
* @returns {Function} Function that registers the attribute change listener
|
||||
*/
|
||||
on.attributeChanged= function(listener, options){
|
||||
if(typeof options !== "object")
|
||||
options= {};
|
||||
return function registerElement(element){
|
||||
element.addEventListener(eva, listener, options);
|
||||
if(element[keyLTE] || els_attribute_store.has(element))
|
||||
return element;
|
||||
|
||||
if(!env.M) return element;
|
||||
|
||||
const observer= new env.M(function(mutations){
|
||||
for(const { attributeName, target } of mutations)
|
||||
target.dispatchEvent(
|
||||
new CustomEvent(eva, { detail: [ attributeName, target.getAttribute(attributeName) ] }));
|
||||
});
|
||||
const c= onAbort(options.signal, ()=> observer.disconnect());
|
||||
if(c) observer.observe(element, { attributes: true });
|
||||
//TODO: clean up when element disconnected
|
||||
return element;
|
||||
};
|
||||
};
|
||||
|
50
src/memo.js
Normal file
50
src/memo.js
Normal 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;
|
||||
};
|
@ -159,38 +159,31 @@ signal.clear= function(...signals){
|
||||
};
|
||||
/** Property key for tracking reactive elements */
|
||||
const key_reactive= "__dde_reactive";
|
||||
import { enviroment as env } from "../dom-common.js";
|
||||
import { enviroment as env, eva } from "../dom-common.js";
|
||||
import { el } from "../dom.js";
|
||||
import { scope } from "../dom.js";
|
||||
import { on } from "../events.js";
|
||||
import { memo } from "../memo.js";
|
||||
|
||||
export function cache(store= oCreate()){
|
||||
return (key, fun)=> hasOwn(store, key) ? store[key] : (store[key]= fun());
|
||||
}
|
||||
/**
|
||||
* Creates a reactive DOM element that re-renders when signal changes
|
||||
*
|
||||
* @TODO Third argument for handle `cache_tmp` in re-render
|
||||
* @param {Object} s - Signal object to watch
|
||||
* @param {Function} map - Function mapping signal value to DOM elements
|
||||
* @returns {DocumentFragment} Fragment containing reactive elements
|
||||
*/
|
||||
signal.el= function(s, map){
|
||||
map= memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
|
||||
const mark_start= el.mark({ type: "reactive", source: new Defined().compact }, true);
|
||||
const mark_end= mark_start.end;
|
||||
const out= env.D.createDocumentFragment();
|
||||
out.append(mark_start, mark_end);
|
||||
const { current }= scope;
|
||||
let cache_shared= oCreate();
|
||||
const reRenderReactiveElement= v=> {
|
||||
if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasn’t yet rendered
|
||||
return removeSignalListener(s, reRenderReactiveElement);
|
||||
const memo= cache(cache_shared);
|
||||
cache_shared= oCreate();
|
||||
scope.push(current);
|
||||
let els= map(v, function useCache(key, fun){
|
||||
return (cache_shared[key]= memo(key, fun));
|
||||
});
|
||||
let els= map(v);
|
||||
scope.pop();
|
||||
if(!Array.isArray(els))
|
||||
els= [ els ];
|
||||
@ -209,7 +202,7 @@ signal.el= function(s, map){
|
||||
reRenderReactiveElement(s.get());
|
||||
current.host(on.disconnected(()=>
|
||||
/*! Clears cached elements for reactive element `S.el` */
|
||||
cache_shared= {}
|
||||
map.clear()
|
||||
));
|
||||
return out;
|
||||
};
|
||||
@ -265,7 +258,7 @@ const key_attributes= "__dde_attributes";
|
||||
signal.observedAttributes= function(element){
|
||||
const store= element[key_attributes]= {};
|
||||
const attrs= observedAttributes(element, observedAttribute(store));
|
||||
on.attributeChanged(function attributeChangeToSignal({ detail }){
|
||||
on(eva, function attributeChangeToSignal({ detail }){
|
||||
/*! This maps attributes to signals (`S.observedAttributes`).
|
||||
Investigate `__dde_attributes` key of the element. */
|
||||
const [ name, value ]= detail;
|
||||
|
Loading…
x
Reference in New Issue
Block a user