From 03e8a25739261c1b40f2ea8e3d7dbf2fa1ec8b59 Mon Sep 17 00:00:00 2001 From: artalar Date: Tue, 31 Dec 2024 01:12:20 +0300 Subject: [PATCH] fix(jsx): render a factory returning an atom --- packages/jsx/src/index.test.tsx | 129 +++++++++++++++++++++++++------- packages/jsx/src/index.ts | 98 ++++++++++++------------ packages/jsx/src/jsx.d.ts | 1 + 3 files changed, 156 insertions(+), 72 deletions(-) diff --git a/packages/jsx/src/index.test.tsx b/packages/jsx/src/index.test.tsx index 5c65dbf1d..d3fe3cf7c 100644 --- a/packages/jsx/src/index.test.tsx +++ b/packages/jsx/src/index.test.tsx @@ -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) => { @@ -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,
Hello, world!
) - assert.is(element.childNodes[0]?.textContent, 'Hello, world!') + assert.is(element.childNodes[1]?.textContent, 'Hello, world!') const inner = inner children(ctx,
{inner}
) - 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') @@ -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) =>
  • {i + 1}
  • )) - - assert.throws(() => { - mount( - parent, - , - ) - }) + const list = atom((ctx) => (<> + {...Array.from({ length: ctx.spy(n) }, (_, i) =>
  • {i + 1}
  • )} + )) - const element = + const element = ( + + ) - 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') })) @@ -288,7 +287,7 @@ it('render HTMLElement atom', setup((ctx, h, hf, mount, parent) => { const element =
    {htmlAtom}
    - assert.is(element.innerHTML, '
    div
    ') + assert.is(element.innerHTML, '
    div
    ') })) it('render SVGElement atom', setup((ctx, h, hf, mount, parent) => { @@ -296,7 +295,7 @@ it('render SVGElement atom', setup((ctx, h, hf, mount, parent) => { const element =
    {svgAtom}
    - assert.is(element.innerHTML, 'svg') + assert.is(element.innerHTML, 'svg') })) it('custom component', setup((ctx, h, hf, mount, parent) => { @@ -529,3 +528,83 @@ it('style object update', setup((ctx, h, hf, mount, parent) => { assert.is(component.getAttribute('style'), 'left: 0px; bottom: 0px;') })) + +it('render different atom children', setup((ctx, h, hf, mount, parent) => { + const name = 'child' + const target = `` + const childAtom = atom(<> +
    div
    +

    p

    + , name) + + const element =
    {childAtom}
    + assert.is(element.innerHTML, `
    ${target}div>div

    p

    `) + + childAtom(ctx, span) + assert.is(element.innerHTML, `
    ${target}span>span
    `) + + childAtom(ctx, 'text') + assert.is(element.innerHTML, `
    ${target}text
    `) +})) + +it('render atom fragments', setup((ctx, h, hf, mount, parent) => { + const bool1Atom = atom(false) + const bool2Atom = atom(false) + + const element = ( +
    +

    0

    + {atom( + (ctx) => ctx.spy(bool1Atom) + ? <> +

    1

    + {atom( + (ctx) => ctx.spy(bool2Atom) + ? <> +

    2

    +

    3

    + + : undefined, + '2' + )} +

    4

    + + : undefined, + '1', + )} +

    5

    +
    + ) + + const expect1 = '

    0

    5

    ' + const expect2 = '

    0

    1

    4

    5

    ' + const expect3 = '

    0

    1

    2

    3

    4

    5

    ' + + bool1Atom(ctx, false) + bool2Atom(ctx, false) + assert.is(element.innerHTML, expect1) + + bool1Atom(ctx, false) + bool2Atom(ctx, true) + assert.is(element.innerHTML, expect1) + + bool1Atom(ctx, true) + bool2Atom(ctx, false) + assert.is(element.innerHTML, expect2) + + bool1Atom(ctx, true) + bool2Atom(ctx, true) + assert.is(element.innerHTML, expect3) + + bool1Atom(ctx, true) + bool2Atom(ctx, false) + assert.is(element.innerHTML, expect2) + + bool1Atom(ctx, false) + bool2Atom(ctx, true) + assert.is(element.innerHTML, expect1) + + bool1Atom(ctx, false) + bool2Atom(ctx, false) + assert.is(element.innerHTML, expect1) +})) diff --git a/packages/jsx/src/index.ts b/packages/jsx/src/index.ts index 97d42d1e5..550019e94 100644 --- a/packages/jsx/src/index.ts +++ b/packages/jsx/src/index.ts @@ -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>() -let unlink = (parent: JSX.Element, un: Unsubscribe) => { +let unsubscribesMap = new WeakMap>() +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 @@ -140,8 +140,44 @@ export const reatomJsx = (ctx: Ctx, DOM: DomApis = globalThis.window) => { } } + const walkAtom = (ctx: Ctx, anAtom: Atom): DocumentFragment => { + 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) + + return fragment + } + let h = (tag: any, props: Rec, ...children: any[]) => { - if (tag === hf) return children + if (isAtom(tag)) { + return walkAtom(ctx, tag) + } + + if (tag === hf) { + const fragment = DOM.document.createDocumentFragment() + children = children.map((child) => isAtom(child) ? walkAtom(ctx, child) : child) + fragment.append(...children) + return fragment + } props ??= {} @@ -209,6 +245,10 @@ export const reatomJsx = (ctx: Ctx, DOM: DomApis = globalThis.window) => { } } + /** + * @todo Explore adding elements to a DocumentFragment before adding them to a Document. + * @see https://www.measurethat.net/Benchmarks/Show/13274 + */ let walk = (child: JSX.DOMAttributes['children']) => { if (Array.isArray(child)) { for (let i = 0; i < child.length; i++) walk(child[i]) @@ -216,49 +256,10 @@ export const reatomJsx = (ctx: Ctx, DOM: DomApis = globalThis.window) => { 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) + const fragment = walkAtom(ctx, child) + element.append(fragment) } 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) } } } @@ -270,7 +271,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) => { @@ -283,7 +287,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 diff --git a/packages/jsx/src/jsx.d.ts b/packages/jsx/src/jsx.d.ts index 52315a62b..32de477ec 100644 --- a/packages/jsx/src/jsx.d.ts +++ b/packages/jsx/src/jsx.d.ts @@ -19,6 +19,7 @@ type ElementsAttributesAtomMaybe> = { 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 =