Skip to content

Commit 481e900

Browse files
authored
feat: serialize element in props. (not only children) (#162)
1 parent 2237cdd commit 481e900

File tree

6 files changed

+51
-14
lines changed

6 files changed

+51
-14
lines changed

mocks/app/islands/Counter.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import type { PropsWithChildren } from 'hono/jsx'
1+
import type { PropsWithChildren, Child } from 'hono/jsx'
22
import { useState } from 'hono/jsx'
33
import Badge from './Badge'
44

55
export default function Counter({
66
children,
77
initial = 0,
88
id = '',
9+
slot,
910
}: PropsWithChildren<{
1011
initial?: number
1112
id?: string
13+
slot?: Child
1214
}>) {
1315
const [count, setCount] = useState(initial)
1416
const increment = () => setCount(count + 1)
@@ -18,6 +20,7 @@ export default function Counter({
1820
<p>Count: {count}</p>
1921
<button onClick={increment}>Increment</button>
2022
{children}
23+
{slot && <div>{slot}</div>}
2124
</div>
2225
)
2326
}

mocks/app/routes/interaction/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export default function Interaction() {
77
<Counter initial={10}>
88
<Counter initial={15} />
99
</Counter>
10+
<Counter initial={20} slot={<Counter id='slot' initial={25} />}>
11+
<Counter initial={30} />
12+
</Counter>
1013
</>
1114
)
1215
}

src/client/client.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,13 @@ export const createClient = async (options?: ClientOptions) => {
5555
const hydrate = options?.hydrate ?? render
5656
const createElement = options?.createElement ?? jsxFn
5757

58-
const maybeTemplate = element.childNodes[element.childNodes.length - 1]
59-
if (
60-
maybeTemplate?.nodeName === 'TEMPLATE' &&
61-
(maybeTemplate as HTMLElement)?.attributes.getNamedItem(DATA_HONO_TEMPLATE) !== null
62-
) {
58+
let maybeTemplate = element.childNodes[element.childNodes.length - 1]
59+
while (maybeTemplate?.nodeName === 'TEMPLATE') {
60+
const propKey = (maybeTemplate as HTMLElement).getAttribute(DATA_HONO_TEMPLATE)
61+
if (propKey == null) {
62+
break
63+
}
64+
6365
let createChildren = options?.createChildren
6466
if (!createChildren) {
6567
const { buildCreateChildrenFn } = await import('./runtime')
@@ -68,9 +70,11 @@ export const createClient = async (options?: ClientOptions) => {
6870
async (name: string) => (await (FILES[`${root}${name}`] as FileCallback)()).default
6971
)
7072
}
71-
props.children = await createChildren(
73+
props[propKey] = await createChildren(
7274
(maybeTemplate as HTMLTemplateElement).content.childNodes
7375
)
76+
77+
maybeTemplate = maybeTemplate.previousSibling as ChildNode
7478
}
7579

7680
const newElem = await createElement(Component, props)

src/vite/components/honox-island.tsx

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createContext, useContext } from 'hono/jsx'
1+
import { createContext, useContext, isValidElement } from 'hono/jsx'
22
import { COMPONENT_NAME, DATA_SERIALIZED_PROPS, DATA_HONO_TEMPLATE } from '../../constants'
33

44
const inIsland = Symbol()
@@ -8,6 +8,11 @@ const IslandContext = createContext({
88
[inChildren]: false,
99
})
1010

11+
const isElementPropValue = (value: unknown): boolean =>
12+
Array.isArray(value)
13+
? value.some(isElementPropValue)
14+
: typeof value === 'object' && isValidElement(value)
15+
1116
export const HonoXIsland = ({
1217
componentName,
1318
Component,
@@ -18,23 +23,35 @@ export const HonoXIsland = ({
1823
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1924
props: any
2025
}) => {
21-
const { children, ...rest } = props
26+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
27+
const elementProps: Record<string, any> = {}
28+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
29+
const restProps: Record<string, any> = {}
30+
for (const key in props) {
31+
const value = props[key]
32+
if (isElementPropValue(value)) {
33+
elementProps[key] = value
34+
} else {
35+
restProps[key] = value
36+
}
37+
}
38+
2239
const islandState = useContext(IslandContext)
2340
return islandState[inChildren] || !islandState[inIsland] ? (
2441
// top-level or slot content
2542
<honox-island
26-
{...{ [COMPONENT_NAME]: componentName, [DATA_SERIALIZED_PROPS]: JSON.stringify(rest) }}
43+
{...{ [COMPONENT_NAME]: componentName, [DATA_SERIALIZED_PROPS]: JSON.stringify(restProps) }}
2744
>
2845
<IslandContext.Provider value={{ ...islandState, [inIsland]: true }}>
2946
<Component {...props} />
3047
</IslandContext.Provider>
31-
{children && (
32-
<template {...{ [DATA_HONO_TEMPLATE]: '' }}>
48+
{Object.entries(elementProps).map(([key, children]) => (
49+
<template {...{ [DATA_HONO_TEMPLATE]: key }}>
3350
<IslandContext.Provider value={{ ...islandState, [inChildren]: true }}>
3451
{children}
3552
</IslandContext.Provider>
3653
</template>
37-
)}
54+
))}
3855
</honox-island>
3956
) : (
4057
// nested component

test-e2e/e2e.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ test('test counter', async ({ page }) => {
1010
await container.getByText('Count: 6').click()
1111
})
1212

13+
test('test counter - child client component in props', async ({ page }) => {
14+
await page.goto('/interaction')
15+
await page.waitForSelector('body[data-client-loaded]')
16+
17+
const container = page.locator('id=slot')
18+
await container.getByText('Count: 25').click()
19+
await container.locator('button').click()
20+
await container.getByText('Count: 26').click()
21+
})
22+
1323
test('test counter - island in the same directory', async ({ page }) => {
1424
await page.goto('/directory')
1525
await page.waitForSelector('body[data-client-loaded]')

test-integration/apps.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ describe('With preserved', () => {
300300
expect(res.status).toBe(200)
301301
// hono/jsx escape a single quote to &#39;
302302
expect(await res.text()).toBe(
303-
'<!DOCTYPE html><html><head><title></title></head><body><honox-island component-name="/islands/Counter.tsx" data-serialized-props="{&quot;initial&quot;:5,&quot;id&quot;:&quot;first&quot;}"><div id="first"><p>Counter</p><p>Count: 5</p><button>Increment</button></div></honox-island><honox-island component-name="/islands/Counter.tsx" data-serialized-props="{&quot;initial&quot;:10}"><div id=""><p>Counter</p><p>Count: 10</p><button>Increment</button><div id=""><p>Counter</p><p>Count: 15</p><button>Increment</button></div></div><template data-hono-template=""><honox-island component-name="/islands/Counter.tsx" data-serialized-props="{&quot;initial&quot;:15}"><div id=""><honox-island component-name="/islands/Badge.tsx" data-serialized-props="{&quot;name&quot;:&quot;Counter&quot;}"><p>Counter</p></honox-island><p>Count: 15</p><button>Increment</button></div></honox-island></template></honox-island><script type="module" async="" src="/app/client.ts"></script></body></html>'
303+
'<!DOCTYPE html><html><head><title></title></head><body><honox-island component-name="/islands/Counter.tsx" data-serialized-props="{&quot;initial&quot;:5,&quot;id&quot;:&quot;first&quot;}"><div id="first"><p>Counter</p><p>Count: 5</p><button>Increment</button></div></honox-island><honox-island component-name="/islands/Counter.tsx" data-serialized-props="{&quot;initial&quot;:10}"><div id=""><p>Counter</p><p>Count: 10</p><button>Increment</button><div id=""><p>Counter</p><p>Count: 15</p><button>Increment</button></div></div><template data-hono-template="children"><honox-island component-name="/islands/Counter.tsx" data-serialized-props="{&quot;initial&quot;:15}"><div id=""><honox-island component-name="/islands/Badge.tsx" data-serialized-props="{&quot;name&quot;:&quot;Counter&quot;}"><p>Counter</p></honox-island><p>Count: 15</p><button>Increment</button></div></honox-island></template></honox-island><honox-island component-name="/islands/Counter.tsx" data-serialized-props="{&quot;initial&quot;:20}"><div id=""><p>Counter</p><p>Count: 20</p><button>Increment</button><div id=""><p>Counter</p><p>Count: 30</p><button>Increment</button></div><div><div id="slot"><p>Counter</p><p>Count: 25</p><button>Increment</button></div></div></div><template data-hono-template="slot"><honox-island component-name="/islands/Counter.tsx" data-serialized-props="{&quot;id&quot;:&quot;slot&quot;,&quot;initial&quot;:25}"><div id="slot"><honox-island component-name="/islands/Badge.tsx" data-serialized-props="{&quot;name&quot;:&quot;Counter&quot;}"><p>Counter</p></honox-island><p>Count: 25</p><button>Increment</button></div></honox-island></template><template data-hono-template="children"><honox-island component-name="/islands/Counter.tsx" data-serialized-props="{&quot;initial&quot;:30}"><div id=""><honox-island component-name="/islands/Badge.tsx" data-serialized-props="{&quot;name&quot;:&quot;Counter&quot;}"><p>Counter</p></honox-island><p>Count: 30</p><button>Increment</button></div></honox-island></template></honox-island><script type="module" async="" src="/app/client.ts"></script></body></html>'
304304
)
305305
})
306306

0 commit comments

Comments
 (0)