signal-h
Version:
A reactive DOM library with vue + h
140 lines (122 loc) • 3.6 kB
text/typescript
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 };