@whatsaaaa/form-builder-react 0.1.0 → 0.2.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/README.md CHANGED
@@ -49,12 +49,13 @@ export function DefinitionForm({ definition }: { definition: JsonForm }) {
49
49
  useJsonForm(definition, {
50
50
  builder: createSchemaBuilder({ fieldTypes: { money } }), // your own types, rules and messages
51
51
  components, // your component per field type, see below
52
+ layoutComponents, // your component per layout type, see Layout
52
53
  validate: 'blur', // when a field is validated the first time, as in Formisch
53
54
  revalidate: 'input', // when it is validated again
54
55
  })
55
56
  ```
56
57
 
57
- `builder`, `validate` and `revalidate` are read on the first render, with the definition. `components` is read on every render.
58
+ `builder`, `validate` and `revalidate` are read on the first render, with the definition. `components` and `layoutComponents` are read on every render.
58
59
 
59
60
  ## Components
60
61
 
@@ -208,9 +209,122 @@ export function Heading({ field }: DisplayFieldProps) {
208
209
  }
209
210
  ```
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
+ ```
260
+
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` |
265
+
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.
271
+
272
+ ### Steps
273
+
274
+ `jsonForm.steps` is where the form is, and how to move:
275
+
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` | every step, with its `step` as written (`id`, `label`, `description`, `ui`), `children` and `fields` |
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
+
211
325
  ## Placing fields yourself
212
326
 
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`:
327
+ 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`:
214
328
 
215
329
  ```tsx
216
330
  <Form of={jsonForm.form} onSubmit={save}>
@@ -227,7 +341,7 @@ export function Heading({ field }: DisplayFieldProps) {
227
341
 
228
342
  - 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
343
  - 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.
344
+ - `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.
231
345
  - 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
346
 
233
347
  ## Typing the form
@@ -257,4 +371,4 @@ useFieldArray(jsonForm.form, { path: ['lines'] })
257
371
 
258
372
  - 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
373
  - `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.
374
+ - 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.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
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";
1
+ import { FieldNode, FieldNode as FieldNode$1, FieldPath, FieldPath as FieldPath$1, FieldRole, FormLayout, FormLayout as FormLayout$1, JsonField as JsonField$1, JsonForm, JsonFormSchema, JsonFormSchema as JsonFormSchema$1, JsonLayoutRow, JsonLayoutSection, LayoutNode, LayoutStep, LayoutStep as LayoutStep$1, SchemaBuilder, TypedJsonFormSchema, TypedJsonFormSchema as TypedJsonFormSchema$1, ValueKind } from "@whatsaaaa/form-builder-schema";
2
2
  import { FieldElementProps, FormSchema, FormStore, ValidationMode } from "@formisch/react";
3
3
  import { ComponentType, ReactElement, ReactNode } from "react";
4
4
  /**
@@ -49,11 +49,61 @@ interface ArrayFieldProps {
49
49
  /** Renders the fields of the item at `index`. */
50
50
  children: (index: number) => ReactNode;
51
51
  }
52
+ /**
53
+ * Your component for each layout type a definition's `layout` uses: `{ section: Card, row: Grid }`.
54
+ * A definition without sections or rows needs none.
55
+ */
56
+ interface LayoutComponents {
57
+ section?: ComponentType<SectionProps>;
58
+ row?: ComponentType<RowProps>;
59
+ }
60
+ /** The props of a component for a section of the layout. */
61
+ interface SectionProps {
62
+ /** The section as written, with its `id`, `label`, `description` and `ui`. */
63
+ section: JsonLayoutSection;
64
+ /** The section's items. */
65
+ children: ReactNode;
66
+ }
67
+ /** The props of a component for a row of the layout: one line of a 12-column grid. */
68
+ interface RowProps {
69
+ /** The row as written, with its `id` and `ui`. */
70
+ row: JsonLayoutRow;
71
+ /** How many of the 12 columns each cell takes, in order. They add up to 12 at most. */
72
+ spans: number[];
73
+ /** Renders the cell at `index`. */
74
+ children: (index: number) => ReactNode;
75
+ }
76
+ /** The steps of a form, from its layout. A form without `steps` in its layout has one. */
77
+ interface JsonFormSteps {
78
+ /** Every step, in order. */
79
+ readonly list: readonly LayoutStep$1[];
80
+ /** The index of the current step in `list`. */
81
+ readonly index: number;
82
+ /** The step `<JsonFields>` renders. */
83
+ readonly current: LayoutStep$1;
84
+ readonly isFirst: boolean;
85
+ readonly isLast: boolean;
86
+ /**
87
+ * Validates the whole form, and moves to the next step when the fields of this one have no
88
+ * issues. Otherwise it stays, and focuses the first field with an issue, in layout order.
89
+ * Resolves to whether it moved.
90
+ */
91
+ next(): Promise<boolean>;
92
+ /** Moves to the step before, without validating. */
93
+ back(): void;
94
+ /**
95
+ * Moves to the step at `index`. Back is free, and forward validates every step on the way, like
96
+ * `next()`, and stops at the first with issues. Resolves to whether it got there.
97
+ */
98
+ goTo(index: number): Promise<boolean>;
99
+ }
52
100
  interface UseJsonFormOptions {
53
101
  /** Builds the schema and knows your own types and rules. Defaults to `createSchemaBuilder()`. */
54
102
  builder?: SchemaBuilder;
55
103
  /** Your component for each field type, so `<JsonFields>` and `<JsonField>` don't need them. */
56
104
  components?: FieldComponents;
105
+ /** Your component for each layout type the definition's `layout` uses, like `section`. */
106
+ layoutComponents?: LayoutComponents;
57
107
  /** When a field is validated the first time, as in Formisch. */
58
108
  validate?: ValidationMode;
59
109
  /** When a field is validated again after that, as in Formisch. */
@@ -67,8 +117,14 @@ interface JsonFormInstance<TSchema extends FormSchema = JsonFormSchema$1> {
67
117
  readonly builder: SchemaBuilder;
68
118
  /** The components from the options, if any. */
69
119
  readonly components: FieldComponents | undefined;
70
- /** The definition's fields as a tree, the way `<JsonFields>` renders them. */
120
+ /** The layout components from the options, if any. */
121
+ readonly layoutComponents: LayoutComponents | undefined;
122
+ /** The definition's fields as a tree, for `<JsonFields only>` and `<JsonField>`. */
71
123
  readonly fields: readonly FieldNode$1[];
124
+ /** The definition's layout, the way `<JsonFields>` renders it. */
125
+ readonly layout: FormLayout$1;
126
+ /** The layout's steps: the current one, and Next, Back and Go to. A form without steps has one. */
127
+ readonly steps: JsonFormSteps;
72
128
  }
73
129
  /** Any form `useJsonForm` returns, typed or not: what the components take. */
74
130
  type AnyJsonFormInstance = JsonFormInstance<FormSchema>;
@@ -77,7 +133,7 @@ type AnyJsonFormInstance = JsonFormInstance<FormSchema>;
77
133
  * definition with issues throws the builder's `DefinitionError`, so run `validate()` first when it
78
134
  * comes from outside your code. Like Formisch's `useForm`, it reads the definition, the builder and
79
135
  * 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.
136
+ * definition. The components and layout components are read on every render.
81
137
  */
82
138
  export declare function useJsonForm(definition: JsonForm, options?: UseJsonFormOptions): JsonFormInstance;
83
139
  /**
@@ -103,15 +159,25 @@ interface JsonFieldsProps {
103
159
  of: AnyJsonFormInstance;
104
160
  /** Defaults to the components `useJsonForm` got. */
105
161
  components?: FieldComponents | undefined;
106
- /** Renders only these fields, by path, in the order of the definition. */
162
+ /** Defaults to the layout components `useJsonForm` got. */
163
+ layoutComponents?: LayoutComponents | undefined;
164
+ /** The content of the layout's slots, by name: `{ submit: <button type="submit">Save</button> }`. */
165
+ slots?: Readonly<Record<string, ReactNode>> | undefined;
166
+ /** Renders only these fields, by path, in the order of the definition, without the layout. */
107
167
  only?: readonly string[] | undefined;
108
- /** Leaves out these fields, by path, e.g. the ones you place with `<JsonField>`. */
168
+ /**
169
+ * Leaves out these fields, by path, e.g. the ones you place with `<JsonField>`. A section or a
170
+ * row left without fields drops out too.
171
+ */
109
172
  except?: readonly string[] | undefined;
173
+ /** Renders the step at this index instead of the current one, e.g. on a review page. */
174
+ step?: number | undefined;
110
175
  }
111
176
  /**
112
- * Renders the fields of a `useJsonForm` form with your components, one per field type. It renders
177
+ * Renders the fields of a `useJsonForm` form with your components, one per field type, where the
178
+ * definition's `layout` puts them: the current step, with its sections, rows and slots. It renders
113
179
  * nothing of its own, so put it inside Formisch's `<Form>` with your own submit button.
114
180
  */
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 };
181
+ export declare function JsonFields(props: JsonFieldsProps): ReactElement;
182
+ export type { AnyJsonFormInstance, ArrayFieldProps, DisplayFieldProps, FieldComponents, FieldNode, FieldPath, FieldRole, FormLayout, GroupFieldProps, JsonFieldProps, JsonFieldsProps, JsonFormInstance, JsonFormSchema, JsonFormSteps, LayoutComponents, LayoutNode, LayoutStep, RowProps, SectionProps, TypedJsonFormSchema, UseJsonFormOptions, ValueFieldProps };
117
183
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,7 +1,159 @@
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";
1
+ import { checkComponents, checkLayoutComponents, coerceInput, createSchemaBuilder, dotPathOf, fieldPathsOf, findField, isRequired, issuesOfStep, selectFields, shownNodes, toFieldPath } from "@whatsaaaa/form-builder-schema";
2
+ import { focus, getErrors, getInput, insert, move, remove, setErrors, useField, useFieldArray, useForm, validate } from "@formisch/react";
3
+ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { Fragment as Fragment$1, jsx } from "react/jsx-runtime";
5
+ // src/json-form-steps.ts
6
+ const fieldEvents = /* @__PURE__ */ new WeakMap();
7
+ /** What a field of `form` calls on each event, for a form with steps. Undefined with one step. */
8
+ const fieldEventsOf = (form) => fieldEvents.get(form);
9
+ /**
10
+ * The steps of `layout`, for `useJsonForm`. With more than one, a step shows the errors it had
11
+ * when the user left it, and none the first time they get to it, a step that failed Next revalidates as the user fixes it, a submit
12
+ * before the last step moves on like Next, and a submit that fails because of an earlier step
13
+ * goes back to it. Like Formisch's `useForm`, it reads the validation modes on the first render.
14
+ */
15
+ function useJsonFormSteps(form, layout, modes) {
16
+ const list = layout.steps;
17
+ const [index, setIndex] = useState(0);
18
+ const state = useRef({
19
+ index: 0,
20
+ left: /* @__PURE__ */ new Map(),
21
+ failed: /* @__PURE__ */ new Set(),
22
+ focus: void 0,
23
+ submitting: false,
24
+ modes
25
+ });
26
+ const methods = useMemo(() => {
27
+ const at = state.current;
28
+ async function goTo(target) {
29
+ if (!Number.isInteger(target) || target < 0 || target >= list.length) throw new RangeError(`No step ${target}. The form has ${list.length}.`);
30
+ if (target <= at.index) {
31
+ move(target);
32
+ return true;
33
+ }
34
+ const { issues } = await validate(form);
35
+ for (let step = at.index; step < target; step++) {
36
+ const found = issuesOfStep(list[step], issues);
37
+ if (found.length) {
38
+ stop(step, found);
39
+ return false;
40
+ }
41
+ at.failed.delete(step);
42
+ }
43
+ move(target);
44
+ return true;
45
+ }
46
+ /**
47
+ * Moves to a step as the user left it: with the errors it had then, and none the first time, and
48
+ * revalidating if it failed Next. Validating the steps before set errors on its fields, which
49
+ * the user never saw.
50
+ */
51
+ function move(target) {
52
+ leave();
53
+ const errors = at.left.get(target);
54
+ for (const path of fieldPathsOf(list[target], getInput(form))) setErrors(form, {
55
+ path,
56
+ errors: errors?.get(JSON.stringify(path)) ?? null
57
+ });
58
+ at.index = target;
59
+ setIndex(target);
60
+ }
61
+ /** Keeps the errors of the current step, for when the user comes back to it. */
62
+ function leave() {
63
+ const errors = fieldPathsOf(list[at.index], getInput(form)).map((path) => [JSON.stringify(path), getErrors(form, { path })]);
64
+ at.left.set(at.index, new Map(errors));
65
+ }
66
+ /** Stays on, or goes to, the step at `target`, with its issues shown. */
67
+ function stop(target, found) {
68
+ at.failed.add(target);
69
+ const path = found.find((issue) => issue.field.role === "value")?.path;
70
+ if (target === at.index) {
71
+ if (path) focus(form, { path });
72
+ return;
73
+ }
74
+ leave();
75
+ at.focus = path;
76
+ at.index = target;
77
+ setIndex(target);
78
+ }
79
+ const events = (event) => {
80
+ const validateMode = at.modes.validate ?? "submit";
81
+ if (!at.failed.has(at.index) || validateMode !== "submit" || form.isSubmitted) return;
82
+ if (event === (at.modes.revalidate ?? "input")) validate(form);
83
+ };
84
+ /** After a failed submit: back to the first step before this one with issues, if any. */
85
+ async function afterSubmit() {
86
+ const { issues } = await validate(form);
87
+ const step = list.findIndex((each) => issuesOfStep(each, issues).length);
88
+ if (step !== -1 && step < at.index) stop(step, issuesOfStep(list[step], issues));
89
+ }
90
+ const next = () => at.index === list.length - 1 ? Promise.resolve(false) : goTo(at.index + 1);
91
+ const back = () => {
92
+ if (at.index > 0) move(at.index - 1);
93
+ };
94
+ return {
95
+ goTo,
96
+ next,
97
+ back,
98
+ events,
99
+ afterSubmit
100
+ };
101
+ }, [form, list]);
102
+ useEffect(() => {
103
+ const path = state.current.focus;
104
+ state.current.focus = void 0;
105
+ if (path) focus(form, { path });
106
+ }, [form, index]);
107
+ const stepped = list.length > 1;
108
+ useEffect(() => {
109
+ if (!stepped) return;
110
+ fieldEvents.set(form, methods.events);
111
+ const onSubmit = (event) => {
112
+ if (event.target !== formElementOf(form)) return;
113
+ if (state.current.index === list.length - 1) {
114
+ state.current.submitting = true;
115
+ return;
116
+ }
117
+ event.preventDefault();
118
+ event.stopImmediatePropagation();
119
+ methods.next();
120
+ };
121
+ document.addEventListener("submit", onSubmit, { capture: true });
122
+ return () => {
123
+ fieldEvents.delete(form);
124
+ document.removeEventListener("submit", onSubmit, { capture: true });
125
+ };
126
+ }, [
127
+ form,
128
+ list,
129
+ methods,
130
+ stepped
131
+ ]);
132
+ const submitting = form.isSubmitting;
133
+ useEffect(() => {
134
+ if (!state.current.submitting || submitting) return;
135
+ state.current.submitting = false;
136
+ if (!form.isValid) methods.afterSubmit();
137
+ });
138
+ return useMemo(() => ({
139
+ list,
140
+ index,
141
+ current: list[index],
142
+ isFirst: index === 0,
143
+ isLast: index === list.length - 1,
144
+ next: methods.next,
145
+ back: methods.back,
146
+ goTo: methods.goTo
147
+ }), [
148
+ list,
149
+ index,
150
+ methods
151
+ ]);
152
+ }
153
+ /** The `<form>` Formisch's `<Form>` renders, which it keeps on the store for its `submit()`. */
154
+ function formElementOf(form) {
155
+ return form["~internal"]?.element;
156
+ }
5
157
  // src/array-field.tsx
