Skip to content

Commit

Permalink
fix(jsx): render a factory returning an atom
Browse files Browse the repository at this point in the history
  • Loading branch information
artalar authored and kasperskei committed Jan 3, 2025
1 parent a906006 commit fb647ec
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 85 deletions.
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions packages/framework/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reatom/framework",
"version": "3.4.56",
"version": "3.4.57",
"private": false,
"sideEffects": false,
"description": "Reatom for framework",
Expand All @@ -19,13 +19,13 @@
},
"dependencies": {
"@reatom/async": "^3.16.4",
"@reatom/core": "^3.9.1",
"@reatom/effects": "^3.10.2",
"@reatom/core": "^3.9.2",
"@reatom/effects": "^3.11.0",
"@reatom/hooks": "^3.6.0",
"@reatom/lens": "^3.11.6",
"@reatom/logger": "^3.8.4",
"@reatom/primitives": "^3.7.3",
"@reatom/utils": "^3.11.0"
"@reatom/utils": "^3.11.1"
},
"author": "artalar",
"license": "MIT",
Expand Down
68 changes: 43 additions & 25 deletions packages/jsx/src/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,16 @@ it('children updates', setup((ctx, h, hf, mount, parent) => {

mount(parent, element)

assert.is(element.childNodes.length, 3)
assert.is(element.childNodes[1]?.textContent, 'foo')
assert.is(element.childNodes[2], a)
assert.is(element.childNodes.length, 5)
assert.is(element.childNodes[2]?.textContent, 'foo')
assert.is(element.childNodes[4], a)

val(ctx, 'bar')
assert.is(element.childNodes[1]?.textContent, 'bar')
assert.is(element.childNodes[2]?.textContent, 'bar')

assert.is(element.childNodes[2], a)
assert.is(element.childNodes[4], a)
route(ctx, 'b')
assert.is(element.childNodes[2], b)
assert.is(element.childNodes[4], b)
}))

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

mount(parent, element)

assert.is(element.childNodes.length, 1)
assert.is(element.childNodes.length, 2)

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

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

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

it('array children', setup((ctx, h, hf, mount, parent) => {
const n = atom(1)
const list = atom((ctx) => Array.from({ length: ctx.spy(n) }, (_, i) => <li>{i + 1}</li>))

assert.throws(() => {
mount(
parent,
<ul>
{list /* expected TS error */ as any}
<br />
</ul>,
)
})
const list = atom((ctx) => (<>
{...Array.from({ length: ctx.spy(n) }, (_, i) => <li>{i + 1}</li>)}
</>))

const element = <ul>{list}</ul>
const element = (
<ul>
{list}
<br />
</ul>
)

assert.is(element.childNodes.length, 1)
mount(parent, element)

assert.is(element.childNodes.length, 3)
assert.is(element.textContent, '1')

n(ctx, 2)
assert.is(element.childNodes.length, 2)
assert.is(element.childNodes.length, 4)
assert.is(element.textContent, '12')
}))

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

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

assert.is(element.innerHTML, '<div>div</div>')
assert.is(element.innerHTML, '<!--html--><div>div</div>')
}))

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

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

assert.is(element.innerHTML, '<svg>svg</svg>')
assert.is(element.innerHTML, '<!--svg--><svg>svg</svg>')
}))

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

assert.is(component.getAttribute('style'), 'left: 0px; bottom: 0px;')
}))

it('render updated atom elements', setup((ctx, h, hf, mount, parent) => {
const name = 'child'
const target = `<!--${name}-->`
const childAtom = atom<Node | string>(<>
<div>div</div>
<p>p</p>
</>, name)

const element = <div>{childAtom}</div>
assert.is(element.innerHTML, `<div>${target}div>div</div><p>p</p></div>`)

childAtom(ctx, <span>span</span>)
assert.is(element.innerHTML, `<div>${target}span>span</span></div>`)

childAtom(ctx, 'text')
assert.is(element.innerHTML, `<div>${target}text</div>`)
}))

