@whatsaaaa/form-builder-react 0.0.0 → 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Whatsaaaa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,260 @@
1
+ # @whatsaaaa/form-builder-react
2
+
3
+ [![npm](https://img.shields.io/npm/v/@whatsaaaa/form-builder-react)](https://www.npmjs.com/package/@whatsaaaa/form-builder-react)
4
+
5
+ Render JSON form definitions with [Formisch](https://formisch.dev) and your own React components.
6
+
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.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ pnpm add @whatsaaaa/form-builder-react @formisch/react valibot
13
+ ```
14
+
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.
16
+
17
+ ## Usage
18
+
19
+ ```tsx
20
+ import { Form } from '@formisch/react'
21
+ import type { JsonForm } from '@whatsaaaa/form-builder-schema'
22
+ import { JsonFields, useJsonForm } from '@whatsaaaa/form-builder-react'
23
+ import { components } from './fields'
24
+
25
+ export function DefinitionForm({ definition }: { definition: JsonForm }) {
26
+ const jsonForm = useJsonForm(definition, { components })
27
+
28
+ function save(output: unknown) {
29
+ // The output of the schema: trimmed, numbers as numbers, empty optional fields left out.
30
+ }
31
+
32
+ return (
33
+ <Form of={jsonForm.form} onSubmit={save}>
34
+ <JsonFields of={jsonForm} />
35
+ <button type="submit">Save</button>
36
+ </Form>
37
+ )
38
+ }
39
+ ```
40
+
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
+ validate: 'blur', // when a field is validated the first time, as in Formisch
53
+ revalidate: 'input', // when it is validated again
54
+ })
55
+ ```
56
+
57
+ `builder`, `validate` and `revalidate` are read on the first render, with the definition. `components` is read on every render.
58
+
59
+ ## Components
60
+
61
+ `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:
62
+
63
+ ```ts
64
+ import type { FieldComponents } from '@whatsaaaa/form-builder-react'
65
+
66
+ export const components: FieldComponents = {
67
+ text: TextInput,
68
+ email: TextInput,
69
+ number: TextInput,
70
+ select: SelectInput,
71
+ group: Fieldset,
72
+ array: Repeater,
73
+ heading: Heading,
74
+ // …one for every type your definitions use
75
+ }
76
+ ```
77
+
78
+ 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.
79
+
80
+ What a component gets depends on the field's role:
81
+
82
+ | Role | Which fields | Props |
83
+ | ------- | ------------------------------ | -------------------------------------------------------------------------------- |
84
+ | value | every field with a value | `field`, `path`, `kind`, `required`, `input`, `errors`, `inputProps`, `setInput` |
85
+ | group | a container of kind `object` | `field`, `path`, `children`: the group's fields |
86
+ | array | a container of kind `array` | `field`, `path`, `items`, `errors`, `add`, `remove`, `move`, `children(index)` |
87
+ | display | a display element, kind `none` | `field` |
88
+
89
+ The props are typed as `ValueFieldProps`, `GroupFieldProps`, `ArrayFieldProps` and `DisplayFieldProps`.
90
+
91
+ Your own container types render through the group or array role too, by their kind.
92
+
93
+ ### A value field
94
+
95
+ ```tsx
96
+ import type { ValueFieldProps } from '@whatsaaaa/form-builder-react'
97
+
98
+ export function TextInput({ field, kind, input, errors, inputProps, ...props }: ValueFieldProps) {
99
+ return (
100
+ <label>
101
+ {field.label}
102
+ <input
103
+ {...inputProps}
104
+ type={kind === 'number' ? 'number' : 'text'}
105
+ value={String(input ?? '')}
106
+ required={props.required}
107
+ onChange={(e) => props.setInput(e.target.value)}
108
+ />
109
+ {errors && <small>{errors[0]}</small>}
110
+ </label>
111
+ )
112
+ }
113
+ ```
114
+
115
+ - `field` is the field from the definition, with its `label`, `placeholder`, `options` and `ui`.
116
+ - `path` is its Formisch path with real indexes, like `['lines', 0, 'quantity']`.
117
+ - `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.
118
+ - `required` is true when the definition makes the field required.
119
+
120
+ ### setInput
121
+
122
+ `setInput` sets the field's input after turning what an input element gives into what the schema expects, by the field's kind:
123
+
124
+ | Kind | Input | Value set |
125
+ | ------------------- | ----------------------- | ------------------------------ |
126
+ | `number` | `''` or only spaces | `undefined` |
127
+ | `number` | a numeric string, `'5'` | `5` |
128
+ | `file` | a `FileList` | its first file, or `undefined` |
129
+ | `array` of `file` | a `FileList` | an array of its files |
130
+ | `array` of `number` | numeric strings | numbers |
131
+
132
+ 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.
133
+
134
+ ```tsx
135
+ const options = field.options as { value: string | number | boolean; label: string }[]
136
+
137
+ <select
138
+ {...inputProps}
139
+ value={options.findIndex((option) => option.value === input)}
140
+ onChange={(e) => setInput(options[Number(e.target.value)]?.value)}
141
+ >
142
+ <option value={-1}>Choose…</option>
143
+ {options.map((option, i) => (
144
+ <option key={i} value={i}>
145
+ {option.label}
146
+ </option>
147
+ ))}
148
+ </select>
149
+ ```
150
+
151
+ For a file input, pass the element's `files` with `onChange={(e) => setInput(e.target.files)}`, and never set its `value`.
152
+
153
+ `setInput` stays the same function while the field is the same, so it is safe in an effect's or a memo's dependencies.
154
+
155
+ ### A group
156
+
157
+ ```tsx
158
+ import type { GroupFieldProps } from '@whatsaaaa/form-builder-react'
159
+
160
+ export function Fieldset({ field, children }: GroupFieldProps) {
161
+ return (
162
+ <fieldset>
163
+ <legend>{field.label}</legend>
164
+ {children}
165
+ </fieldset>
166
+ )
167
+ }
168
+ ```
169
+
170
+ ### An array
171
+
172
+ ```tsx
173
+ import type { ArrayFieldProps } from '@whatsaaaa/form-builder-react'
174
+
175
+ export function Repeater({ field, items, errors, add, remove, children }: ArrayFieldProps) {
176
+ return (
177
+ <fieldset>
178
+ <legend>{field.label}</legend>
179
+ {items.map((item, index) => (
180
+ <div key={item}>
181
+ {children(index)}
182
+ <button type="button" onClick={() => remove(index)}>
183
+ Remove
184
+ </button>
185
+ </div>
186
+ ))}
187
+ <button type="button" onClick={add}>
188
+ Add
189
+ </button>
190
+ {errors && <small>{errors[0]}</small>}
191
+ </fieldset>
192
+ )
193
+ }
194
+ ```
195
+
196
+ - `children(index)` renders the fields of the item at `index`.
197
+ - `items` has one id per item. Use it as the `key`, so each item keeps its inputs when one is removed or moved.
198
+ - `add()` adds an item at the end, with the defaults its fields have in the definition.
199
+ - `errors` are the array's own messages, like a required array's or `maxLength`'s. Its items' fields show their own.
200
+
201
+ ### A display element
202
+
203
+ ```tsx
204
+ import type { DisplayFieldProps } from '@whatsaaaa/form-builder-react'
205
+
206
+ export function Heading({ field }: DisplayFieldProps) {
207
+ return <h2>{String(field.text ?? '')}</h2>
208
+ }
209
+ ```
210
+
211
+ ## Placing fields yourself
212
+
213
+ `<JsonFields>` renders every field in the order of the definition. To put some of them somewhere else, place them with `<JsonField>` and leave them out of `<JsonFields>` with `except`:
214
+
215
+ ```tsx
216
+ <Form of={jsonForm.form} onSubmit={save}>
217
+ <main>
218
+ <JsonFields of={jsonForm} except={['coupon', 'acceptTerms']} />
219
+ </main>
220
+ <aside>
221
+ <JsonField of={jsonForm} path="coupon" />
222
+ <JsonField of={jsonForm} path="acceptTerms" />
223
+ <button type="submit">Place order</button>
224
+ </aside>
225
+ </Form>
226
+ ```
227
+
228
+ - 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.
229
+ - 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']}`.
230
+ - `only` renders just the fields it names, in the order of the definition: `<JsonFields of={jsonForm} only={['name', 'email']} />`. Display elements have no name, so `only` leaves them out.
231
+ - 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.
232
+
233
+ ## Typing the form
234
+
235
+ 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:
236
+
237
+ ```ts
238
+ import { getInput, useFieldArray } from '@formisch/react'
239
+
240
+ interface CheckoutInput {
241
+ name: string
242
+ address: { city: string }
243
+ lines: { product: string; quantity: number }[]
244
+ }
245
+
246
+ const jsonForm = useJsonForm<CheckoutInput>(definition, { components })
247
+
248
+ getInput(jsonForm.form, { path: ['address', 'city'] }) // string | undefined
249
+ useFieldArray(jsonForm.form, { path: ['lines'] })
250
+ ```
251
+
252
+ - 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.
253
+ - The type is a promise you make: nothing checks it against the definition. Keep the two in step.
254
+ - 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`.
255
+
256
+ ## Not included
257
+
258
+ - 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.
259
+ - `ui.widget` doesn't pick a component. A component can read `field.ui` itself, e.g. to show a multiselect as checkboxes.
260
+ - Showing and hiding fields, or making one required because of another, belongs to a future affects package.
@@ -0,0 +1,117 @@
1
+ import { FieldNode, FieldNode as FieldNode$1, FieldPath, FieldPath as FieldPath$1, FieldRole, JsonField as JsonField$1, JsonForm, JsonFormSchema, JsonFormSchema as JsonFormSchema$1, SchemaBuilder, TypedJsonFormSchema, TypedJsonFormSchema as TypedJsonFormSchema$1, ValueKind } from "@whatsaaaa/form-builder-schema";
2
+ import { FieldElementProps, FormSchema, FormStore, ValidationMode } from "@formisch/react";
3
+ import { ComponentType, ReactElement, ReactNode } from "react";
4
+ /**
5
+ * Your component for each field type, by type name: `{ text: TextInput, group: Fieldset }`. Each
6
+ * one takes the props of the role its type has.
7
+ */
8
+ type FieldComponents = Record<string, ComponentType<ValueFieldProps> | ComponentType<GroupFieldProps> | ComponentType<ArrayFieldProps> | ComponentType<DisplayFieldProps>>;
9
+ /** The props of a component for a display element, like a heading. */
10
+ interface DisplayFieldProps {
11
+ field: JsonField$1;
12
+ }
13
+ /** The props of a component for a field with a value. */
14
+ interface ValueFieldProps {
15
+ field: JsonField$1;
16
+ path: FieldPath$1;
17
+ /** The kind of the value, e.g. `'number'`. */
18
+ kind: ValueKind;
19
+ /** True when the definition makes the field required. */
20
+ required: boolean;
21
+ /** The field's current input. */
22
+ input: unknown;
23
+ /** The field's error messages, or null when it has none. */
24
+ errors: [string, ...string[]] | null;
25
+ /** Spread these on the input element: its name, ref, and focus, change and blur handlers. */
26
+ inputProps: FieldElementProps;
27
+ /** Sets the input, after turning `'5'` into `5` for a number, or a `FileList` into files. */
28
+ setInput: (input: unknown) => void;
29
+ }
30
+ /** The props of a component for a container of kind `object`, like the built-in group. */
31
+ interface GroupFieldProps {
32
+ field: JsonField$1;
33
+ path: FieldPath$1;
34
+ /** The group's fields. */
35
+ children: ReactNode;
36
+ }
37
+ /** The props of a component for a container of kind `array`, like the built-in array. */
38
+ interface ArrayFieldProps {
39
+ field: JsonField$1;
40
+ path: FieldPath$1;
41
+ /** One id per item, to use as its `key`. */
42
+ items: string[];
43
+ /** The array's own error messages, e.g. from `maxLength`, or null. */
44
+ errors: [string, ...string[]] | null;
45
+ /** Adds an item at the end. */
46
+ add: () => void;
47
+ remove: (index: number) => void;
48
+ move: (from: number, to: number) => void;
49
+ /** Renders the fields of the item at `index`. */
50
+ children: (index: number) => ReactNode;
51
+ }
52
+ interface UseJsonFormOptions {
53
+ /** Builds the schema and knows your own types and rules. Defaults to `createSchemaBuilder()`. */
54
+ builder?: SchemaBuilder;
55
+ /** Your component for each field type, so `<JsonFields>` and `<JsonField>` don't need them. */
56
+ components?: FieldComponents;
57
+ /** When a field is validated the first time, as in Formisch. */
58
+ validate?: ValidationMode;
59
+ /** When a field is validated again after that, as in Formisch. */
60
+ revalidate?: Exclude<ValidationMode, 'initial'>;
61
+ }
62
+ /** What `useJsonForm` returns. Pass it to `<JsonFields>`. */
63
+ interface JsonFormInstance<TSchema extends FormSchema = JsonFormSchema$1> {
64
+ /** The Formisch form: pass it to Formisch's `<Form of>`, or to any of its methods. */
65
+ readonly form: FormStore<TSchema>;
66
+ readonly definition: JsonForm;
67
+ readonly builder: SchemaBuilder;
68
+ /** The components from the options, if any. */
69
+ readonly components: FieldComponents | undefined;
70
+ /** The definition's fields as a tree, the way `<JsonFields>` renders them. */
71
+ readonly fields: readonly FieldNode$1[];
72
+ }
73
+ /** Any form `useJsonForm` returns, typed or not: what the components take. */
74
+ type AnyJsonFormInstance = JsonFormInstance<FormSchema>;
75
+ /**
76
+ * A Formisch form for a definition, with its schema and initial input from the builder. A
77
+ * definition with issues throws the builder's `DefinitionError`, so run `validate()` first when it
78
+ * comes from outside your code. Like Formisch's `useForm`, it reads the definition, the builder and
79
+ * the validation modes on the first render: give the component a new `key` to use another
80
+ * definition. The components are read on every render.
81
+ */
82
+ export declare function useJsonForm(definition: JsonForm, options?: UseJsonFormOptions): JsonFormInstance;
83
+ /**
84
+ * The same, with the form typed by your input type, and output type when it differs, so Formisch's
85
+ * paths and values are checked. The types are yours to keep in step with the definition.
86
+ */
87
+ export declare function useJsonForm<TInput extends object, TOutput extends object = TInput>(definition: JsonForm, options?: UseJsonFormOptions): JsonFormInstance<TypedJsonFormSchema$1<TInput, TOutput>>;
88
+ interface JsonFieldProps {
89
+ /** What `useJsonForm` returned. */
90
+ of: AnyJsonFormInstance;
91
+ /** The field's path: `'email'`, or `'address.city'` for a field of a group. */
92
+ path: string;
93
+ /** Defaults to the components `useJsonForm` got. */
94
+ components?: FieldComponents | undefined;
95
+ }
96
+ /**
97
+ * Renders one field of a `useJsonForm` form, wherever you put it inside Formisch's `<Form>`. A
98
+ * group or an array comes with its fields. Leave the field out of `<JsonFields>` with `except`.
99
+ */
100
+ export declare function JsonField({ of, path, components }: JsonFieldProps): ReactElement;
101
+ interface JsonFieldsProps {
102
+ /** What `useJsonForm` returned. */
103
+ of: AnyJsonFormInstance;
104
+ /** Defaults to the components `useJsonForm` got. */
105
+ components?: FieldComponents | undefined;
106
+ /** Renders only these fields, by path, in the order of the definition. */
107
+ only?: readonly string[] | undefined;
108
+ /** Leaves out these fields, by path, e.g. the ones you place with `<JsonField>`. */
109
+ except?: readonly string[] | undefined;
110
+ }
111
+ /**
112
+ * Renders the fields of a `useJsonForm` form with your components, one per field type. It renders
113
+ * nothing of its own, so put it inside Formisch's `<Form>` with your own submit button.
114
+ */
115
+ export declare function JsonFields({ of, components, only, except }: JsonFieldsProps): ReactElement;
116
+ export type { AnyJsonFormInstance, ArrayFieldProps, DisplayFieldProps, FieldComponents, FieldNode, FieldPath, FieldRole, GroupFieldProps, JsonFieldProps, JsonFieldsProps, JsonFormInstance, JsonFormSchema, TypedJsonFormSchema, UseJsonFormOptions, ValueFieldProps };
117
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,158 @@
1
+ import { checkComponents, coerceInput, createSchemaBuilder, dotPathOf, findField, isRequired, selectFields, toFieldPath } from "@whatsaaaa/form-builder-schema";
2
+ import { insert, move, remove, useField, useFieldArray, useForm } from "@formisch/react";
3
+ import { useCallback, useMemo, useState } from "react";
4
+ import { Fragment, jsx } from "react/jsx-runtime";
5
+ // src/array-field.tsx
6
+ /** Connects a container of kind `array` to Formisch, and renders your component for it. */
7
+ function ArrayField({ of, node, path, component: Component, renderItem }) {
8
+ const array = useFieldArray(of.form, { path });
9
+ const { form, builder } = of;
10
+ const { field } = node;
11
+ const at = array.path;
12
+ const methods = useMemo(() => ({
13
+ add: () => insert(form, {
14
+ path: at,
15
+ initialInput: builder.getItemInput(field)
16
+ }),
17
+ remove: (index) => remove(form, {
18
+ path: at,
19
+ at: index
20
+ }),
21
+ move: (from, to) => move(form, {
22
+ path: at,
23
+ from,
24
+ to
25
+ })
26
+ }), [
27
+ form,
28
+ builder,
29
+ field,
30
+ at
31
+ ]);
32
+ return /* @__PURE__ */ jsx(Component, {
33
+ field,
34
+ path,
35
+ items: array.items,
36
+ errors: array.errors,
37
+ ...methods,
38
+ children: renderItem
39
+ });
40
+ }
41
+ // src/value-field.tsx
42
+ /** Connects a field with a value to Formisch, and renders your component for it. */
43
+ function ValueField({ of, node, path, component: Component }) {
44
+ const field = useField(of.form, { path });
45
+ const setInput = useCallback((input) => field.onChange(coerceInput(input, node)), [field, node]);
46
+ return /* @__PURE__ */ jsx(Component, {
47
+ field: node.field,
48
+ path,
49
+ kind: node.kind,
50
+ required: isRequired(node.field),
51
+ input: field.input,
52
+ errors: field.errors,
53
+ inputProps: field.props,
54
+ setInput
55
+ });
56
+ }
57
+ // src/render-fields.tsx
58
+ /** The components from the props, or else the ones `useJsonForm` got. */
59
+ function resolveComponents(components, of) {
60
+ const resolved = components ?? of.components;
61
+ if (!resolved) throw new TypeError("No field components. Pass them to useJsonForm, or to the component.");
62
+ return resolved;
63
+ }
64
+ /** `indexes` are the indexes of the array items the fields are in, outermost first. */
65
+ function renderFields(ctx, nodes, indexes) {
66
+ return nodes.map((node, position) => renderField(ctx, node, indexes, position));
67
+ }
68
+ function renderField(ctx, node, indexes, position) {
69
+ const component = ctx.components[node.type];
70
+ const path = toFieldPath(node.dataPath, indexes);
71
+ const key = dotPathOf(node) ?? `#${position}`;
72
+ switch (node.role) {
73
+ case "display": return /* @__PURE__ */ jsx(component, { field: node.field }, key);
74
+ case "group": return /* @__PURE__ */ jsx(component, {
75
+ field: node.field,
76
+ path,
77
+ children: renderFields(ctx, node.children, indexes)
78
+ }, key);
79
+ case "array": return /* @__PURE__ */ jsx(ArrayField, {
80
+ of: ctx.of,
81
+ node,
82
+ path,
83
+ component,
84
+ renderItem: (index) => renderFields(ctx, node.children, [...indexes, index])
85
+ }, key);
86
+ case "value": return /* @__PURE__ */ jsx(ValueField, {
87
+ of: ctx.of,
88
+ node,
89
+ path,
90
+ component
91
+ }, key);
92
+ }
93
+ }
94
+ // src/json-field.tsx
95
+ /**
96
+ * Renders one field of a `useJsonForm` form, wherever you put it inside Formisch's `<Form>`. A
97
+ * group or an array comes with its fields. Leave the field out of `<JsonFields>` with `except`.
98
+ */
99
+ function JsonField({ of, path, components }) {
100
+ const node = findField(of.fields, path, { placed: true });
101
+ const ctx = {
102
+ of,
103
+ components: resolveComponents(components, of)
104
+ };
105
+ checkComponents([node], ctx.components);
106
+ return renderField(ctx, node, [], 0);
107
+ }
108
+ // src/json-fields.tsx
109
+ /**
110
+ * Renders the fields of a `useJsonForm` form with your components, one per field type. It renders
111
+ * nothing of its own, so put it inside Formisch's `<Form>` with your own submit button.
112
+ */
113
+ function JsonFields({ of, components, only, except }) {
114
+ const ctx = {
115
+ of,
116
+ components: resolveComponents(components, of)
117
+ };
118
+ const nodes = selectFields(of.fields, {
119
+ only,
120
+ except
121
+ });
122
+ checkComponents(nodes, ctx.components);
123
+ return /* @__PURE__ */ jsx(Fragment, { children: renderFields(ctx, nodes, []) });
124
+ }
125
+ // src/use-json-form.ts
126
+ function useJsonForm(definition, options = {}) {
127
+ const [built] = useState(() => {
128
+ const builder = options.builder ?? createSchemaBuilder();
129
+ return {
130
+ definition,
131
+ builder,
132
+ schema: builder.build(definition),
133
+ initialInput: builder.getInitialInput(definition),
134
+ fields: builder.getFieldTree(definition)
135
+ };
136
+ });
137
+ const form = useForm({
138
+ schema: built.schema,
139
+ initialInput: built.initialInput,
140
+ validate: options.validate,
141
+ revalidate: options.revalidate
142
+ });
143
+ const { components } = options;
144
+ return useMemo(() => ({
145
+ form,
146
+ definition: built.definition,
147
+ builder: built.builder,
148
+ components,
149
+ fields: built.fields
150
+ }), [
151
+ form,
152
+ built,
153
+ components
154
+ ]);
155
+ }
156
+ export { JsonField, JsonFields, useJsonForm };
157
+
158
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["Display","Group"],"sources":["../src/array-field.tsx","../src/value-field.tsx","../src/render-fields.tsx","../src/json-field.tsx","../src/json-fields.tsx","../src/use-json-form.ts"],"sourcesContent":["import { insert, move, remove, useFieldArray } from '@formisch/react'\nimport type { FieldNode, FieldPath } from '@whatsaaaa/form-builder-schema'\nimport { type ComponentType, type ReactElement, type ReactNode, useMemo } from 'react'\nimport type { ArrayFieldProps } from './field-components'\nimport type { AnyJsonFormInstance } from './use-json-form'\n\ninterface ArrayFieldConnectionProps {\n of: AnyJsonFormInstance\n node: FieldNode\n path: FieldPath\n component: ComponentType<ArrayFieldProps>\n /** Renders the fields of the item at `index`. */\n renderItem: (index: number) => ReactNode\n}\n\n/** Connects a container of kind `array` to Formisch, and renders your component for it. */\nexport function ArrayField({\n of,\n node,\n path,\n component: Component,\n renderItem,\n}: ArrayFieldConnectionProps): ReactElement {\n // The path comes from the definition at runtime, so the schema's types can't check it.\n const array = useFieldArray(of.form, { path: path as never })\n\n // Formisch keeps the path it got while the array is the same, so the methods stay the same too.\n const { form, builder } = of\n const { field } = node\n const at = array.path\n const methods = useMemo(\n () => ({\n // A new item starts with the defaults of its fields, like the items of a new form.\n add: () => insert(form, { path: at, initialInput: builder.getItemInput(field) as never }),\n remove: (index: number) => remove(form, { path: at, at: index }),\n move: (from: number, to: number) => move(form, { path: at, from, to }),\n }),\n [form, builder, field, at],\n )\n\n return (\n <Component field={field} path={path} items={array.items} errors={array.errors} {...methods}>\n {renderItem}\n </Component>\n )\n}\n","import { useField } from '@formisch/react'\nimport {\n coerceInput,\n type FieldNode,\n type FieldPath,\n isRequired,\n} from '@whatsaaaa/form-builder-schema'\nimport { type ComponentType, type ReactElement, useCallback } from 'react'\nimport type { ValueFieldProps } from './field-components'\nimport type { AnyJsonFormInstance } from './use-json-form'\n\ninterface ValueFieldConnectionProps {\n of: AnyJsonFormInstance\n node: FieldNode\n path: FieldPath\n component: ComponentType<ValueFieldProps>\n}\n\n/** Connects a field with a value to Formisch, and renders your component for it. */\nexport function ValueField({\n of,\n node,\n path,\n component: Component,\n}: ValueFieldConnectionProps): ReactElement {\n // The path comes from the definition at runtime, so the schema's types can't check it.\n const field = useField(of.form, { path: path as never })\n const setInput = useCallback(\n (input: unknown) => field.onChange(coerceInput(input, node) as never),\n [field, node],\n )\n\n return (\n <Component\n field={node.field}\n path={path}\n kind={node.kind}\n required={isRequired(node.field)}\n input={field.input}\n errors={field.errors}\n inputProps={field.props}\n setInput={setInput}\n />\n )\n}\n","import { dotPathOf, type FieldNode, toFieldPath } from '@whatsaaaa/form-builder-schema'\nimport type { ComponentType, ReactElement } from 'react'\nimport { ArrayField } from './array-field'\nimport type {\n ArrayFieldProps,\n DisplayFieldProps,\n FieldComponents,\n GroupFieldProps,\n ValueFieldProps,\n} from './field-components'\nimport type { AnyJsonFormInstance } from './use-json-form'\nimport { ValueField } from './value-field'\n\n/** What `<JsonFields>` and `<JsonField>` render with. */\nexport interface RenderContext {\n of: AnyJsonFormInstance\n components: FieldComponents\n}\n\n/** The components from the props, or else the ones `useJsonForm` got. */\nexport function resolveComponents(\n components: FieldComponents | undefined,\n of: AnyJsonFormInstance,\n): FieldComponents {\n const resolved = components ?? of.components\n if (!resolved) {\n throw new TypeError('No field components. Pass them to useJsonForm, or to the component.')\n }\n return resolved\n}\n\n/** `indexes` are the indexes of the array items the fields are in, outermost first. */\nexport function renderFields(\n ctx: RenderContext,\n nodes: readonly FieldNode[],\n indexes: readonly number[],\n): ReactElement[] {\n return nodes.map((node, position) => renderField(ctx, node, indexes, position))\n}\n\nexport function renderField(\n ctx: RenderContext,\n node: FieldNode,\n indexes: readonly number[],\n position: number,\n): ReactElement {\n // The fields were checked for components, and the role tells which props the component takes.\n const component = ctx.components[node.type]\n const path = toFieldPath(node.dataPath, indexes)\n const key = dotPathOf(node) ?? `#${position}`\n switch (node.role) {\n case 'display': {\n const Display = component as ComponentType<DisplayFieldProps>\n return <Display key={key} field={node.field} />\n }\n case 'group': {\n const Group = component as ComponentType<GroupFieldProps>\n return (\n <Group key={key} field={node.field} path={path}>\n {renderFields(ctx, node.children, indexes)}\n </Group>\n )\n }\n case 'array':\n return (\n <ArrayField\n key={key}\n of={ctx.of}\n node={node}\n path={path}\n component={component as ComponentType<ArrayFieldProps>}\n renderItem={(index) => renderFields(ctx, node.children, [...indexes, index])}\n />\n )\n case 'value':\n return (\n <ValueField\n key={key}\n of={ctx.of}\n node={node}\n path={path}\n component={component as ComponentType<ValueFieldProps>}\n />\n )\n }\n}\n","import { checkComponents, findField } from '@whatsaaaa/form-builder-schema'\nimport type { ReactElement } from 'react'\nimport type { FieldComponents } from './field-components'\nimport { type RenderContext, renderField, resolveComponents } from './render-fields'\nimport type { AnyJsonFormInstance } from './use-json-form'\n\nexport interface JsonFieldProps {\n /** What `useJsonForm` returned. */\n of: AnyJsonFormInstance\n /** The field's path: `'email'`, or `'address.city'` for a field of a group. */\n path: string\n /** Defaults to the components `useJsonForm` got. */\n components?: FieldComponents | undefined\n}\n\n/**\n * Renders one field of a `useJsonForm` form, wherever you put it inside Formisch's `<Form>`. A\n * group or an array comes with its fields. Leave the field out of `<JsonFields>` with `except`.\n */\nexport function JsonField({ of, path, components }: JsonFieldProps): ReactElement {\n const node = findField(of.fields, path, { placed: true })\n const ctx: RenderContext = { of, components: resolveComponents(components, of) }\n checkComponents([node], ctx.components)\n\n return renderField(ctx, node, [], 0)\n}\n","import { checkComponents, selectFields } from '@whatsaaaa/form-builder-schema'\nimport type { ReactElement } from 'react'\nimport type { FieldComponents } from './field-components'\nimport { type RenderContext, renderFields, resolveComponents } from './render-fields'\nimport type { AnyJsonFormInstance } from './use-json-form'\n\nexport interface JsonFieldsProps {\n /** What `useJsonForm` returned. */\n of: AnyJsonFormInstance\n /** Defaults to the components `useJsonForm` got. */\n components?: FieldComponents | undefined\n /** Renders only these fields, by path, in the order of the definition. */\n only?: readonly string[] | undefined\n /** Leaves out these fields, by path, e.g. the ones you place with `<JsonField>`. */\n except?: readonly string[] | undefined\n}\n\n/**\n * Renders the fields of a `useJsonForm` form with your components, one per field type. It renders\n * nothing of its own, so put it inside Formisch's `<Form>` with your own submit button.\n */\nexport function JsonFields({ of, components, only, except }: JsonFieldsProps): ReactElement {\n const ctx: RenderContext = { of, components: resolveComponents(components, of) }\n const nodes = selectFields(of.fields, { only, except })\n checkComponents(nodes, ctx.components)\n\n return <>{renderFields(ctx, nodes, [])}</>\n}\n","import { type FormSchema, type FormStore, useForm, type ValidationMode } from '@formisch/react'\nimport {\n createSchemaBuilder,\n type FieldNode,\n type JsonForm,\n type JsonFormSchema,\n type SchemaBuilder,\n type TypedJsonFormSchema,\n} from '@whatsaaaa/form-builder-schema'\nimport { useMemo, useState } from 'react'\nimport type { FieldComponents } from './field-components'\n\nexport interface UseJsonFormOptions {\n /** Builds the schema and knows your own types and rules. Defaults to `createSchemaBuilder()`. */\n builder?: SchemaBuilder\n /** Your component for each field type, so `<JsonFields>` and `<JsonField>` don't need them. */\n components?: FieldComponents\n /** When a field is validated the first time, as in Formisch. */\n validate?: ValidationMode\n /** When a field is validated again after that, as in Formisch. */\n revalidate?: Exclude<ValidationMode, 'initial'>\n}\n\n/** What `useJsonForm` returns. Pass it to `<JsonFields>`. */\nexport interface JsonFormInstance<TSchema extends FormSchema = JsonFormSchema> {\n /** The Formisch form: pass it to Formisch's `<Form of>`, or to any of its methods. */\n readonly form: FormStore<TSchema>\n readonly definition: JsonForm\n readonly builder: SchemaBuilder\n /** The components from the options, if any. */\n readonly components: FieldComponents | undefined\n /** The definition's fields as a tree, the way `<JsonFields>` renders them. */\n readonly fields: readonly FieldNode[]\n}\n\n/** Any form `useJsonForm` returns, typed or not: what the components take. */\nexport type AnyJsonFormInstance = JsonFormInstance<FormSchema>\n\n/**\n * A Formisch form for a definition, with its schema and initial input from the builder. A\n * definition with issues throws the builder's `DefinitionError`, so run `validate()` first when it\n * comes from outside your code. Like Formisch's `useForm`, it reads the definition, the builder and\n * the validation modes on the first render: give the component a new `key` to use another\n * definition. The components are read on every render.\n */\nexport function useJsonForm(definition: JsonForm, options?: UseJsonFormOptions): JsonFormInstance\n/**\n * The same, with the form typed by your input type, and output type when it differs, so Formisch's\n * paths and values are checked. The types are yours to keep in step with the definition.\n */\nexport function useJsonForm<TInput extends object, TOutput extends object = TInput>(\n definition: JsonForm,\n options?: UseJsonFormOptions,\n): JsonFormInstance<TypedJsonFormSchema<TInput, TOutput>>\nexport function useJsonForm(\n definition: JsonForm,\n options: UseJsonFormOptions = {},\n): JsonFormInstance {\n // Formisch reads its config on the first render only, so the schema is built once too.\n const [built] = useState(() => {\n const builder = options.builder ?? createSchemaBuilder()\n return {\n definition,\n builder,\n schema: builder.build(definition),\n initialInput: builder.getInitialInput(definition),\n fields: builder.getFieldTree(definition),\n }\n })\n const form = useForm({\n schema: built.schema,\n initialInput: built.initialInput,\n validate: options.validate,\n revalidate: options.revalidate,\n })\n const { components } = options\n return useMemo(\n () => ({\n form,\n definition: built.definition,\n builder: built.builder,\n components,\n fields: built.fields,\n }),\n [form, built, components],\n )\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,68 @@
1
1
  {
2
2
  "name": "@whatsaaaa/form-builder-react",
3
- "version": "0.0.0",
4
- "description": "Placeholder - first real release is published from CI.",
3
+ "version": "0.1.0",
4
+ "description": "Render JSON form definitions with Formisch and your own React components.",
5
+ "keywords": [
6
+ "form",
7
+ "formisch",
8
+ "headless",
9
+ "json",
10
+ "react"
11
+ ],
12
+ "homepage": "https://github.com/whatsaaaa/form-builder",
13
+ "bugs": "https://github.com/whatsaaaa/form-builder/issues",
5
14
  "license": "MIT",
6
- "repository": { "type": "git", "url": "git+https://github.com/whatsaaaa/form-builder.git", "directory": "frameworks/react" }
7
- }
15
+ "author": "Whatsaaaa",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/whatsaaaa/form-builder.git",
19
+ "directory": "frameworks/react"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "type": "module",
25
+ "sideEffects": false,
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ }
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "dependencies": {
36
+ "@whatsaaaa/form-builder-schema": "^0.13.0"
37
+ },
38
+ "devDependencies": {
39
+ "@arethetypeswrong/cli": "^0.18.5",
40
+ "@formisch/react": "^1.2.0",
41
+ "@size-limit/preset-small-lib": "^14.3.0",
42
+ "@types/react": "^19.3.0",
43
+ "@types/react-dom": "^19.3.0",
44
+ "@vitest/coverage-v8": "^5.0.5",
45
+ "happy-dom": "^20.0.0",
46
+ "publint": "^0.3.25",
47
+ "react": "^19.3.1",
48
+ "react-dom": "^19.3.1",
49
+ "size-limit": "^14.3.0",
50
+ "tsdown": "^0.23.2",
51
+ "typescript": "~6.0.3",
52
+ "valibot": "^1.5.0",
53
+ "vitest": "^5.0.5"
54
+ },
55
+ "peerDependencies": {
56
+ "@formisch/react": "^1.2.0",
57
+ "react": "^18.0.0 || ^19.0.0",
58
+ "valibot": "^1.4.1"
59
+ },
60
+ "scripts": {
61
+ "build": "tsdown",
62
+ "test": "vitest run",
63
+ "test:coverage": "vitest run --coverage",
64
+ "type-check": "tsc --noEmit",
65
+ "lint:pkg": "publint && pnpm pack --pack-destination .attw && attw .attw/*.tgz --profile esm-only",
66
+ "size": "size-limit"
67
+ }
68
+ }