Skip to content

Commit

Permalink
feat(jsx): add Bind component
Browse files Browse the repository at this point in the history
  • Loading branch information
artalar committed Jan 25, 2025
1 parent 8a0bd4f commit ea4617e
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 51 deletions.
92 changes: 58 additions & 34 deletions packages/jsx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<div
style={atom((ctx) => ctx.spy(bool)
? ({top: 0})
: ({bottom: 0}))}
></div>
<div style={atom((ctx) => (ctx.spy(bool) ? { top: 0 } : { bottom: 0 }))}></div>
```

Correct:

```tsx
<div
style={atom((ctx) => ctx.spy(bool)
Expand Down Expand Up @@ -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.
Expand All @@ -190,11 +188,7 @@ const Button = (props) => {
},
])

return (
<button class={classNameAtom}>
{props.children}
</button>
)
return <button class={classNameAtom}>{props.children}</button>
}
```

Expand Down Expand Up @@ -253,9 +247,7 @@ In Reatom, there is no concept of "rerender" like React. Instead, we have a spec

```tsx
<div
$spread={atom((ctx) => (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 }))}
/>
```

Expand All @@ -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
}
```
Expand All @@ -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
<button ref={(ctx: Ctx, el: HTMLButtonElement) => {
el.focus()
return (ctx: Ctx, el: HTMLButtonElement) => el.blur()
}}></button>
<button
ref={(ctx: Ctx, el: HTMLButtonElement) => {
el.focus()
return (ctx: Ctx, el: HTMLButtonElement) => el.blur()
}}
></button>
```

Mounting and unmounting functions are called in order from child to parent.

```tsx
<div ref={(ctx: Ctx, el: HTMLDivElement) => {
console.log('mount', 'parent')
return () => console.log('unmount', 'parent')
}}>
<span ref={(ctx: Ctx, el: HTMLSpanElement) => {
console.log('mount', 'child')
return () => console.log('unmount', 'child')
}}>
</span>
<div
ref={(ctx: Ctx, el: HTMLDivElement) => {
console.log('mount', 'parent')
return () => console.log('unmount', 'parent')
}}
>
<span
ref={(ctx: Ctx, el: HTMLSpanElement) => {
console.log('mount', 'child')
return () => console.log('unmount', 'child')
}}
></span>
</div>
```

Expand Down Expand Up @@ -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 <Bind element={container} class={atom((ctx) => (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.
Expand Down Expand Up @@ -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
<div style={'display: "contents";'}>
<div class="parent-1">
Expand Down
46 changes: 45 additions & 1 deletion packages/jsx/src/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -738,3 +739,46 @@ it(
assert.is(element.innerHTML, expect1)
}),
)

it(
'Bind',
setup(async (ctx, h, hf, mount, parent) => {
const div = (<div />) as HTMLDivElement
const input = (<input />) as HTMLInputElement
const svg = (<svg:svg />) as SVGSVGElement

const inputState = atom('42')

const testDiv = (
<Bind
element={div}
// @ts-expect-error there should be an error here
value={inputState}
/>
)
const testInput = (
<Bind element={input} value={inputState} on:input={(ctx, e) => inputState(ctx, e.currentTarget.value)} />
)
const testSvg = (
<Bind element={svg}>
<svg:path d="M 10 10 H 100" />
</Bind>
)

mount(
parent,
<main>
{testDiv}
{testInput}
{testSvg}
</main>,
)

await sleep()

inputState(ctx, '43')

assert.is(input.value, '43')
assert.is(testSvg.innerHTML, '<path d="M 10 10 H 100"></path>')
}),
)
45 changes: 29 additions & 16 deletions packages/jsx/src/index.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -324,3 +333,7 @@ export const css = (strings: TemplateStringsArray, ...values: any[]) => {
}
return result
}

export const Bind = <T extends Element>(
props: { element: T } & AttributesAtomMaybe<Partial<Omit<T, 'children'>> & JSX.DOMAttributes<T>>,
): T => props.element

0 comments on commit ea4617e

Please sign in to comment.