Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(jsx): correct behaviour #690

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 165 additions & 6 deletions package-lock.json

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

101 changes: 88 additions & 13 deletions packages/jsx/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
This is an **EXPERIMENTAL** package that allows you to describe native DOM elements using JSX. The cool thing is that you can assign atoms and actions to the native properties, and they will be bound correctly.
An **EXPERIMENTAL** JSX runtime for describing dynamic DOM UIs with Reatom.

## Installation

```sh
npm i @reatom/jsx
npm install @reatom/jsx
```

`tsconfig.json`
`tsconfig.json`:

```json
{
Expand All @@ -17,7 +17,7 @@ npm i @reatom/jsx
}
```

`vite.config.js`
`vite.config.js`:

```js
import { defineConfig } from 'vite'
Expand All @@ -31,7 +31,9 @@ export default defineConfig({
})
```

## Usage
## Example

Define a component:

```ts
import { atom, action } from '@reatom/core'
Expand All @@ -43,26 +45,99 @@ const onInput = action((ctx, event) =>
export const Input = () => <input value={inputAtom} oninput={onInput} />
```

In the app root.
Render it:

```tsx
import { connectLogger } from '@reatom/framework'
import { ctx, mount } from '@reatom/jsx'
import { App } from './App'

if (import.meta.env.DEV) {
connectLogger(ctx)
const disconnect = connectLogger(ctx)
}

mount(document.getElementById('app')!, <App />)
```

## Limitations
## Reference

- No SSR support
- No SVG support
- No keyed lists support
This library implements a common TypeScript JSX factory that creates and configures **native** DOM elements.

By default, props passed to the JSX factory are set as attributes. Add `field:` prefix to a prop name to set element fields instead of attributes. It can be used to set stuff like `HTMLInputElement..indeterminate` and `SVGPathElement..d`. There are props that treated as fields by default:

- `className` (for compatibility with React libraries like [`stylerun`](https://github.com/artalar/stylerun))
- `innerHTML`
- `innerText`

Comment on lines +68 to +70
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid innerHTML and innerText defaults as they are very rarely used in regular application usage. className bindings may be an extra thing here too; I will think about it.

Atom-valued props create a reactive binding to an element's attribute/field. The binding is automatically disposed when the element is disconnected from the DOM.

`children` prop specifies the inner content of an element, which can be one of the following:

- `false`/`null`/`undefined` to render nothing
- a string, a number, or `true` to create a text node
- a native DOM node to insert it as-is

### Handling events

Use `on*` props to add event handlers. Passed functions are automatically bound to a relevant `Ctx` value: `oninput={(ctx, event) => setValue(ctx, event.currentTarget.value)}`.

### Styles

Object-valued `style` prop applies styles granularly: `style={{top: 0, display: equalsFalseForNow && 'none'}}` sets `top: 0;`.

`false`, `null` and `undefined` style values remove the property. Non-string style values are stringified (we don't add `px` to numeric values automatically).

### Dynamic props

There's a special `$props` prop which can be used to spread props reactively: `<div someStaticProp $props={atom(ctx => getReactiveProps(ctx))}>`. Passing an array of atoms is also possible. Note that `$props` can't be used to set event handlers.

### SVG

To create elements with names within the SVG namespace, you should prepend `svg:` to the tag name:

```tsx
const anSvgElement = (
<svg:svg>
<svg:path field:d="???" />
</svg:svg>
)
```

### Lifecycle

In Reatom, every atom has lifecycle events to which you can subscribe with `onConnect`/`onDisconnect` functions. `@reatom/jsx` lets you toBy default, components don't have an atom associated with them, but you may wrap the component code in an atom manually to achieve the same result:

```tsx
import { onConnect, onDisconnect } from '@reatom/hooks'

const MyWidget = () => {
const lifecycle = atom((ctx) => <div>Something inside</div>)

onConnect(lifecycle, (ctx) => console.log('component connected'))
onDisconnect(lifecycle, (ctx) => console.log('component disconnected'))

About the last one: When you create an element (`<Element />`), it renders once, binds all passed atoms and actions, and will not render anymore. All changes will be propagated exactly to the element's properties. However, if you need to describe conditional logic, you can put an element in the atom and achieve rerenders through this. There is no "virtual DOM," so the elements will be recreated with each render, but this could be acceptable for some cases.
return lifecycle
}
```

Here is an example: [https://github.com/artalar/reatom-jsx/blob/main/src/App.tsx](https://github.com/artalar/reatom-jsx/blob/main/src/App.tsx)
Because the pattern used above is somewhat verbose and excessive atoms may cause performance overhead, `@reatom/jsx` lets you to subscribe to lifecycle hooks of an atom, created by the library internally:

```tsx
const MyWidget = () => {
return (
<div
onConnect={(ctx) => console.log('component connected')}
onDisconnect={(ctx) => console.log('component disconnected')}
>
Comment on lines +129 to +130
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onDisconnect is an extra thing. Let's stick with onConnect(() => () => cleanup()). Also, since this property is specific to the reatom/jsx runtime, let's mark it with a special prefix like $connect.

However, the hook bindings to the returned element are very weird (poor semantics), and we should find another way.

A possible compromise solution could be the $ref property, similar to React, which accepts a callback with ctx and the connected element as arguments. The callback could return the cleanup function.

Something inside
</div>
)
}
```

Special `onConnect` and `onDisconnect` props pass their value to the relevant lifecycle hook, if provided.

## Limitations

- No DOM-less SSR (requires a DOM API implementation like `linkedom` to be provided)
- No keyed lists support
3 changes: 2 additions & 1 deletion packages/jsx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"@reatom/core": ">=3.5.0",
"@reatom/lens": "^3.6.2",
"@reatom/utils": "^3.5.0",
"csstype": "^3.1.2"
"csstype": "^3.1.2",
"linkedom": "^0.16.4"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move it to devDependencies

},
"author": "artalar",
"maintainers": [
Expand Down
Loading
Loading