From 6f6bb8b4244f3d985412dfb2392179d333465ff1 Mon Sep 17 00:00:00 2001 From: volkanceylan Date: Sat, 11 Oct 2025 01:58:03 +0300 Subject: [PATCH] feat: Add jsxDomAttributeHook and jsxDomChildrenHook for reactive attribute and child updates - Introduced `jsxDomAttributeHook` to intercept attribute assignment and enable reactive updates. - Added `jsxDomChildrenHook` to allow dynamic replacement of child elements using custom logic. - These hooks are designed to integrate seamlessly with reactive libraries like Preact Signals. - Ensured backward compatibility by making the hooks opt-in. - Added tests to validate the functionality of the hooks in various scenarios. This feature enhances the flexibility of `jsx-dom` by enabling developers to implement reactive DOM updates with minimal effort. --- src/jsx-dom.ts | 10 ++++++++- src/util.ts | 8 ++++++++ test/hooks.test.tsx | 50 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/jsx-dom.ts b/src/jsx-dom.ts index 4fa5208..16fe22c 100644 --- a/src/jsx-dom.ts +++ b/src/jsx-dom.ts @@ -2,7 +2,9 @@ import { isRef } from "./ref" import { forEach, isArrayLike, + isAttributeHook, isBoolean, + isChildrenHook, isComponentClass, isElement, isFunction, @@ -219,6 +221,8 @@ function appendChild( const shadowRoot = (node as HTMLElement).attachShadow(child.attr) appendChild(child.children, shadowRoot) attachRef(child.ref, shadowRoot) + } else if (isChildrenHook(child)) { + appendChild(child.jsxDomChildrenHook.call(child, node), node) } } @@ -406,7 +410,11 @@ function attrNS(node: Element, namespace: string, key: string, value: string | n function attributes(attr: object, node: HTMLElement | SVGElement) { for (const key of keys(attr)) { - attribute(key, attr[key], node) + let value: any = attr[key] + if (isAttributeHook(value)) { + value = value.jsxDomAttributeHook(node, key) + } + attribute(key, value, node) } return node } diff --git a/src/util.ts b/src/util.ts index c3ff94a..e351376 100644 --- a/src/util.ts +++ b/src/util.ts @@ -40,6 +40,14 @@ export function isArrayLike(obj: any): obj is ArrayLike { return isObject(obj) && typeof obj.length === "number" && typeof obj.nodeType !== "number" } +export function isAttributeHook(val: any): val is { jsxDomAttributeHook: (node: HTMLElement | SVGElement, attr: string) => any } { + return isObject(val) && isFunction(val.jsxDomAttributeHook) +} + +export function isChildrenHook(val: any): val is { jsxDomChildrenHook: (parent: HTMLElement | SVGElement) => any } { + return isObject(val) && isFunction(val.jsxDomChildrenHook) +} + export function forEach(value: { [key: string]: V }, fn: (value: V, key: string) => void) { if (!value) return for (const key of keys(value)) { diff --git a/test/hooks.test.tsx b/test/hooks.test.tsx index 5b5df99..fbacbf5 100644 --- a/test/hooks.test.tsx +++ b/test/hooks.test.tsx @@ -85,4 +85,52 @@ describe("hooks", () => { cls.toggle("container", false) expect(cls.contains("container")).toBe(false) }) -}) + + it("supports jsxDomAttributeHook", () => { + let callValue: any, callNode: HTMLElement | SVGElement, callTimes: number = 0 + function hook(node: HTMLElement | SVGElement, attr: string) { + callNode = node; + callValue = this; + callTimes++ + return `hooked:${attr}` + } + const hookedValue = { jsxDomAttributeHook: hook } + const div =
+ expect(callTimes).toBe(1) + expect(callValue).toBe(hookedValue) + expect(callNode).toBe(div) + expect(div.getAttribute("title")).toBe("hooked:title") + }) + + it("supports jsxDomChildrenHook as sole content", () => { + let callValue: any, callParentNode: HTMLElement | SVGElement, callTimes: number = 0 + function hook(parentNode: HTMLElement | SVGElement) { + callParentNode = parentNode + callValue = this + callTimes++ + return "test" + } + const hookedValue = { jsxDomChildrenHook: hook } + const div =
{hookedValue}
+ expect(callTimes).toBe(1) + expect(callValue).toBe(hookedValue) + expect(callParentNode).toBe(div) + expect(div.textContent).toBe("test") + }) + + it("supports jsxDomChildrenHook inside a fragment", () => { + let callValue: any, callParentNode: HTMLElement | SVGElement, callTimes: number = 0 + function hook(parentNode: HTMLElement | SVGElement) { + callParentNode = parentNode + callValue = this + callTimes++ + return "test" + } + const hookedValue = { jsxDomChildrenHook: hook } + const div =
<>{hookedValue}
+ expect(callTimes).toBe(1) + expect(callValue).toBe(hookedValue) + expect(callParentNode instanceof DocumentFragment).toBe(true) + expect(div.textContent).toBe("test") + }) +}) \ No newline at end of file