@whatsaaaa/form-builder-react 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +31 -335
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -1,20 +1,26 @@
1
1
  # @whatsaaaa/form-builder-react
2
2
 
3
- [![npm](https://img.shields.io/npm/v/@whatsaaaa/form-builder-react)](https://www.npmjs.com/package/@whatsaaaa/form-builder-react)
4
-
5
3
  Render JSON form definitions with [Formisch](https://formisch.dev) and your own React components.
6
4
 
7
- It's headless: it reads the definition, connects every field to Formisch and turns input into the values the schema expects, but it renders nothing of its own. You give it a component per field type, so it works with plain HTML, shadcn/ui or any other UI library.
5
+ [![npm version](https://img.shields.io/npm/v/@whatsaaaa/form-builder-react)](https://www.npmjs.com/package/@whatsaaaa/form-builder-react)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@whatsaaaa/form-builder-react)](https://www.npmjs.com/package/@whatsaaaa/form-builder-react)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/whatsaaaa/form-builder/ci.yml?branch=main&label=CI)](https://github.com/whatsaaaa/form-builder/actions/workflows/ci.yml)
8
+ [![Coverage](https://codecov.io/gh/whatsaaaa/form-builder/graph/badge.svg?flag=react)](https://codecov.io/gh/whatsaaaa/form-builder)
9
+ [![React](https://img.shields.io/npm/dependency-version/@whatsaaaa/form-builder-react/peer/react?logo=react)](https://react.dev)
10
+ [![Types](https://img.shields.io/npm/types/@whatsaaaa/form-builder-react)](https://www.npmjs.com/package/@whatsaaaa/form-builder-react)
11
+ [![License](https://img.shields.io/npm/l/@whatsaaaa/form-builder-react)](https://github.com/whatsaaaa/form-builder/blob/main/LICENSE)
12
+
13
+ This is the React adapter of [form-builder](https://github.com/whatsaaaa/form-builder). It is headless: it reads the definition, connects every field to Formisch and turns input into the values the schema expects, but renders nothing of its own. You give it a component per field type, so it works with plain HTML, shadcn/ui or any other UI library.
8
14
 
9
15
  ## Install
10
16
 
11
17
  ```sh
12
- pnpm add @whatsaaaa/form-builder-react @formisch/react valibot
18
+ pnpm add @whatsaaaa/form-builder-react @whatsaaaa/form-builder-schema @formisch/react valibot
13
19
  ```
14
20
 
15
- React 18 or 19, `@formisch/react` 1.2 and Valibot 1.4.1 or later are required. `@whatsaaaa/form-builder-schema` comes with it.
21
+ React 18 or 19, `@formisch/react` 1.2 and Valibot 1.4.1 or later are required. Install `@whatsaaaa/form-builder-schema` too: the examples import its types, such as `JsonForm`.
16
22
 
17
- ## Usage
23
+ ## Example
18
24
 
19
25
  ```tsx
20
26
  import { Form } from '@formisch/react'
@@ -38,60 +44,7 @@ export function DefinitionForm({ definition }: { definition: JsonForm }) {
38
44
  }
39
45
  ```
40
46
 
41
- - `useJsonForm(definition, options?)` builds the schema and the initial input, and creates the Formisch form. `jsonForm.form` is a normal Formisch form, so every Formisch method works on it.
42
- - `<JsonFields>` renders the fields inside your `<Form>`. The form element, the submit button and anything around them are yours.
43
- - A definition with issues throws the builder's `DefinitionError` while rendering, so an error boundary can catch it. Run `validate()` first when the definition comes from outside your code.
44
- - Like Formisch's `useForm`, `useJsonForm` reads the definition on the first render. To show another one, give the component that calls `useJsonForm` a new `key`: `<DefinitionForm key={id} definition={definition} />`.
45
-
46
- ### Options
47
-
48
- ```ts
49
- useJsonForm(definition, {
50
- builder: createSchemaBuilder({ fieldTypes: { money } }), // your own types, rules and messages
51
- components, // your component per field type, see below
52
- layoutComponents, // your component per layout type, see Layout
53
- validate: 'blur', // when a field is validated the first time, as in Formisch
54
- revalidate: 'input', // when it is validated again
55
- })
56
- ```
57
-
58
- `builder`, `validate` and `revalidate` are read on the first render, with the definition. `components` and `layoutComponents` are read on every render.
59
-
60
- ## Components
61
-
62
- `components` has one component per field type, by type name. Pass it to `useJsonForm`, or to `<JsonFields>` and `<JsonField>` as a prop, which wins. Several types can share one:
63
-
64
- ```ts
65
- import type { FieldComponents } from '@whatsaaaa/form-builder-react'
66
-
67
- export const components: FieldComponents = {
68
- text: TextInput,
69
- email: TextInput,
70
- number: TextInput,
71
- select: SelectInput,
72
- group: Fieldset,
73
- array: Repeater,
74
- heading: Heading,
75
- // …one for every type your definitions use
76
- }
77
- ```
78
-
79
- A type that a definition uses but `components` doesn't have throws a `TypeError` that names every missing type, when `<JsonFields>` or `<JsonField>` renders. Only the fields it renders count.
80
-
81
- What a component gets depends on the field's role:
82
-
83
- | Role | Which fields | Props |
84
- | ------- | ------------------------------ | -------------------------------------------------------------------------------- |
85
- | value | every field with a value | `field`, `path`, `kind`, `required`, `input`, `errors`, `inputProps`, `setInput` |
86
- | group | a container of kind `object` | `field`, `path`, `children`: the group's fields |
87
- | array | a container of kind `array` | `field`, `path`, `items`, `errors`, `add`, `remove`, `move`, `children(index)` |
88
- | display | a display element, kind `none` | `field` |
89
-
90
- The props are typed as `ValueFieldProps`, `GroupFieldProps`, `ArrayFieldProps` and `DisplayFieldProps`.
91
-
92
- Your own container types render through the group or array role too, by their kind.
93
-
94
- ### A value field
47
+ `components` maps each field type to a component of yours. A text input looks like this:
95
48
 
96
49
  ```tsx
97
50
  import type { ValueFieldProps } from '@whatsaaaa/form-builder-react'
@@ -113,286 +66,29 @@ export function TextInput({ field, kind, input, errors, inputProps, ...props }:
113
66
  }
114
67
  ```
115
68
 
116
- - `field` is the field from the definition, with its `label`, `placeholder`, `options` and `ui`.
117
- - `path` is its Formisch path with real indexes, like `['lines', 0, 'quantity']`.
118
- - `inputProps` are Formisch's element props: the field's name, a ref, and focus, change and blur handlers. Spread them on the input element, so Formisch knows when the field is touched and can focus it after a failed submit. Your own `onChange` after the spread replaces Formisch's, and calls `setInput` instead.
119
- - `required` is true when the definition makes the field required.
120
-
121
- ### setInput
122
-
123
- `setInput` sets the field's input after turning what an input element gives into what the schema expects, by the field's kind:
124
-
125
- | Kind | Input | Value set |
126
- | ------------------- | ----------------------- | ------------------------------ |
127
- | `number` | `''` or only spaces | `undefined` |
128
- | `number` | a numeric string, `'5'` | `5` |
129
- | `file` | a `FileList` | its first file, or `undefined` |
130
- | `array` of `file` | a `FileList` | an array of its files |
131
- | `array` of `number` | numeric strings | numbers |
132
-
133
- Anything else is set as it is, and the schema reports it. For a choice, like a radio or a select, set the option's value from `field.options` rather than the element's value: that keeps `2` a number and `true` a boolean.
134
-
135
- ```tsx
136
- const options = field.options as { value: string | number | boolean; label: string }[]
137
-
138
- <select
139
- {...inputProps}
140
- value={options.findIndex((option) => option.value === input)}
141
- onChange={(e) => setInput(options[Number(e.target.value)]?.value)}
142
- >
143
- <option value={-1}>Choose…</option>
144
- {options.map((option, i) => (
145
- <option key={i} value={i}>
146
- {option.label}
147
- </option>
148
- ))}
149
- </select>
150
- ```
151
-
152
- For a file input, pass the element's `files` with `onChange={(e) => setInput(e.target.files)}`, and never set its `value`.
153
-
154
- `setInput` stays the same function while the field is the same, so it is safe in an effect's or a memo's dependencies.
155
-
156
- ### A group
157
-
158
- ```tsx
159
- import type { GroupFieldProps } from '@whatsaaaa/form-builder-react'
160
-
161
- export function Fieldset({ field, children }: GroupFieldProps) {
162
- return (
163
- <fieldset>
164
- <legend>{field.label}</legend>
165
- {children}
166
- </fieldset>
167
- )
168
- }
169
- ```
170
-
171
- ### An array
172
-
173
- ```tsx
174
- import type { ArrayFieldProps } from '@whatsaaaa/form-builder-react'
175
-
176
- export function Repeater({ field, items, errors, add, remove, children }: ArrayFieldProps) {
177
- return (
178
- <fieldset>
179
- <legend>{field.label}</legend>
180
- {items.map((item, index) => (
181
- <div key={item}>
182
- {children(index)}
183
- <button type="button" onClick={() => remove(index)}>
184
- Remove
185
- </button>
186
- </div>
187
- ))}
188
- <button type="button" onClick={add}>
189
- Add
190
- </button>
191
- {errors && <small>{errors[0]}</small>}
192
- </fieldset>
193
- )
194
- }
195
- ```
196
-
197
- - `children(index)` renders the fields of the item at `index`.
198
- - `items` has one id per item. Use it as the `key`, so each item keeps its inputs when one is removed or moved.
199
- - `add()` adds an item at the end, with the defaults its fields have in the definition.
200
- - `errors` are the array's own messages, like a required array's or `maxLength`'s. Its items' fields show their own.
201
-
202
- ### A display element
203
-
204
- ```tsx
205
- import type { DisplayFieldProps } from '@whatsaaaa/form-builder-react'
206
-
207
- export function Heading({ field }: DisplayFieldProps) {
208
- return <h2>{String(field.text ?? '')}</h2>
209
- }
210
- ```
211
-
212
- ## Layout
213
-
214
- A definition's [`layout`](https://github.com/whatsaaaa/form-builder/tree/main/packages/schema#layout) puts its fields in sections, rows of a 12-column grid and steps, and leaves slots for content of yours. `<JsonFields>` renders the current step of it. Give `useJsonForm` a component for each layout type the definition uses, and `<JsonFields>` the content of its slots:
215
-
216
- ```tsx
217
- import type { LayoutComponents, RowProps, SectionProps } from '@whatsaaaa/form-builder-react'
218
-
219
- function Section({ section, children }: SectionProps) {
220
- return (
221
- <fieldset>
222
- <legend>{section.label}</legend>
223
- {children}
224
- </fieldset>
225
- )
226
- }
227
-
228
- // One line of a 12-column grid. On a narrow screen, give each cell the whole row.
229
- function Row({ spans, children }: RowProps) {
230
- return (
231
- <div className="row">
232
- {spans.map((span, index) => (
233
- <div key={index} style={{ gridColumn: `span ${span}` }}>
234
- {children(index)}
235
- </div>
236
- ))}
237
- </div>
238
- )
239
- }
240
-
241
- const layoutComponents: LayoutComponents = { section: Section, row: Row }
242
-
243
- function Checkout({ definition }: { definition: JsonForm }) {
244
- const jsonForm = useJsonForm(definition, { components, layoutComponents })
245
- return (
246
- <Form of={jsonForm.form} onSubmit={save}>
247
- <JsonFields of={jsonForm} slots={{ submit: <button type="submit">Place order</button> }} />
248
- </Form>
249
- )
250
- }
251
- ```
252
-
253
- ```css
254
- .row {
255
- display: grid;
256
- grid-template-columns: repeat(12, 1fr);
257
- gap: 1rem;
258
- }
259
- ```
69
+ `jsonForm.form` is a normal Formisch form, so every Formisch method works on it.
260
70
 
261
- | Layout type | Props |
262
- | ----------- | ------------------------------------------------------------------------------------------------------------------------ |
263
- | `section` | `section`, as written, with its `id`, `label`, `description` and `ui`; `children` |
264
- | `row` | `row`, as written, with its `id` and `ui`; `spans`, the span of each cell; `children(index)` renders the cell at `index` |
71
+ ## Highlights
265
72
 
266
- - `layoutComponents` goes to `useJsonForm`, or to `<JsonFields>` as a prop, which wins. A definition without sections or rows needs none, and one without a `layout` renders as it always did.
267
- - A layout type the layout uses without a component, or a slot without content in `slots`, throws a `TypeError` naming each one, on every step.
268
- - Display elements written in the layout render with your field components, like the ones in `fields`.
269
- - `except` leaves fields out of the layout too, and a section or a row left without fields drops out. The other cells of a row keep their spans. `only` renders the fields it names without the layout.
270
- - The spans are for a wide screen: the layout says nothing about narrow ones, so your row component decides, e.g. with a media query.
73
+ - Headless: one component of yours per field type, and per layout type
74
+ - `setInput` turns what an element gives into the value the schema expects
75
+ - Groups, arrays and display elements, with stable keys for array items
76
+ - Sections, 12-column rows, slots and multi-step forms from the definition's layout
77
+ - Steps that validate on Next and only show errors the user has seen
78
+ - Conditions from the affects package, re-rendering only when what is hidden changes
79
+ - Fields placed in your own markup with `<JsonField>`, `only` and `except`
271
80
 
272
- ### Steps
81
+ ## Documentation
273
82
 
274
- `jsonForm.steps` is where the form is, and how to move:
83
+ - [Guide](https://whatsaaaa.github.io/form-builder/frameworks/react): options, components for every role, layout, steps, conditions, placing fields yourself and typing the form
84
+ - [API reference](https://whatsaaaa.github.io/form-builder/api/react/)
275
85
 
276
- ```tsx
277
- function Signup({ definition }: { definition: JsonForm }) {
278
- const jsonForm = useJsonForm(definition, { components, layoutComponents })
279
- const { steps } = jsonForm
280
- return (
281
- <Form of={jsonForm.form} onSubmit={save}>
282
- <ol>
283
- {steps.list.map((step, i) => (
284
- <li key={step.step.id ?? i} aria-current={i === steps.index ? 'step' : undefined}>
285
- {step.step.label}
286
- </li>
287
- ))}
288
- </ol>
289
- <JsonFields of={jsonForm} />
290
- {!steps.isFirst && (
291
- <button type="button" onClick={steps.back}>
292
- Back
293
- </button>
294
- )}
295
- {steps.isLast ? (
296
- <button type="submit">Sign up</button>
297
- ) : (
298
- <button type="button" onClick={() => void steps.next()}>
299
- Next
300
- </button>
301
- )}
302
- </Form>
303
- )
304
- }
305
- ```
306
-
307
- | Member | |
308
- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
309
- | `list` | the steps on show, each with its `step` as written (`id`, `label`, `description`, `ui`), `children` and `fields`: a step that [conditions](#conditions) hide is left out, unless the user is on it |
310
- | `index`, `current` | the step `<JsonFields>` renders |
311
- | `isFirst`, `isLast` | |
312
- | `next()` | validates, and moves on when the step's fields have no issues. Resolves to whether it moved |
313
- | `back()` | moves back, without validating |
314
- | `goTo(index)` | back freely; forward through each step on the way, stopping at the first with issues |
315
-
316
- The form is one Formisch form, so Formisch validates all of it at once. The steps make that work:
317
-
318
- - **Next** validates the whole form, and only the issues on the current step's fields keep the user there. It focuses the first one, in layout order.
319
- - **A step shows what the user left**: the errors it had when they left it, and none the first time they get to it, so the issues of steps nobody saw yet stay out of sight.
320
- - **A step that failed Next revalidates** as the user fixes it, on the `revalidate` event, like a form after a failed submit.
321
- - **A submit before the last step is a Next**, so Enter in a step with a single text input doesn't submit the form early. On the last step, Formisch submits as usual.
322
- - **A submit that fails because of an earlier step**, like a form rule that reads fields of two steps, goes back to that step.
323
- - `<JsonFields step={2} />` renders a given step instead of the current one, e.g. on a review page. A form without steps has one, and nothing changes for it.
324
-
325
- ## Conditions
326
-
327
- With the [affects package](https://github.com/whatsaaaa/form-builder/tree/main/packages/affects), fields, sections and steps show and hide by the values of other fields. Pass `useJsonForm` a builder with the extension:
328
-
329
- ```tsx
330
- import { affects } from '@whatsaaaa/form-builder-affects'
331
-
332
- const builder = createSchemaBuilder({ extensions: [affects()] })
333
-
334
- function DefinitionForm({ definition }: { definition: JsonForm }) {
335
- const jsonForm = useJsonForm(definition, { builder, components, layoutComponents })
336
- // ...
337
- }
338
- ```
339
-
340
- - **`<JsonFields>` and `<JsonField>` leave out what the conditions hide**: a hidden field, a hidden section, and a group, section or row whose fields are all hidden. A field hidden in a row drops out of it, and the others keep their spans.
341
- - **Steps skip the hidden ones.** `steps.list` holds the steps on show, so a step whose fields are all hidden is left out, and Next and Back go past it. The step the user is on stays, until they leave it.
342
- - **`useJsonForm` clears what `hideAndClear` clears**, with Formisch's `setInput`, as soon as it is cleared, wherever the field renders: in `<JsonFields>`, with `<JsonField>`, or by your own code. A field that starts hidden this way starts blank.
343
- - **The schema skips hidden fields**, so they never block the form, and submits what the affects package says.
344
- - **The form renders again only when what is hidden changes**, not on every keystroke: `useJsonForm` watches the input outside React, through `@formisch/react/internals`.
345
- - **`jsonForm.visibility`** is the state for the form's input now, for your own renderers: `isHidden(node, indexes)`, `isStepHidden(step)`, `shownNodes(nodes, indexes)`. It is a new object only when what is hidden changes.
346
-
347
- Without conditions, nothing is hidden, nothing is watched, and nothing changes.
348
-
349
- ## Placing fields yourself
350
-
351
- Without a `layout`, `<JsonFields>` renders every field in the order of the definition. To put some of them somewhere else, in code rather than in the definition, place them with `<JsonField>` and leave them out of `<JsonFields>` with `except`:
352
-
353
- ```tsx
354
- <Form of={jsonForm.form} onSubmit={save}>
355
- <main>
356
- <JsonFields of={jsonForm} except={['coupon', 'acceptTerms']} />
357
- </main>
358
- <aside>
359
- <JsonField of={jsonForm} path="coupon" />
360
- <JsonField of={jsonForm} path="acceptTerms" />
361
- <button type="submit">Place order</button>
362
- </aside>
363
- </Form>
364
- ```
365
-
366
- - A path is the field's names joined with dots: `'email'`, or `'address.city'` for a field of a group. A placed field validates, focuses and submits like any other, because it is still part of the one Formisch form.
367
- - A group or an array comes with all its fields. A field of an array's items can't be placed on its own, because it has no index outside its item, so place the array. To leave such a field out of every item, use `'*'` for the index: `except={['lines.*.note']}`.
368
- - `only` renders just the fields it names, in the order of the definition: `<JsonFields of={jsonForm} only={['name', 'email']} />`. Display elements have no dot path, even with a name, so `only` leaves them out.
369
- - A path that names no field throws a `TypeError`, so a typo shows up at once. `only` and `except` are read on every render, so they can change, and a field keeps what was typed while it is hidden.
370
-
371
- ## Typing the form
372
-
373
- The definition is data, so TypeScript doesn't know its fields, and `jsonForm.form` is typed loosely: Formisch's own functions accept only top-level paths on it. Give `useJsonForm` your input type, and your output type when it differs, and they check paths and values as in any Formisch form:
374
-
375
- ```ts
376
- import { getInput, useFieldArray } from '@formisch/react'
377
-
378
- interface CheckoutInput {
379
- name: string
380
- address: { city: string }
381
- lines: { product: string; quantity: number }[]
382
- }
383
-
384
- const jsonForm = useJsonForm<CheckoutInput>(definition, { components })
385
-
386
- getInput(jsonForm.form, { path: ['address', 'city'] }) // string | undefined
387
- useFieldArray(jsonForm.form, { path: ['lines'] })
388
- ```
86
+ ## Related packages
389
87
 
390
- - Formisch's `<Field>`, `<FieldArray>`, `useField`, `getInput` and `setInput` work on `jsonForm.form` either way. With a type, nested paths type-check, their values are typed, and so is the output your submit handler gets.
391
- - The type is a promise you make: nothing checks it against the definition. Keep the two in step.
392
- - A field you render with Formisch's `<Field>` gets none of the adapter's help, like `setInput` turning `'5'` into `5`, or the defaults of a new array item. Leave it out of `<JsonFields>` with `except`.
88
+ - [`@whatsaaaa/form-builder-schema`](https://github.com/whatsaaaa/form-builder/tree/main/packages/schema): build Formisch-ready Valibot schemas from JSON definitions
89
+ - [`@whatsaaaa/form-builder-affects`](https://github.com/whatsaaaa/form-builder/tree/main/packages/affects): show, hide and clear fields by conditions
90
+ - [`@whatsaaaa/form-builder-vue`](https://github.com/whatsaaaa/form-builder/tree/main/frameworks/vue): the same adapter for Vue
393
91
 
394
- ## Not included
92
+ ## License
395
93
 
396
- - Labels and display text reach your components as written in the definition. The builder's `resolveMessage` only changes what goes into the schema, so translate `field.label` in your components.
397
- - `ui.widget` doesn't pick a component. A component can read `field.ui` itself, e.g. to show a multiselect as checkboxes.
398
- - Showing and hiding fields, or making one required because of another, belongs to a future affects package. The layout already hides a section, a row or a step whose fields are all hidden, through `shownNodes` in the schema package.
94
+ [MIT](https://github.com/whatsaaaa/form-builder/blob/main/LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whatsaaaa/form-builder-react",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Render JSON form definitions with Formisch and your own React components.",
5
5
  "keywords": [
6
6
  "form",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@whatsaaaa/form-builder-schema": "^0.15.0"
36
+ "@whatsaaaa/form-builder-schema": "^0.15.2"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@arethetypeswrong/cli": "^0.18.5",
@@ -42,7 +42,7 @@
42
42
  "@types/react": "^19.3.0",
43
43
  "@types/react-dom": "^19.3.0",
44
44
  "@vitest/coverage-v8": "^5.0.5",
45
- "@whatsaaaa/form-builder-affects": "^0.1.0",
45
+ "@whatsaaaa/form-builder-affects": "^0.1.2",
46
46
  "happy-dom": "^20.0.0",
47
47
  "publint": "^0.3.25",
48
48
  "react": "^19.3.1",