Skip to content

Commit 6f6bb8b

Browse files
committed
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.
1 parent 2d9b86d commit 6f6bb8b

File tree

3 files changed

+66
-2
lines changed

3 files changed

+66
-2
lines changed

src/jsx-dom.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { isRef } from "./ref"
22
import {
33
forEach,
44
isArrayLike,
5+
isAttributeHook,
56
isBoolean,
7+
isChildrenHook,
68
isComponentClass,
79
isElement,
810
isFunction,
@@ -219,6 +221,8 @@ function appendChild(
219221
const shadowRoot = (node as HTMLElement).attachShadow(child.attr)
220222
appendChild(child.children, shadowRoot)
221223
attachRef(child.ref, shadowRoot)
224+
} else if (isChildrenHook(child)) {
225+
appendChild(child.jsxDomChildrenHook.call(child, node), node)
222226
}
223227
}
224228

@@ -406,7 +410,11 @@ function attrNS(node: Element, namespace: string, key: string, value: string | n
406410

407411
function attributes(attr: object, node: HTMLElement | SVGElement) {
408412
for (const key of keys(attr)) {
409-
attribute(key, attr[key], node)
413+
let value: any = attr[key]
414+
if (isAttributeHook(value)) {
415+
value = value.jsxDomAttributeHook(node, key)
416+
}
417+
attribute(key, value, node)
410418
}
411419
return node
412420
}

src/util.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ export function isArrayLike(obj: any): obj is ArrayLike<any> {
4040
return isObject(obj) && typeof obj.length === "number" && typeof obj.nodeType !== "number"
4141
}
4242

43+
export function isAttributeHook(val: any): val is { jsxDomAttributeHook: (node: HTMLElement | SVGElement, attr: string) => any } {
44+
return isObject(val) && isFunction(val.jsxDomAttributeHook)
45+
}
46+
47+
export function isChildrenHook(val: any): val is { jsxDomChildrenHook: (parent: HTMLElement | SVGElement) => any } {
48+
return isObject(val) && isFunction(val.jsxDomChildrenHook)
49+
}
50+
4351
export function forEach<V = any>(value: { [key: string]: V }, fn: (value: V, key: string) => void) {
4452
if (!value) return
4553
for (const key of keys(value)) {

test/hooks.test.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,52 @@ describe("hooks", () => {
8585
cls.toggle("container", false)
8686
expect(cls.contains("container")).toBe(false)
8787
})
88-
})
88+
89+
it("supports jsxDomAttributeHook", () => {
90+
let callValue: any, callNode: HTMLElement | SVGElement, callTimes: number = 0
91+
function hook(node: HTMLElement | SVGElement, attr: string) {
92+
callNode = node;
93+
callValue = this;
94+
callTimes++
95+
return `hooked:${attr}`
96+
}
97+
const hookedValue = { jsxDomAttributeHook: hook }
98+
const div = <div title={hookedValue} />
99+
expect(callTimes).toBe(1)
100+
expect(callValue).toBe(hookedValue)
101+
expect(callNode).toBe(div)
102+
expect(div.getAttribute("title")).toBe("hooked:title")
103+
})
104+
105+
it("supports jsxDomChildrenHook as sole content", () => {
106+
let callValue: any, callParentNode: HTMLElement | SVGElement, callTimes: number = 0
107+
function hook(parentNode: HTMLElement | SVGElement) {
108+
callParentNode = parentNode
109+
callValue = this
110+
callTimes++
111+
return "test"
112+
}
113+
const hookedValue = { jsxDomChildrenHook: hook }
114+
const div = <div>{hookedValue}</div>
115+
expect(callTimes).toBe(1)
116+
expect(callValue).toBe(hookedValue)
117+
expect(callParentNode).toBe(div)
118+
expect(div.textContent).toBe("test")
119+
})
120+
121+
it("supports jsxDomChildrenHook inside a fragment", () => {
122+
let callValue: any, callParentNode: HTMLElement | SVGElement, callTimes: number = 0
123+
function hook(parentNode: HTMLElement | SVGElement) {
124+
callParentNode = parentNode
125+
callValue = this
126+
callTimes++
127+
return "test"
128+
}
129+
const hookedValue = { jsxDomChildrenHook: hook }
130+
const div = <div><>{hookedValue}</></div>
131+
expect(callTimes).toBe(1)
132+
expect(callValue).toBe(hookedValue)
133+
expect(callParentNode instanceof DocumentFragment).toBe(true)
134+
expect(div.textContent).toBe("test")
135+
})
136+
})

0 commit comments

Comments
 (0)