UNPKG

signal-h

Version:

A reactive DOM library with vue + h

140 lines (122 loc) 3.6 kB
import { signal, watch } from "./core"; import { registerNode } from "./global-life"; import { If } from "./if"; import { createOwner, onCleanup, onMount } from "./life"; import { List } from "./list"; import { TAGS, type TagName } from "./tags"; // Lifecycle tag type interface LifecycleTag { __lifecycle: "mount" | "cleanup"; fn: () => void; } // Type for element tag shortcuts - simplified without HTMLElementTagNameMap indexing type TagShortcuts = { [K in TagName]: ( props?: Record<string, unknown>, ...children: unknown[] ) => HTMLElement; }; // Main h function type export interface HFunction { <T extends TagName>( tag: T, props?: Record<string, unknown>, ...children: unknown[] ): HTMLElement; // API properties signal: typeof signal; watch: typeof watch; If: typeof If; List: typeof List; onMount: (fn: () => void) => { __lifecycle: "mount"; fn: () => void }; onCleanup: (fn: () => void) => { __lifecycle: "cleanup"; fn: () => void }; } const hFn = ( tag: string, props: Record<string, unknown> = {}, ...children: unknown[] ) => { const owner = createOwner(); const el = document.createElement(tag); // Register lifecycle owner registerNode(el, owner); // bind props for (const [key, val] of Object.entries(props)) { if (key.startsWith("on") && typeof val === "function") { const eventName = key.slice(2).toLowerCase(); el.addEventListener(eventName, val as EventListener); } else if (typeof val === "function") { // Capture val to avoid closure issue const valFn = val; const keyName = key; watch(() => { const value = valFn(); // For input/textarea elements, set the value property directly if ( (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) && keyName === "value" ) { el.value = String(value); } else { el.setAttribute(keyName, String(value)); } }); } else { el.setAttribute(key, String(val)); } } // children const append = (child: unknown) => { // lifecycle tag if ( child && typeof child === "object" && (child as LifecycleTag).__lifecycle ) { const lifecycleChild = child as LifecycleTag; if (lifecycleChild.__lifecycle === "mount") onMount(lifecycleChild.fn); if (lifecycleChild.__lifecycle === "cleanup") onCleanup(lifecycleChild.fn); return; } if (Array.isArray(child)) return child.forEach(append); if (child instanceof Node) return el.appendChild(child); const text = document.createTextNode(""); el.appendChild(text); if (typeof child === "function") { // Capture the child function in a closure to avoid closure issue const childFn = child; watch(() => { const value = childFn(); const current = typeof value === "function" ? value() : value; text.textContent = String(current); }); } else { text.textContent = String(child); } }; children.forEach(append); return el; }; // Cast to add tag shortcuts const hWithTags = hFn as HFunction & TagShortcuts; // Add API properties hWithTags.signal = signal; hWithTags.watch = watch; hWithTags.If = If; hWithTags.List = List; // Lifecycle API (now children-friendly) hWithTags.onMount = (fn: () => void) => ({ __lifecycle: "mount", fn }); hWithTags.onCleanup = (fn: () => void) => ({ __lifecycle: "cleanup", fn }); // Element tag shortcuts: h.div({class:"..."}) for (const tag of TAGS) { hWithTags[tag] = ( props: Record<string, unknown> = {}, ...children: unknown[] ) => { return hWithTags(tag as TagName, props, ...children); }; } // Export the enhanced h function export { hWithTags as h };