Skip to content

Commit fb647ec

Browse files
artalarkasperskei
authored andcommitted
fix(jsx): render a factory returning an atom
1 parent a906006 commit fb647ec

File tree

5 files changed

+101
-85
lines changed

5 files changed

+101
-85
lines changed

package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/framework/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@reatom/framework",
3-
"version": "3.4.56",
3+
"version": "3.4.57",
44
"private": false,
55
"sideEffects": false,
66
"description": "Reatom for framework",
@@ -19,13 +19,13 @@
1919
},
2020
"dependencies": {
2121
"@reatom/async": "^3.16.4",
22-
"@reatom/core": "^3.9.1",
23-
"@reatom/effects": "^3.10.2",
22+
"@reatom/core": "^3.9.2",
23+
"@reatom/effects": "^3.11.0",
2424
"@reatom/hooks": "^3.6.0",
2525
"@reatom/lens": "^3.11.6",
2626
"@reatom/logger": "^3.8.4",
2727
"@reatom/primitives": "^3.7.3",
28-
"@reatom/utils": "^3.11.0"
28+
"@reatom/utils": "^3.11.1"
2929
},
3030
"author": "artalar",
3131
"license": "MIT",

packages/jsx/src/index.test.tsx

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,16 @@ it('children updates', setup((ctx, h, hf, mount, parent) => {
7777

7878
mount(parent, element)
7979

80-
assert.is(element.childNodes.length, 3)
81-
assert.is(element.childNodes[1]?.textContent, 'foo')
82-
assert.is(element.childNodes[2], a)
80+
assert.is(element.childNodes.length, 5)
81+
assert.is(element.childNodes[2]?.textContent, 'foo')
82+
assert.is(element.childNodes[4], a)
8383

8484
val(ctx, 'bar')
85-
assert.is(element.childNodes[1]?.textContent, 'bar')
85+
assert.is(element.childNodes[2]?.textContent, 'bar')
8686

87-
assert.is(element.childNodes[2], a)
87+
assert.is(element.childNodes[4], a)
8888
route(ctx, 'b')
89-
assert.is(element.childNodes[2], b)
89+
assert.is(element.childNodes[4], b)
9090
}))
9191

