Something went wrong. Try again.
a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt
hypermedia frontend signals
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261/** * Global lifecycle hook system for Volt.js * Provides beforeMount, afterMount, beforeUnmount, and afterUnmount hooks */
import type { ElementLifecycleState, GlobalHookName, MountHookCallback, Scope, UnmountHookCallback } from "$types/volt";import { report } from "./error";
/** * Global lifecycle hooks registry */const lifecycleHooks = new Map<GlobalHookName, Set<MountHookCallback | UnmountHookCallback>>([ ["beforeMount", new Set()], ["afterMount", new Set()], ["beforeUnmount", new Set()], ["afterUnmount", new Set()],]);
const elementLifecycleStates = new WeakMap<Element, ElementLifecycleState>();
/** * Register a global lifecycle hook. * Global hooks run for every mount/unmount operation in the application. * * @param name - Name of the lifecycle hook * @param cb - Callback function to execute * @returns Unregister function * * @example * // Log every mount operation * registerGlobalHook('beforeMount', (root, scope) => { * console.log('Mounting', root, 'with scope', scope); * }); * * @example * // Track mounted elements * const mountedElements = new Set<Element>(); * registerGlobalHook('afterMount', (root) => { * mountedElements.add(root); * }); * registerGlobalHook('beforeUnmount', (root) => { * mountedElements.delete(root); * }); */export function registerGlobalHook(name: GlobalHookName, cb: MountHookCallback | UnmountHookCallback): () => void { const hooks = lifecycleHooks.get(name); if (!hooks) { throw new Error(`Unknown lifecycle hook: ${name}`); }
hooks.add(cb);
return () => { hooks.delete(cb); };}
/** * Unregister a global lifecycle hook. * * @param name - Name of the lifecycle hook * @param cb - Callback function to remove * @returns true if the hook was removed, false if it wasn't registered */export function unregisterGlobalHook(name: GlobalHookName, cb: MountHookCallback | UnmountHookCallback): boolean { const hooks = lifecycleHooks.get(name); if (!hooks) { return false; }
return hooks.delete(cb);}
/** * Clear all global hooks for a specific lifecycle event. * * @param name - Name of the lifecycle hook to clear */export function clearGlobalHooks(name: GlobalHookName): void { const hooks = lifecycleHooks.get(name); if (hooks) { hooks.clear(); }}
export function clearAllGlobalHooks(): void { for (const hooks of lifecycleHooks.values()) { hooks.clear(); }}
/** * Get all registered hooks for a specific lifecycle event. * Used internally by the binder system. * * @param name - Name of the lifecycle hook * @returns Array of registered callbacks */export function getGlobalHooks(name: GlobalHookName): Array<MountHookCallback | UnmountHookCallback> { const hooks = lifecycleHooks.get(name); return hooks ? [...hooks] : [];}
/** * Execute all registered hooks for a lifecycle event. * Used internally by the binder system. * * @param hookName - Name of the lifecycle hook to execute * @param root - The root element being mounted/unmounted * @param scope - The scope object (only for mount hooks) */export function execGlobalHooks(hookName: GlobalHookName, root: Element, scope?: Scope): void { const hooks = lifecycleHooks.get(hookName); if (!hooks || hooks.size === 0) { return; }
for (const callback of hooks) { try { if (hookName === "beforeMount" || hookName === "afterMount") { if (scope !== undefined) { (callback as MountHookCallback)(root, scope); } } else { (callback as UnmountHookCallback)(root); } } catch (error) { report(error as Error, { source: "lifecycle", element: root as HTMLElement, hookName: hookName }); } }}
/** * Get or create lifecycle state for an element. * * @param element - The element to track * @returns The lifecycle state object */function getElementLifecycleState(element: Element): ElementLifecycleState { let state = elementLifecycleStates.get(element); if (!state) { state = { isMounted: false, bindings: new Set(), onMount: new Set(), onUnmount: new Set() }; elementLifecycleStates.set(element, state); } return state;}
/** * Register a per-element lifecycle hook. * These hooks are specific to individual elements. * * @param element - The element to attach the hook to * @param hookType - Type of hook ('mount' or 'unmount') * @param cb - Callback to execute */export function registerElementHook(element: Element, hookType: "mount" | "unmount", cb: () => void): void { const state = getElementLifecycleState(element);
if (hookType === "mount") { state.onMount.add(cb); } else { state.onUnmount.add(cb); }}
/** * Notify that an element has been mounted. * Executes all registered onMount callbacks for the element. * * @param el - The mounted element */export function notifyElementMounted(el: Element): void { const state = getElementLifecycleState(el);
if (state.isMounted) { return; }
state.isMounted = true;
for (const callback of state.onMount) { try { callback(); } catch (error) { report(error as Error, { source: "lifecycle", element: el as HTMLElement, hookName: "onMount" }); } }}
/** * Notify that an element is being unmounted. * Executes all registered onUnmount callbacks for the element. * * @param el - The element being unmounted */export function notifyElementUnmounted(el: Element): void { const state = getElementLifecycleState(el);
if (!state.isMounted) { return; }
state.isMounted = false;
for (const callback of state.onUnmount) { try { callback(); } catch (error) { report(error as Error, { source: "lifecycle", element: el as HTMLElement, hookName: "onUnmount" }); } }
elementLifecycleStates.delete(el);}
/** * Notify that a binding has been created on an element. * * @param element - The element the binding is on * @param name - Name of the binding (e.g., 'text', 'class', 'on-click') */export function notifyBindingCreated(element: Element, name: string): void { const state = getElementLifecycleState(element); state.bindings.add(name);}
/** * Notify that a binding has been destroyed on an element. * * @param element - The element the binding was on * @param name - Name of the binding */export function notifyBindingDestroyed(element: Element, name: string): void { const state = elementLifecycleStates.get(element); if (state) { state.bindings.delete(name); }}
/** * Check if an element is currently mounted. * * @param element - The element to check * @returns true if the element is mounted */export function isElementMounted(element: Element): boolean { const state = elementLifecycleStates.get(element); return state?.isMounted ?? false;}
/** * Get all bindings on an element. * * @param element - The element to query * @returns Array of binding names */export function getElementBindings(element: Element): string[] { const state = elementLifecycleStates.get(element); return state ? [...state.bindings] : [];}