91 changes: 44 additions & 47 deletions packages/jsx/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ type DomApis = Pick<
const isSkipped = (value: unknown): value is boolean | '' | null | undefined =>
typeof value === 'boolean' || value === '' || value == null

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

const walkAtom = (ctx: Ctx, parent: JSX.Element, anAtom: Atom<JSX.ElementPrimitiveChildren>) => {
const fragment = DOM.document.createDocumentFragment()
const target = DOM.document.createComment(anAtom.__reatom.name ?? '')
fragment.append(target)

let childNodes: ChildNode[] = []
const un = ctx.subscribe(anAtom, (v): void => {
childNodes.forEach((node) => node.remove())

if (v instanceof DOM.Node) {
childNodes = v instanceof DOM.DocumentFragment ? [...v.childNodes] : [v as ChildNode]
target.after(v)
} else if (isSkipped(v)) {
childNodes = []
} else {
const node = DOM.document.createTextNode(String(v))
childNodes = [node]
target.after(node)
}
})

if (!unsubscribesMap.get(target)) unsubscribesMap.set(target, [])
unlink(target, un)

parent.append(fragment)
}

let h = (tag: any, props: Rec, ...children: any[]) => {
if (tag === hf) return children
if (tag === hf) {
const fragment = DOM.document.createDocumentFragment()
fragment.append(...children)
return fragment
}

props ??= {}

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

/**
* @todo Explore adding elements to a DocumentFragment before adding them to a Document.
*/
let walk = (child: JSX.DOMAttributes<JSX.Element>['children']) => {
if (Array.isArray(child)) {
for (let i = 0; i < child.length; i++) walk(child[i])
} else {
if (isLinkedListAtom(child)) {
walkLinkedList(ctx, element, child)
} else if (isAtom(child)) {
let innerChild = DOM.document.createTextNode('') as ChildNode | DocumentFragment
let error: any
var un: undefined | Unsubscribe = ctx.subscribe(child, (v): void => {
try {
if (un && !innerChild.isConnected && innerChild instanceof DOM.DocumentFragment === false) {
un()
} else {
throwReatomError(
Array.isArray(v) && children.length > 1,
'array children with other children are not supported',
)

if (v instanceof DOM.Element) {
let list = unsubscribesMap.get(v)
if (!list) unsubscribesMap.set(v, (list = []))

if (un) element.replaceChild(v, innerChild)
innerChild = v
} else if (Array.isArray(v)) {
if (un) element.replaceChildren(...v)
else {
const fragment = new DOM.DocumentFragment()
v.forEach((el) => fragment.append(el))
innerChild = fragment
}
} else {
// TODO more tests
innerChild.textContent = isSkipped(v) ? '' : String(v)
}
}
} catch (e) {
error = e
}
})
if (error) throw error
unlink(element, un)
element.appendChild(innerChild)
walkAtom(ctx, element, child)
} else if (!isSkipped(child)) {
element.appendChild(
isObject(child) && 'nodeType' in child
? (child as JSX.Element)
: DOM.document.createTextNode(String(child)),
)
element.append(child as Node | string)
}
}
}
Expand All @@ -270,7 +264,10 @@ export const reatomJsx = (ctx: Ctx, DOM: DomApis = globalThis.window) => {
return element
}

/** Fragment */
/**
* Fragment.
* @todo Describe a function as a component.
*/
let hf = () => {}

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

do {
const node = walker.currentNode as Element
Expand Down
1 change: 1 addition & 0 deletions packages/jsx/src/jsx.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type ElementsAttributesAtomMaybe<T extends Record<keyof any, any>> = {
export namespace JSX {
type Element = HTMLElement | SVGElement

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

type ElementChildren =
Expand Down

0 comments on commit fb647ec

Please sign in to comment.