9292
it('dynamic children', setup((ctx, h, hf, mount, parent) => {
@@ -96,14 +96,14 @@ it('dynamic children', setup((ctx, h, hf, mount, parent) => {
9696

9797
mount(parent, element)
9898

99-
assert.is(element.childNodes.length, 1)
99+
assert.is(element.childNodes.length, 2)
100100

101101
children(ctx, <div>Hello, world!</div>)
102-
assert.is(element.childNodes[0]?.textContent, 'Hello, world!')
102+
assert.is(element.childNodes[1]?.textContent, 'Hello, world!')
103103

104104
const inner = <span>inner</span>
105105
children(ctx, <div>{inner}</div>)
106-
assert.is(element.childNodes[0]?.childNodes[0], inner)
106+
assert.is(element.childNodes[1]?.childNodes[0], inner)
107107

108108
const before = atom('before', 'before')
109109
const after = atom('after', 'after')
@@ -159,25 +159,24 @@ it('fragment as child', setup((ctx, h, hf, mount, parent) => {
159159

160160
it('array children', setup((ctx, h, hf, mount, parent) => {
161161
const n = atom(1)
162-
const list = atom((ctx) => Array.from({ length: ctx.spy(n) }, (_, i) => <li>{i + 1}</li>))
163-
164-
assert.throws(() => {
165-
mount(
166-
parent,
167-
<ul>
168-
{list /* expected TS error */ as any}
169-
<br />
170-
</ul>,
171-
)
172-
})
162+
const list = atom((ctx) => (<>
163+
{...Array.from({ length: ctx.spy(n) }, (_, i) => <li>{i + 1}</li>)}
164+
</>))
173165

174-
const element = <ul>{list}</ul>
166+
const element = (
167+
<ul>
168+
{list}
169+
<br />
170+
</ul>
171+
)
175172

176-
assert.is(element.childNodes.length, 1)
173+
mount(parent, element)
174+
175+
assert.is(element.childNodes.length, 3)
177176
assert.is(element.textContent, '1')
178177

179178
n(ctx, 2)
180-
assert.is(element.childNodes.length, 2)
179+
assert.is(element.childNodes.length, 4)
181180
assert.is(element.textContent, '12')
182181
}))
183182

@@ -288,15 +287,15 @@ it('render HTMLElement atom', setup((ctx, h, hf, mount, parent) => {
288287

289288
const element = <div>{htmlAtom}</div>
290289

291-
assert.is(element.innerHTML, '<div>div</div>')
290+
assert.is(element.innerHTML, '<!--html--><div>div</div>')
292291
}))
293292

294293
it('render SVGElement atom', setup((ctx, h, hf, mount, parent) => {
295294
const svgAtom = atom(<svg:svg>svg</svg:svg>, 'svg')
296295

297296
const element = <div>{svgAtom}</div>
298297

299-
assert.is(element.innerHTML, '<svg>svg</svg>')
298+
assert.is(element.innerHTML, '<!--svg--><svg>svg</svg>')
300299
}))
301300

302301
it('custom component', setup((ctx, h, hf, mount, parent) => {
@@ -529,3 +528,22 @@ it('style object update', setup((ctx, h, hf, mount, parent) => {
529528

530529
assert.is(component.getAttribute('style'), 'left: 0px; bottom: 0px;')
531530
}))
531+
532+
it('render updated atom elements', setup((ctx, h, hf, mount, parent) => {
533+
const name = 'child'
534+
const target = `<!--${name}-->`
535+
const childAtom = atom<Node | string>(<>
536+
<div>div</div>
537+
<p>p</p>
538+
</>, name)
539+
540+
const element = <div>{childAtom}</div>
541+
assert.is(element.innerHTML, `<div>${target}div>div</div><p>p</p></div>`)
542+
543+
childAtom(ctx, <span>span</span>)
544+
assert.is(element.innerHTML, `<div>${target}span>span</span></div>`)
545+
546+
childAtom(ctx, 'text')
547+
assert.is(element.innerHTML, `<div>${target}text</div>`)
548+
}))
549+

packages/jsx/src/index.ts

Lines changed: 44 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ type DomApis = Pick<
1919
const isSkipped = (value: unknown): value is boolean | '' | null | undefined =>
2020
typeof value === 'boolean' || value === '' || value == null
2121

22-
let unsubscribesMap = new WeakMap<Element, Array<Fn>>()
23-
let unlink = (parent: JSX.Element, un: Unsubscribe) => {
22+
let unsubscribesMap = new WeakMap<Node, Array<Fn>>()
23+
let unlink = (parent: Node, un: Unsubscribe) => {
2424
// check the connection in the next tick
2525
// to give the user (programmer) an ability
2626
// to put the created element in the dom
@@ -140,8 +140,39 @@ export const reatomJsx = (ctx: Ctx, DOM: DomApis = globalThis.window) => {
140140
}
141141
}
142142

143+
const walkAtom = (ctx: Ctx, parent: JSX.Element, anAtom: Atom<JSX.ElementPrimitiveChildren>) => {
144+
const fragment = DOM.document.createDocumentFragment()
145+
const target = DOM.document.createComment(anAtom.__reatom.name ?? '')
146+
fragment.append(target)
147+
148+
let childNodes: ChildNode[] = []
149+
const un = ctx.subscribe(anAtom, (v): void => {
150+
childNodes.forEach((node) => node.remove())
151+
152+
if (v instanceof DOM.Node) {
153+
childNodes = v instanceof DOM.DocumentFragment ? [...v.childNodes] : [v as ChildNode]
154+
target.after(v)
155+
} else if (isSkipped(v)) {
156+
childNodes = []
157+
} else {
158+
const node = DOM.document.createTextNode(String(v))
159+
childNodes = [node]
160+
target.after(node)
161+
}
162+
})
163+
164+
if (!unsubscribesMap.get(target)) unsubscribesMap.set(target, [])
165+
unlink(target, un)
166+
167+
parent.append(fragment)
168+
}
169+
143170
let h = (tag: any, props: Rec, ...children: any[]) => {
144-
if (tag === hf) return children
171+
if (tag === hf) {
172+
const fragment = DOM.document.createDocumentFragment()
173+
fragment.append(...children)
174+
return fragment
175+
}
145176

146177
props ??= {}
147178

@@ -209,56 +240,19 @@ export const reatomJsx = (ctx: Ctx, DOM: DomApis = globalThis.window) => {
209240
}
210241
}
211242

243+
/**
244+
* @todo Explore adding elements to a DocumentFragment before adding them to a Document.
245+
*/
212246
let walk = (child: JSX.DOMAttributes<JSX.Element>['children']) => {
213247
if (Array.isArray(child)) {
214248
for (let i = 0; i < child.length; i++) walk(child[i])
215249
} else {
216250
if (isLinkedListAtom(child)) {
217251
walkLinkedList(ctx, element, child)
218252
} else if (isAtom(child)) {
219-
let innerChild = DOM.document.createTextNode('') as ChildNode | DocumentFragment
220-
let error: any
221-
var un: undefined | Unsubscribe = ctx.subscribe(child, (v): void => {
222-
try {
223-
if (un && !innerChild.isConnected && innerChild instanceof DOM.DocumentFragment === false) {
224-
un()
225-
} else {
226-
throwReatomError(
227-
Array.isArray(v) && children.length > 1,
228-
'array children with other children are not supported',
229-
)
230-
231-
if (v instanceof DOM.Element) {
232-
let list = unsubscribesMap.get(v)
233-
if (!list) unsubscribesMap.set(v, (list = []))
234-
235-
if (un) element.replaceChild(v, innerChild)
236-
innerChild = v
237-
} else if (Array.isArray(v)) {
238-
if (un) element.replaceChildren(...v)
239-
else {
240-
const fragment = new DOM.DocumentFragment()
241-
v.forEach((el) => fragment.append(el))
242-
innerChild = fragment
243-
}
244-
} else {
245-
// TODO more tests
246-
innerChild.textContent = isSkipped(v) ? '' : String(v)
247-
}
248-
}
249-
} catch (e) {
250-
error = e
251-
}
252-
})
253-
if (error) throw error
254-
unlink(element, un)
255-
element.appendChild(innerChild)
253+
walkAtom(ctx, element, child)
256254
} else if (!isSkipped(child)) {
257-
element.appendChild(
258-
isObject(child) && 'nodeType' in child
259-
? (child as JSX.Element)
260-
: DOM.document.createTextNode(String(child)),
261-
)
255+
element.append(child as Node | string)
262256
}
263257
}
264258
}
@@ -270,7 +264,10 @@ export const reatomJsx = (ctx: Ctx, DOM: DomApis = globalThis.window) => {
270264
return element
271265
}
272266

273-
/** Fragment */
267+
/**
268+
* Fragment.
269+
* @todo Describe a function as a component.
270+
*/
274271
let hf = () => {}
275272

276273
let mount = (target: Element, child: Element) => {
@@ -283,7 +280,7 @@ export const reatomJsx = (ctx: Ctx, DOM: DomApis = globalThis.window) => {
283280
* @see https://stackoverflow.com/a/64551276
284281
* @note A custom NodeFilter function slows down performance by 1.5 times.
285282
*/
286-
const walker = DOM.document.createTreeWalker(removedNode, 1)
283+
const walker = DOM.document.createTreeWalker(removedNode, 1 | 128)
287284

288285
do {
289286
const node = walker.currentNode as Element

packages/jsx/src/jsx.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type ElementsAttributesAtomMaybe<T extends Record<keyof any, any>> = {
1919
export namespace JSX {
2020
type Element = HTMLElement | SVGElement
2121

22+
/** @todo Try replacing `Node | Element` with `ChildNode`. */
2223
type ElementPrimitiveChildren = Node | Element | (string & {}) | number | boolean | null | undefined
2324

2425
type ElementChildren =

0 commit comments

Comments
 (0)