From ea4617e3d9f8bd680b1fc8b1ef2d28eb00ea0cb6 Mon Sep 17 00:00:00 2001 From: artalar Date: Sat, 25 Jan 2025 15:44:28 +0300 Subject: [PATCH] feat(jsx): add Bind component --- packages/jsx/README.md | 92 +++++++++++++++++++++------------ packages/jsx/src/index.test.tsx | 46 ++++++++++++++++- packages/jsx/src/index.ts | 45 ++++++++++------ 3 files changed, 132 insertions(+), 51 deletions(-) diff --git a/packages/jsx/README.md b/packages/jsx/README.md index 5bcc655c5..23a2f0c75 100644 --- a/packages/jsx/README.md +++ b/packages/jsx/README.md @@ -131,15 +131,13 @@ Object-valued `style` prop applies styles granularly: `style={{top: 0, display: `false`, `null` and `undefined` style values remove the property. Non-string style values are stringified (we don't add `px` to numeric values automatically). Incorrect: + ```tsx -
ctx.spy(bool) - ? ({top: 0}) - : ({bottom: 0}))} ->
+
(ctx.spy(bool) ? { top: 0 } : { bottom: 0 }))}>
``` Correct: + ```tsx
ctx.spy(bool) @@ -172,7 +170,7 @@ cn(['first', atom('second')]) // Atom<'first second'> /** The `active` class will be determined by the truthiness of the data property `isActiveAtom`. */ cn({ active: isActiveAtom }) // Atom<'active' | ''> -cn((ctx) => ctx.spy(isActiveAtom) ? 'active' : undefined) // Atom<'active' | ''> +cn((ctx) => (ctx.spy(isActiveAtom) ? 'active' : undefined)) // Atom<'active' | ''> ``` The `cn` function supports various complex data combinations, making it easier to declaratively describe classes for complex UI components. @@ -190,11 +188,7 @@ const Button = (props) => { }, ]) - return ( - - ) + return } ``` @@ -253,9 +247,7 @@ In Reatom, there is no concept of "rerender" like React. Instead, we have a spec ```tsx
(ctx.spy(valid) - ? { disabled: true, readonly: true } - : { disabled: false, readonly: false }))} + $spread={atom((ctx) => (ctx.spy(valid) ? { disabled: true, readonly: true } : { disabled: false, readonly: false }))} /> ``` @@ -276,11 +268,8 @@ If you need to use SVG as a string, you can choose from these options: Option 1: ```tsx -const SvgIcon = (props: {svg: string}) => { - const svgEl = new DOMParser() - .parseFromString(props.svg, 'image/svg+xml') - .children - .item(0) as SVGElement +const SvgIcon = (props: { svg: string }) => { + const svgEl = new DOMParser().parseFromString(props.svg, 'image/svg+xml').children.item(0) as SVGElement return svgEl } ``` @@ -301,26 +290,30 @@ const SvgIcon = (props: {svg: string}) => { The `ref` property is used to create and track references to DOM elements, allowing actions to be performed when these elements are mounted and unmounted. - ```tsx - + ``` Mounting and unmounting functions are called in order from child to parent. ```tsx -
{ - console.log('mount', 'parent') - return () => console.log('unmount', 'parent') -}}> - { - console.log('mount', 'child') - return () => console.log('unmount', 'child') - }}> - +
{ + console.log('mount', 'parent') + return () => console.log('unmount', 'parent') + }} +> + { + console.log('mount', 'child') + return () => console.log('unmount', 'child') + }} + >
``` @@ -367,6 +360,36 @@ const MyWidget = () => { } ``` --> +## Utilities + +### css utility + +You can import `css` function from `@reatom/jsx` to describe separate css-in-js styles with syntax highlight and prettier support. Also, this function skips all falsy values, except `0`. + +```tsx +import { css } from '@reatom/jsx' + +const styles = css` + color: red; + background: blue; + ${somePredicate && 'border: 0;'} +` +``` + +### Bind utility + +You can use `Bind` component to use all reatom/jsx features on top of existed element. For example, there are some library, which creates an element and returns it to you and you want to add some reactive properties to it. + +```tsx +import { Bind } from '@reatom/jsx' + +const MyComponent = () => { + const container = new SomeLibrary() + + return (ctx.spy(visible) ? 'active' : 'disabled'))} /> +} +``` + ### TypeScript To type your custom component props accepting general HTML attributes, for example for a `div` element, you should extend `JSX.HTMLAttributes`. However, if you want to define props for a specific HTML element you should use it name in the type name, like in the code below. @@ -419,8 +442,9 @@ export const Form = () => { These limitations will be fixed in the feature - No DOM-less SSR (requires a DOM API implementation like `linkedom` to be provided) -- No keyed lists support +- No keyed lists support (use linked lists instead) - A component should have no more than one root element. If this interferes with the layout, you can wrap the parent elements in another element with the style `display: "contents"`: + ```tsx
diff --git a/packages/jsx/src/index.test.tsx b/packages/jsx/src/index.test.tsx index 3e34550eb..b527a9084 100644 --- a/packages/jsx/src/index.test.tsx +++ b/packages/jsx/src/index.test.tsx @@ -3,9 +3,10 @@ import { createTestCtx, mockFn, type TestCtx } from '@reatom/testing' import { type Fn, type Rec, atom } from '@reatom/core' import { reatomLinkedList } from '@reatom/primitives' import { isConnected } from '@reatom/hooks' -import { reatomJsx, type JSX } from '.' import { sleep } from '@reatom/utils' +import { Bind, reatomJsx, type JSX } from '.' + type SetupFn = ( ctx: TestCtx, h: (tag: any, props: Rec, ...children: any[]) => any, @@ -738,3 +739,46 @@ it( assert.is(element.innerHTML, expect1) }), ) + +it( + 'Bind', + setup(async (ctx, h, hf, mount, parent) => { + const div = (
) as HTMLDivElement + const input = () as HTMLInputElement + const svg = () as SVGSVGElement + + const inputState = atom('42') + + const testDiv = ( + + ) + const testInput = ( + inputState(ctx, e.currentTarget.value)} /> + ) + const testSvg = ( + + + + ) + + mount( + parent, +
+ {testDiv} + {testInput} + {testSvg} +
, + ) + + await sleep() + + inputState(ctx, '43') + + assert.is(input.value, '43') + assert.is(testSvg.innerHTML, '') + }), +) diff --git a/packages/jsx/src/index.ts b/packages/jsx/src/index.ts index b43a84936..47b7502d8 100644 --- a/packages/jsx/src/index.ts +++ b/packages/jsx/src/index.ts @@ -1,7 +1,7 @@ import { action, Atom, AtomMut, createCtx, Ctx, Fn, isAtom, Rec, throwReatomError, Unsubscribe } from '@reatom/core' import { isObject, random } from '@reatom/utils' import { type LinkedList, type LLNode, isLinkedListAtom, LL_NEXT } from '@reatom/primitives' -import type { JSX } from './jsx' +import type { AttributesAtomMaybe, JSX } from './jsx' declare type JSXElement = JSX.Element @@ -179,33 +179,42 @@ export const reatomJsx = (ctx: Ctx, DOM: DomApis = globalThis.window) => { if (tag === hf) { const fragment = DOM.document.createDocumentFragment() - children = children.map((child) => isAtom(child) ? walkAtom(ctx, child) : child) - fragment.append(...children) + for (let i = 0; i < children.length; i++) { + const child = children[i] + fragment.append(isAtom(child) ? walkAtom(ctx, child) : child) + } return fragment } props ??= {} + let element: JSX.Element + if (typeof tag === 'function') { - if (children.length) { - props.children = children - } + if (tag === Bind) { + element = props.element + props.element = undefined + } else { + if (children.length) { + props.children = children + } - let _name = name - try { - name = tag.name - return tag(props) - } finally { - name = _name + let _name = name + try { + name = tag.name + return tag(props) + } finally { + name = _name + } } + } else { + element = tag.startsWith('svg:') + ? DOM.document.createElementNS('http://www.w3.org/2000/svg', tag.slice(4)) + : DOM.document.createElement(tag) } if ('children' in props) children = props.children - let element: JSX.Element = tag.startsWith('svg:') - ? DOM.document.createElementNS('http://www.w3.org/2000/svg', tag.slice(4)) - : DOM.document.createElement(tag) - for (let k in props) { if (k !== 'children') { let prop = props[k] @@ -324,3 +333,7 @@ export const css = (strings: TemplateStringsArray, ...values: any[]) => { } return result } + +export const Bind = ( + props: { element: T } & AttributesAtomMaybe> & JSX.DOMAttributes>, +): T => props.element