6
158
  /** Connects a container of kind `array` to Formisch, and renders your component for it. */
7
159
  function ArrayField({ of, node, path, component: Component, renderItem }) {
@@ -10,19 +162,28 @@ function ArrayField({ of, node, path, component: Component, renderItem }) {
10
162
  const { field } = node;
11
163
  const at = array.path;
12
164
  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
- })
165
+ add: () => {
166
+ insert(form, {
167
+ path: at,
168
+ initialInput: builder.getItemInput(field)
169
+ });
170
+ fieldEventsOf(form)?.("input");
171
+ },
172
+ remove: (index) => {
173
+ remove(form, {
174
+ path: at,
175
+ at: index
176
+ });
177
+ fieldEventsOf(form)?.("input");
178
+ },
179
+ move: (from, to) => {
180
+ move(form, {
181
+ path: at,
182
+ from,
183
+ to
184
+ });
185
+ fieldEventsOf(form)?.("input");
186
+ }
26
187
  }), [
27
188
  form,
28
189
  builder,
@@ -42,7 +203,31 @@ function ArrayField({ of, node, path, component: Component, renderItem }) {
42
203
  /** Connects a field with a value to Formisch, and renders your component for it. */
43
204
  function ValueField({ of, node, path, component: Component }) {
44
205
  const field = useField(of.form, { path });
45
- const setInput = useCallback((input) => field.onChange(coerceInput(input, node)), [field, node]);
206
+ const { form } = of;
207
+ const setInput = useCallback((input) => {
208
+ field.onChange(coerceInput(input, node));
209
+ fieldEventsOf(form)?.("input");
210
+ }, [
211
+ field,
212
+ node,
213
+ form
214
+ ]);
215
+ const inputProps = of.steps.list.length > 1 ? {
216
+ ...field.props,
217
+ onFocus: () => {
218
+ field.props.onFocus();
219
+ fieldEventsOf(form)?.("touch");
220
+ },
221
+ onChange: (event) => {
222
+ field.props.onChange(event);
223
+ fieldEventsOf(form)?.("input");
224
+ fieldEventsOf(form)?.("change");
225
+ },
226
+ onBlur: () => {
227
+ field.props.onBlur();
228
+ fieldEventsOf(form)?.("blur");
229
+ }
230
+ } : field.props;
46
231
  return /* @__PURE__ */ jsx(Component, {
47
232
  field: node.field,
48
233
  path,
@@ -50,7 +235,7 @@ function ValueField({ of, node, path, component: Component }) {
50
235
  required: isRequired(node.field),
51
236
  input: field.input,
52
237
  errors: field.errors,
53
- inputProps: field.props,
238
+ inputProps,
54
239
  setInput
55
240
  });
56
241
  }
@@ -63,25 +248,28 @@ function resolveComponents(components, of) {
63
248
  }
64
249
  /** `indexes` are the indexes of the array items the fields are in, outermost first. */
65
250
  function renderFields(ctx, nodes, indexes) {
66
- return nodes.map((node, position) => renderField(ctx, node, indexes, position));
251
+ return nodes.map((node, position) => renderField(ctx, node, indexes, dotPathOf(node) ?? `#${position}`, (inner) => renderFields(ctx, node.children, inner)));
67
252
  }
68
- function renderField(ctx, node, indexes, position) {
253
+ /**
254
+ * Renders one field with its component. `children` renders the fields inside a group, or inside
255
+ * the item of an array, with the indexes of the items they are in.
256
+ */
257
+ function renderField(ctx, node, indexes, key, children) {
69
258
  const component = ctx.components[node.type];
70
259
  const path = toFieldPath(node.dataPath, indexes);
71
- const key = dotPathOf(node) ?? `#${position}`;
72
260
  switch (node.role) {
73
261
  case "display": return /* @__PURE__ */ jsx(component, { field: node.field }, key);
74
262
  case "group": return /* @__PURE__ */ jsx(component, {
75
263
  field: node.field,
76
264
  path,
77
- children: renderFields(ctx, node.children, indexes)
265
+ children: children(indexes)
78
266
  }, key);
79
267
  case "array": return /* @__PURE__ */ jsx(ArrayField, {
80
268
  of: ctx.of,
81
269
  node,
82
270
  path,
83
271
  component,
84
- renderItem: (index) => renderFields(ctx, node.children, [...indexes, index])
272
+ renderItem: (index) => children([...indexes, index])
85
273
  }, key);
86
274
  case "value": return /* @__PURE__ */ jsx(ValueField, {
87
275
  of: ctx.of,
@@ -103,24 +291,80 @@ function JsonField({ of, path, components }) {
103
291
  components: resolveComponents(components, of)
104
292
  };
105
293
  checkComponents([node], ctx.components);
106
- return renderField(ctx, node, [], 0);
294
+ return renderField(ctx, node, [], dotPathOf(node) ?? "#0", (indexes) => renderFields(ctx, node.children, indexes));
295
+ }
296
+ // src/render-layout.tsx
297
+ /**
298
+ * Renders layout nodes. A field left out drops out, and so does a section or a row left without
299
+ * fields. `indexes` are the indexes of the array items the nodes are in, outermost first.
300
+ */
301
+ function renderLayout(ctx, nodes, indexes) {
302
+ return shownNodes(nodes, indexes, ctx.isShown).map((node) => renderNode(ctx, node, indexes));
303
+ }
304
+ function renderNode(ctx, node, indexes) {
305
+ const { key } = node;
306
+ switch (node.type) {
307
+ case "field": return renderField(ctx, node.node, indexes, key, (inner) => renderLayout(ctx, node.children, inner));
308
+ case "section": {
309
+ const Section = ctx.layoutComponents.section;
310
+ return /* @__PURE__ */ jsx(Section, {
311
+ section: node.section,
312
+ children: renderLayout(ctx, node.children, indexes)
313
+ }, key);
314
+ }
315
+ case "row": {
316
+ const Row = ctx.layoutComponents.row;
317
+ return /* @__PURE__ */ jsx(Row, {
318
+ row: node.row,
319
+ spans: node.children.map((cell) => cell.span),
320
+ children: (index) => renderNode(ctx, node.children[index], indexes)
321
+ }, key);
322
+ }
323
+ case "slot": return /* @__PURE__ */ jsx(Fragment, { children: ctx.slots[node.name] }, key);
324
+ }
107
325
  }
108
326
  // src/json-fields.tsx
109
327
  /**
110
- * Renders the fields of a `useJsonForm` form with your components, one per field type. It renders
328
+ * Renders the fields of a `useJsonForm` form with your components, one per field type, where the
329
+ * definition's `layout` puts them: the current step, with its sections, rows and slots. It renders
111
330
  * nothing of its own, so put it inside Formisch's `<Form>` with your own submit button.
112
331
  */
113
- function JsonFields({ of, components, only, except }) {
332
+ function JsonFields(props) {
333
+ const { of, only, except } = props;
114
334
  const ctx = {
115
335
  of,
116
- components: resolveComponents(components, of)
336
+ components: resolveComponents(props.components, of)
117
337
  };
118
338
  const nodes = selectFields(of.fields, {
119
339
  only,
120
340
  except
121
341
  });
122
- checkComponents(nodes, ctx.components);
123
- return /* @__PURE__ */ jsx(Fragment, { children: renderFields(ctx, nodes, []) });
342
+ if (only || !of.definition.layout) {
343
+ checkComponents(nodes, ctx.components);
344
+ return /* @__PURE__ */ jsx(Fragment$1, { children: renderFields(ctx, nodes, []) });
345
+ }
346
+ const { layout, steps } = of;
347
+ const left = new Set(except);
348
+ const layoutCtx = {
349
+ ...ctx,
350
+ layoutComponents: props.layoutComponents ?? of.layoutComponents ?? {},
351
+ slots: props.slots ?? {},
352
+ isShown: (node) => !left.has(dotPathOf(node) ?? "")
353
+ };
354
+ checkComponents(layout.steps.flatMap((step) => fieldsOf(step.children)), ctx.components);
355
+ checkLayoutComponents(layout, layoutCtx.layoutComponents, layoutCtx.slots);
356
+ const index = props.step ?? steps.index;
357
+ const step = steps.list[index];
358
+ if (!step) throw new RangeError(`No step ${props.step}. The form has ${steps.list.length}.`);
359
+ return /* @__PURE__ */ jsx(Fragment, { children: renderLayout(layoutCtx, step.children, []) }, index);
360
+ }
361
+ /** Every field of the nodes, display elements written in the layout included. */
362
+ function fieldsOf(nodes) {
363
+ return nodes.flatMap((node) => {
364
+ if (node.type === "slot") return [];
365
+ const inner = fieldsOf(node.children);
366
+ return node.type === "field" ? [node.node, ...inner] : inner;
367
+ });
124
368
  }
125
369
  // src/use-json-form.ts
126
370
  function useJsonForm(definition, options = {}) {
@@ -131,7 +375,8 @@ function useJsonForm(definition, options = {}) {
131
375
  builder,
132
376
  schema: builder.build(definition),
133
377
  initialInput: builder.getInitialInput(definition),
134
- fields: builder.getFieldTree(definition)
378
+ fields: builder.getFieldTree(definition),
379
+ layout: builder.getLayout(definition)
135
380
  };
136
381
  });
137
382
  const form = useForm({
@@ -140,17 +385,23 @@ function useJsonForm(definition, options = {}) {
140
385
  validate: options.validate,
141
386
  revalidate: options.revalidate
142
387
  });
143
- const { components } = options;
388
+ const steps = useJsonFormSteps(form, built.layout, options);
389
+ const { components, layoutComponents } = options;
144
390
  return useMemo(() => ({
145
391
  form,
146
392
  definition: built.definition,
147
393
  builder: built.builder,
148
394
  components,
149
- fields: built.fields
395
+ layoutComponents,
396
+ fields: built.fields,
397
+ layout: built.layout,
398
+ steps
150
399
  }), [
151
400
  form,
152
401
  built,
153
- components
402
+ components,
403
+ layoutComponents,
404
+ steps
154
405
  ]);
155
406
  }
156
407
  export { JsonField, JsonFields, useJsonForm };
package/dist/index.js.map CHANGED
@@ -1 +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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"}
1
+ {"version":3,"file":"index.js","names":["Display","Group"],"sources":["../src/json-form-steps.ts","../src/array-field.tsx","../src/value-field.tsx","../src/render-fields.tsx","../src/json-field.tsx","../src/render-layout.tsx","../src/json-fields.tsx","../src/use-json-form.ts"],"sourcesContent":["import {\n focus,\n type FormStore,\n getErrors,\n getInput,\n setErrors,\n validate,\n type ValidationMode,\n} from '@formisch/react'\nimport {\n type FieldPath,\n fieldPathsOf,\n type FormLayout,\n issuesOfStep,\n type LayoutStep,\n type StepIssue,\n} from '@whatsaaaa/form-builder-schema'\nimport { useEffect, useMemo, useRef, useState } from 'react'\n\n/** The steps of a form, from its layout. A form without `steps` in its layout has one. */\nexport interface JsonFormSteps {\n /** Every step, in order. */\n readonly list: readonly LayoutStep[]\n /** The index of the current step in `list`. */\n readonly index: number\n /** The step `<JsonFields>` renders. */\n readonly current: LayoutStep\n readonly isFirst: boolean\n readonly isLast: boolean\n /**\n * Validates the whole form, and moves to the next step when the fields of this one have no\n * issues. Otherwise it stays, and focuses the first field with an issue, in layout order.\n * Resolves to whether it moved.\n */\n next(): Promise<boolean>\n /** Moves to the step before, without validating. */\n back(): void\n /**\n * Moves to the step at `index`. Back is free, and forward validates every step on the way, like\n * `next()`, and stops at the first with issues. Resolves to whether it got there.\n */\n goTo(index: number): Promise<boolean>\n}\n\n/** A field's error messages in Formisch, or null. */\ntype Errors = [string, ...string[]] | null\n\n/** What a field tells the steps it was through, by Formisch's name for the validation mode. */\nexport type FieldEvent = Exclude<ValidationMode, 'initial' | 'submit'>\n\n/** Revalidates a step that failed Next on each field event, like a form after a failed submit. */\ntype FieldEvents = (event: FieldEvent) => void\n\nconst fieldEvents = new WeakMap<FormStore, FieldEvents>()\n\n/** What a field of `form` calls on each event, for a form with steps. Undefined with one step. */\nexport const fieldEventsOf = (form: FormStore): FieldEvents | undefined => fieldEvents.get(form)\n\ninterface Modes {\n validate?: ValidationMode | undefined\n revalidate?: Exclude<ValidationMode, 'initial'> | undefined\n}\n\n/**\n * The steps of `layout`, for `useJsonForm`. With more than one, a step shows the errors it had\n * when the user left it, and none the first time they get to it, a step that failed Next revalidates as the user fixes it, a submit\n * before the last step moves on like Next, and a submit that fails because of an earlier step\n * goes back to it. Like Formisch's `useForm`, it reads the validation modes on the first render.\n */\nexport function useJsonFormSteps(form: FormStore, layout: FormLayout, modes: Modes): JsonFormSteps {\n const list = layout.steps\n const [index, setIndex] = useState(0)\n // What the async methods and the event handlers read: always the latest.\n const state = useRef({\n index: 0,\n // The errors each step had when the user left it, by the JSON of each field's path.\n left: new Map<number, Map<string, Errors>>(),\n // The steps that failed Next, and haven't passed since.\n failed: new Set<number>(),\n // The field to focus once the step it is on has rendered.\n focus: undefined as FieldPath | undefined,\n // Whether a submit on the last step is on its way through Formisch.\n submitting: false,\n modes,\n })\n\n const methods = useMemo(() => {\n const at = state.current\n\n async function goTo(target: number): Promise<boolean> {\n if (!Number.isInteger(target) || target < 0 || target >= list.length) {\n throw new RangeError(`No step ${target}. The form has ${list.length}.`)\n }\n if (target <= at.index) {\n move(target)\n return true\n }\n const { issues } = await validate(form)\n for (let step = at.index; step < target; step++) {\n const found = issuesOfStep(list[step] as LayoutStep, issues)\n if (found.length) {\n stop(step, found)\n return false\n }\n at.failed.delete(step)\n }\n move(target)\n return true\n }\n\n /**\n * Moves to a step as the user left it: with the errors it had then, and none the first time, and\n * revalidating if it failed Next. Validating the steps before set errors on its fields, which\n * the user never saw.\n */\n function move(target: number) {\n leave()\n const errors = at.left.get(target)\n for (const path of fieldPathsOf(list[target] as LayoutStep, getInput(form))) {\n setErrors(form, { path, errors: errors?.get(JSON.stringify(path)) ?? null } as never)\n }\n at.index = target\n setIndex(target)\n }\n\n /** Keeps the errors of the current step, for when the user comes back to it. */\n function leave() {\n const paths = fieldPathsOf(list[at.index] as LayoutStep, getInput(form))\n const errors = paths.map((path): [string, Errors] => [\n JSON.stringify(path),\n getErrors(form, { path } as never),\n ])\n at.left.set(at.index, new Map(errors))\n }\n\n /** Stays on, or goes to, the step at `target`, with its issues shown. */\n function stop(target: number, found: readonly StepIssue[]) {\n at.failed.add(target)\n // A group or an array has no element: the first field that has one gets the focus.\n const path = found.find((issue) => issue.field.role === 'value')?.path\n if (target === at.index) {\n if (path) focus(form, { path: path as never })\n return\n }\n leave()\n at.focus = path\n at.index = target\n setIndex(target)\n }\n\n const events: FieldEvents = (event) => {\n // In the other modes, Formisch revalidates a field with errors itself.\n const validateMode = at.modes.validate ?? 'submit'\n if (!at.failed.has(at.index) || validateMode !== 'submit' || form.isSubmitted) return\n if (event === (at.modes.revalidate ?? 'input')) void validate(form)\n }\n\n /** After a failed submit: back to the first step before this one with issues, if any. */\n async function afterSubmit() {\n const { issues } = await validate(form)\n const step = list.findIndex((each) => issuesOfStep(each, issues).length)\n if (step !== -1 && step < at.index) stop(step, issuesOfStep(list[step] as LayoutStep, issues))\n }\n\n const next = () => (at.index === list.length - 1 ? Promise.resolve(false) : goTo(at.index + 1))\n const back = () => {\n if (at.index > 0) move(at.index - 1)\n }\n return { goTo, next, back, events, afterSubmit }\n }, [form, list])\n\n // The field a step was stopped on, once the step has rendered.\n useEffect(() => {\n const path = state.current.focus\n state.current.focus = undefined\n if (path) focus(form, { path: path as never })\n }, [form, index])\n\n const stepped = list.length > 1\n useEffect(() => {\n if (!stepped) return\n fieldEvents.set(form, methods.events)\n // Before the last step, a submit, like Enter in a step with a single text input, is a Next.\n // It is caught on its way down to the form, before Formisch's own handler sees it.\n const onSubmit = (event: Event) => {\n if (event.target !== formElementOf(form)) return\n if (state.current.index === list.length - 1) {\n state.current.submitting = true\n return\n }\n event.preventDefault()\n event.stopImmediatePropagation()\n void methods.next()\n }\n document.addEventListener('submit', onSubmit, { capture: true })\n return () => {\n fieldEvents.delete(form)\n document.removeEventListener('submit', onSubmit, { capture: true })\n }\n }, [form, list, methods, stepped])\n\n // A submit that fails because of a step before the last, e.g. on a form rule, goes back to it.\n // React can batch the start and the end of a submit into one render, so the submit is marked\n // as it starts, and each render checks whether Formisch has finished it.\n const submitting = form.isSubmitting\n useEffect(() => {\n if (!state.current.submitting || submitting) return\n state.current.submitting = false\n if (!form.isValid) void methods.afterSubmit()\n })\n\n return useMemo(\n () => ({\n list,\n index,\n current: list[index] as LayoutStep,\n isFirst: index === 0,\n isLast: index === list.length - 1,\n next: methods.next,\n back: methods.back,\n goTo: methods.goTo,\n }),\n [list, index, methods],\n )\n}\n\n/** The `<form>` Formisch's `<Form>` renders, which it keeps on the store for its `submit()`. */\nfunction formElementOf(form: FormStore): HTMLFormElement | undefined {\n return (form as unknown as { '~internal'?: { element?: HTMLFormElement } })['~internal']?.element\n}\n","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 { fieldEventsOf } from './json-form-steps'\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 // In a form with steps, a step that failed Next revalidates on a change of its items too.\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: () => {\n insert(form, { path: at, initialInput: builder.getItemInput(field) as never })\n fieldEventsOf(form)?.('input')\n },\n remove: (index: number) => {\n remove(form, { path: at, at: index })\n fieldEventsOf(form)?.('input')\n },\n move: (from: number, to: number) => {\n move(form, { path: at, from, to })\n fieldEventsOf(form)?.('input')\n },\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 { fieldEventsOf } from './json-form-steps'\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 { form } = of\n const setInput = useCallback(\n (input: unknown) => {\n field.onChange(coerceInput(input, node) as never)\n fieldEventsOf(form)?.('input')\n },\n [field, node, form],\n )\n // In a form with steps, a step that failed Next revalidates on these events too.\n const inputProps =\n of.steps.list.length > 1\n ? {\n ...field.props,\n onFocus: () => {\n field.props.onFocus()\n fieldEventsOf(form)?.('touch')\n },\n onChange: (event: Parameters<typeof field.props.onChange>[0]) => {\n field.props.onChange(event)\n fieldEventsOf(form)?.('input')\n fieldEventsOf(form)?.('change')\n },\n onBlur: () => {\n field.props.onBlur()\n fieldEventsOf(form)?.('blur')\n },\n }\n : field.props\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={inputProps}\n setInput={setInput}\n />\n )\n}\n","import { dotPathOf, type FieldNode, toFieldPath } from '@whatsaaaa/form-builder-schema'\nimport type { ComponentType, ReactElement, ReactNode } 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) =>\n renderField(ctx, node, indexes, dotPathOf(node) ?? `#${position}`, (inner) =>\n renderFields(ctx, node.children, inner),\n ),\n )\n}\n\n/**\n * Renders one field with its component. `children` renders the fields inside a group, or inside\n * the item of an array, with the indexes of the items they are in.\n */\nexport function renderField(\n ctx: RenderContext,\n node: FieldNode,\n indexes: readonly number[],\n key: string,\n children: (indexes: readonly number[]) => ReactNode,\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 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 {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) => 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, dotPathOf, findField } from '@whatsaaaa/form-builder-schema'\nimport type { ReactElement } from 'react'\nimport type { FieldComponents } from './field-components'\nimport { type RenderContext, renderField, renderFields, 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, [], dotPathOf(node) ?? '#0', (indexes) =>\n renderFields(ctx, node.children, indexes),\n )\n}\n","import { type IsFieldShown, type LayoutNode, shownNodes } from '@whatsaaaa/form-builder-schema'\nimport { type ComponentType, Fragment, type ReactElement, type ReactNode } from 'react'\nimport type { LayoutComponents, RowProps, SectionProps } from './field-components'\nimport { type RenderContext, renderField } from './render-fields'\n\n/** What `<JsonFields>` renders a layout with. */\nexport interface LayoutContext extends RenderContext {\n layoutComponents: LayoutComponents\n /** The content of the layout's slots, by name. */\n slots: Readonly<Record<string, ReactNode>>\n /** False for a field left out with `except`. */\n isShown: IsFieldShown\n}\n\n/**\n * Renders layout nodes. A field left out drops out, and so does a section or a row left without\n * fields. `indexes` are the indexes of the array items the nodes are in, outermost first.\n */\nexport function renderLayout(\n ctx: LayoutContext,\n nodes: readonly LayoutNode[],\n indexes: readonly number[],\n): ReactElement[] {\n return shownNodes(nodes, indexes, ctx.isShown).map((node) => renderNode(ctx, node, indexes))\n}\n\nfunction renderNode(\n ctx: LayoutContext,\n node: LayoutNode,\n indexes: readonly number[],\n): ReactElement {\n const { key } = node\n switch (node.type) {\n case 'field':\n return renderField(ctx, node.node, indexes, key, (inner) =>\n renderLayout(ctx, node.children, inner),\n )\n case 'section': {\n // The layout was checked for components.\n const Section = ctx.layoutComponents.section as ComponentType<SectionProps>\n return (\n <Section key={key} section={node.section}>\n {renderLayout(ctx, node.children, indexes)}\n </Section>\n )\n }\n case 'row': {\n const Row = ctx.layoutComponents.row as ComponentType<RowProps>\n return (\n <Row key={key} row={node.row} spans={node.children.map((cell) => cell.span)}>\n {(index) => renderNode(ctx, node.children[index] as LayoutNode, indexes)}\n </Row>\n )\n }\n case 'slot':\n return <Fragment key={key}>{ctx.slots[node.name]}</Fragment>\n }\n}\n","import {\n checkComponents,\n checkLayoutComponents,\n dotPathOf,\n type FieldNode,\n type LayoutNode,\n selectFields,\n} from '@whatsaaaa/form-builder-schema'\nimport { Fragment, type ReactElement, type ReactNode } from 'react'\nimport type { FieldComponents, LayoutComponents } from './field-components'\nimport { type LayoutContext, renderLayout } from './render-layout'\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 /** Defaults to the layout components `useJsonForm` got. */\n layoutComponents?: LayoutComponents | undefined\n /** The content of the layout's slots, by name: `{ submit: <button type=\"submit\">Save</button> }`. */\n slots?: Readonly<Record<string, ReactNode>> | undefined\n /** Renders only these fields, by path, in the order of the definition, without the layout. */\n only?: readonly string[] | undefined\n /**\n * Leaves out these fields, by path, e.g. the ones you place with `<JsonField>`. A section or a\n * row left without fields drops out too.\n */\n except?: readonly string[] | undefined\n /** Renders the step at this index instead of the current one, e.g. on a review page. */\n step?: number | undefined\n}\n\n/**\n * Renders the fields of a `useJsonForm` form with your components, one per field type, where the\n * definition's `layout` puts them: the current step, with its sections, rows and slots. It renders\n * nothing of its own, so put it inside Formisch's `<Form>` with your own submit button.\n */\nexport function JsonFields(props: JsonFieldsProps): ReactElement {\n const { of, only, except } = props\n const ctx: RenderContext = { of, components: resolveComponents(props.components, of) }\n const nodes = selectFields(of.fields, { only, except })\n // Without a layout, it renders the fields the way it always did, and so with `only`.\n if (only || !of.definition.layout) {\n checkComponents(nodes, ctx.components)\n return <>{renderFields(ctx, nodes, [])}</>\n }\n\n const { layout, steps } = of\n const left = new Set(except)\n const layoutCtx: LayoutContext = {\n ...ctx,\n layoutComponents: props.layoutComponents ?? of.layoutComponents ?? {},\n slots: props.slots ?? {},\n isShown: (node) => !left.has(dotPathOf(node) ?? ''),\n }\n checkComponents(\n layout.steps.flatMap((step) => fieldsOf(step.children)),\n ctx.components,\n )\n checkLayoutComponents(layout, layoutCtx.layoutComponents, layoutCtx.slots)\n\n const index = props.step ?? steps.index\n const step = steps.list[index]\n if (!step) throw new RangeError(`No step ${props.step}. The form has ${steps.list.length}.`)\n // Keyed by the step, so another step's fields don't take over this one's components.\n return <Fragment key={index}>{renderLayout(layoutCtx, step.children, [])}</Fragment>\n}\n\n/** Every field of the nodes, display elements written in the layout included. */\nfunction fieldsOf(nodes: readonly LayoutNode[]): FieldNode[] {\n return nodes.flatMap((node) => {\n if (node.type === 'slot') return []\n const inner = fieldsOf(node.children)\n return node.type === 'field' ? [node.node, ...inner] : inner\n })\n}\n","import { type FormSchema, type FormStore, useForm, type ValidationMode } from '@formisch/react'\nimport {\n createSchemaBuilder,\n type FieldNode,\n type FormLayout,\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, LayoutComponents } from './field-components'\nimport { type JsonFormSteps, useJsonFormSteps } from './json-form-steps'\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 /** Your component for each layout type the definition's `layout` uses, like `section`. */\n layoutComponents?: LayoutComponents\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 layout components from the options, if any. */\n readonly layoutComponents: LayoutComponents | undefined\n /** The definition's fields as a tree, for `<JsonFields only>` and `<JsonField>`. */\n readonly fields: readonly FieldNode[]\n /** The definition's layout, the way `<JsonFields>` renders it. */\n readonly layout: FormLayout\n /** The layout's steps: the current one, and Next, Back and Go to. A form without steps has one. */\n readonly steps: JsonFormSteps\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 and layout 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 layout: builder.getLayout(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 steps = useJsonFormSteps(form, built.layout, options)\n const { components, layoutComponents } = options\n return useMemo(\n () => ({\n form,\n definition: built.definition,\n builder: built.builder,\n components,\n layoutComponents,\n fields: built.fields,\n layout: built.layout,\n steps,\n }),\n [form, built, components, layoutComponents, steps],\n )\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whatsaaaa/form-builder-react",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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.13.0"
36
+ "@whatsaaaa/form-builder-schema": "^0.14.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@arethetypeswrong/cli": "^0.18.5",