diff --git a/package-lock.json b/package-lock.json index 2a6b94e7..18c2b1fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14891,7 +14891,7 @@ }, "packages/core": { "name": "@reatom/core", - "version": "3.9.1", + "version": "3.9.2", "license": "MIT" }, "packages/core-v1": { @@ -14952,7 +14952,7 @@ }, "packages/effects": { "name": "@reatom/effects", - "version": "3.10.2", + "version": "3.11.0", "license": "MIT", "dependencies": { "@reatom/core": "^3.2.0", @@ -15015,17 +15015,17 @@ }, "packages/framework": { "name": "@reatom/framework", - "version": "3.4.56", + "version": "3.4.57", "license": "MIT", "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" } }, "packages/hooks": { @@ -15156,7 +15156,7 @@ }, "packages/npm-react": { "name": "@reatom/npm-react", - "version": "3.10.3", + "version": "3.10.4", "license": "MIT", "dependencies": { "@reatom/core": "^3.5.0", @@ -15283,7 +15283,7 @@ }, "packages/testing": { "name": "@reatom/testing", - "version": "3.4.7", + "version": "3.4.8", "license": "MIT", "dependencies": { "@reatom/core": "^3.5.0", @@ -15326,7 +15326,7 @@ }, "packages/utils": { "name": "@reatom/utils", - "version": "3.11.0", + "version": "3.11.1", "license": "MIT" }, "packages/web": { diff --git a/packages/framework/package.json b/packages/framework/package.json index 999cd1a8..4e44b50e 100644 --- a/packages/framework/package.json +++ b/packages/framework/package.json @@ -1,6 +1,6 @@ { "name": "@reatom/framework", - "version": "3.4.56", + "version": "3.4.57", "private": false, "sideEffects": false, "description": "Reatom for framework", @@ -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", diff --git a/packages/jsx/src/index.test.tsx b/packages/jsx/src/index.test.tsx index 5c65dbf1..b1aa2933 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,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 = `` + 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
    `) +})) + diff --git a/packages/jsx/src/index.ts b/packages/jsx/src/index.ts index 97d42d1e..d44b7db1 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,39 @@ export const reatomJsx = (ctx: Ctx, DOM: DomApis = globalThis.window) => { } } + const walkAtom = (ctx: Ctx, parent: JSX.Element, anAtom: Atom) => { + 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 ??= {} @@ -209,6 +240,9 @@ 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['children']) => { if (Array.isArray(child)) { for (let i = 0; i < child.length; i++) walk(child[i]) @@ -216,49 +250,9 @@ 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) + 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) } } } @@ -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) => { @@ -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 diff --git a/packages/jsx/src/jsx.d.ts b/packages/jsx/src/jsx.d.ts index 52315a62..32de477e 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 =