@thomasfosterau/effect-forms 0.1.0 → 0.3.1

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
@@ -1,12 +1,16 @@
1
1
  # @thomasfosterau/effect-forms
2
2
 
3
- Framework-neutral, **Effect-native** form validation and state — schema-driven validation, nested errors, posted-value retention, status messages, and native input constraints, built on **Effect Schema** with no framework dependency. The Svelte 5 bindings live in [`@thomasfosterau/effect-forms-svelte`](../svelte).
3
+ Framework-neutral, **Effect-native** form validation and state — schema-driven validation, nested errors, posted-value retention, status messages, and native input constraints, built on **Effect Schema** with no framework dependency. The Svelte 5 bindings live in [`@thomasfosterau/effect-forms-svelte`](https://www.npmjs.com/package/@thomasfosterau/effect-forms-svelte).
4
4
 
5
- ## Why
5
+ ## Install
6
6
 
7
- The app validates everything with Effect Schema (Zod is gone). The progressive form libraries in the ecosystem are excellent, but they speak Standard Schema / Zod and bake in a framework. This package keeps the ergonomics — one schema drives validation, errors nest by field, posted values survive a failed submit, native input constraints come for free — while staying on Effect Schema and free of any framework, so the core is testable in isolation and reusable from a Worker, the CLI, or a Svelte route.
7
+ ```sh
8
+ pnpm add @thomasfosterau/effect-forms effect
9
+ ```
10
+
11
+ `effect` is a peer dependency; the package never bundles its own copy.
8
12
 
9
- ## The shape
13
+ ## Usage
10
14
 
11
15
  ```ts
12
16
  import * as Schema from "effect/Schema";
@@ -24,15 +28,17 @@ const signupForm = Form.make(Signup, {
24
28
  });
25
29
 
26
30
  // In a server action / load:
31
+ declare const request: Request;
27
32
  const state = await Effect.runPromise(signupForm.validate(request));
28
33
  // state: { id, valid, posted, data, errors, constraints, message? }
29
34
  ```
30
35
 
31
- ## Building a form incrementally
36
+ ### Building a form incrementally
32
37
 
33
38
  `Form.make` binds a hand-written `Schema.Struct`. When you'd rather compose a form field by field — reusing field groups, merging them, and attaching whole-form (cross-field) rules — reach for `FormBuilder`:
34
39
 
35
40
  ```ts
41
+ import * as Effect from "effect/Effect";
36
42
  import * as Schema from "effect/Schema";
37
43
  import { Field, Form, FormBuilder } from "@thomasfosterau/effect-forms";
38
44
 
@@ -47,6 +53,7 @@ const signup = FormBuilder.empty
47
53
 
48
54
  // Bind it exactly like a schema (defaults are derived when omitted):
49
55
  const form = Form.fromBuilder(signup, { id: "signup" });
56
+ declare const request: Request; // the incoming submission
50
57
  const state = await Effect.runPromise(form.validate(request));
51
58
  ```
52
59
 
@@ -54,47 +61,36 @@ const state = await Effect.runPromise(form.validate(request));
54
61
 
55
62
  Modules are exposed as Effect-style namespaces (`Form`, `State`, `Result`, `Errors`, …). `Form.make(schema, { defaults })` is the server-side validate-and-seed entry point:
56
63
 
57
- - **`validate(input)`** accepts a `FormData`, `URLSearchParams`, `Request`, or a plain object. It parses the body (nested `author.name` / `tags[0]` / `tags[]` notation), decodes it through the schema collecting **every** field error (`errors: "all"`), and returns a serialisable [`FormState`](src/State.ts) — valid with the decoded data, or invalid with the nested [`FormErrors`](src/Errors.ts) tree and the **posted values preserved** so the form re-renders what the user typed. It is an `Effect`, so a schema that requires decoding services composes naturally; it never fails on the error channel (validation failures live in the returned state).
64
+ - **`validate(input)`** accepts a `FormData`, `URLSearchParams`, `Request`, or a plain object. It parses the body (nested `author.name` / `tags[0]` / `tags[]` notation), decodes it through the schema collecting **every** field error (`errors: "all"`), and returns a serialisable [`FormState`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/State.ts) — valid with the decoded data, or invalid with the nested [`FormErrors`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/Errors.ts) tree and the **posted values preserved** so the form re-renders what the user typed. It is an `Effect`, so a schema that requires decoding services composes naturally; it never fails on the error channel (validation failures live in the returned state).
58
65
  - **`empty()`** seeds a blank, unposted form from the defaults — what a `load` returns to render a create form.
59
66
 
60
- ## What's in the box
61
-
62
- | Module | Purpose |
63
- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
64
- | [`Form`](src/Form.ts) | `make` — bind a schema to a form (validate + empty); `fromBuilder` — bind a `FormBuilder`. |
65
- | [`FormBuilder`](src/FormBuilder.ts) | Compose a form field by field: `addField` / `merge` / `refine` / `refineEffect` / `buildSchema`. |
66
- | [`Field`](src/Field.ts) | `makeField` / `makeArrayField` field defs + guards, and blank-default derivation from a schema — `defaultValue` / `defaultEncodedValues` for a builder field, `schemaAt` / `defaultItemAt` for any nested path. |
67
- | [`State`](src/State.ts) | `FormState`, `makeFormState`, `setMessage`, `withError`, `FormStateSchema` (transport). |
68
- | [`Errors`](src/Errors.ts) | The nested `FormErrors` tree + `issuesToErrors` / `flattenErrors` / `getError` / `setError` / `mergeErrors` / `remapIndices`; `FieldValidators` per-source error tracking via `replaceAt` / `setSourceAt` / `clearSourceAt` / `mergeSourced`. |
69
- | [`Issues`](src/Issues.ts) | `FormIssue` + the Effect Schema issue-tree walker. |
70
- | [`FormData`](src/FormData.ts) | `formDataToObject` / `parseFieldPath` — structured parsing of an HTML body. |
71
- | [`Path`](src/Path.ts) | `getAt` / `setAt` (immutable) along a `FormPath`, `toString` / `fromString` notation conversion, the type-level `FieldPath<T>` / `PathValue<T, P>` deep-key projections a typed field handle is built on, and the `IndexMap` constructors (`removeAt` / `insertAt` / `swapAt` / `moveAt` / `replaceAt` / `clearAll`) + `remapIndex` / `remapKeyedRecord` for keeping per-index state in sync with an array mutation. |
72
- | [`Coerce`](src/Coerce.ts) | Tri-state (value / `null` / `undefined`) coercers for the form boundary. |
73
- | [`Constraints`](src/Constraints.ts) | `constraintsFromSchema` — native input attributes (`required`, `minlength`/`maxlength`, `min`/`max`, `pattern`, `step`) derived from the schema AST, including into array items; `constraintAt` reads one field's leaf out of the derived tree. |
74
- | [`Mode`](src/Mode.ts) | `FormMode` + `parse` — when a form validates (`onSubmit` / `onBlur` / `onChange`), whether it auto-submits, and when a posted form revalidates (`revalidate`). |
75
- | [`Result`](src/Result.ts) | `FormActionResult` — the framework-neutral submission outcome (success / failure / redirect / error). |
76
- | [`Validate`](src/Validate.ts) | `validateValue` / `validateValueSync` — ad-hoc value/field validation; `FieldValidators` + `runSyncFieldValidator` / `runAsyncFieldValidator` — per-field sync/async validators (debounced, fiber-cancellable). |
67
+ ## API
68
+
69
+ | Module | Purpose |
70
+ | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
71
+ | [`Form`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/Form.ts) | `make` — bind a schema to a form (validate + empty); `fromBuilder` — bind a `FormBuilder`. |
72
+ | [`FormBuilder`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/FormBuilder.ts) | Compose a form field by field: `addField` / `merge` / `refine` / `refineEffect` / `buildSchema`. |
73
+ | [`Field`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/Field.ts) | `makeField` / `makeArrayField` field defs + guards, and blank-default derivation from a schema — `defaultValue` / `defaultEncodedValues` for a builder field, `schemaAt` / `defaultItemAt` for any nested path. |
74
+ | [`State`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/State.ts) | `FormState`, `make`, `withMessage`, `withError`, `isValid`, `FormStateSchema` (transport). |
75
+ | [`Errors`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/Errors.ts) | The nested `FormErrors` tree + `issuesToErrors` / `flattenErrors` / `getError` / `setError` / `mergeErrors` / `remapIndices`; `SourcedErrors` per-source tracking via `replaceAt` / `setSourceAt` / `clearSourceAt` / `mergeSourced`. |
76
+ | [`Issues`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/Issues.ts) | `FormIssue` + the Effect Schema issue-tree walker. |
77
+ | [`FormData`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/FormData.ts) | `formDataToObject` / `parseFieldPath` — structured parsing of an HTML body. |
78
+ | [`Path`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/Path.ts) | `getAt` / `setAt` (immutable) along a `FormPath`, `toString` / `fromString` notation conversion, the type-level `FieldPath<T>` / `PathValue<T, P>` deep-key projections a typed field handle is built on, and the `IndexMap` constructors (`removeAt` / `insertAt` / `swapAt` / `moveAt` / `replaceAt` / `clearAll`) + `remapIndex` / `remapKeyedRecord` for keeping per-index state in sync with an array mutation. |
79
+ | [`Coerce`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/Coerce.ts) | Tri-state (value / `null` / `undefined`) coercers for the form boundary. |
80
+ | [`Command`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/Command.ts) | Derive form-input schemas, blank defaults and tri-state create / update adapters from a command-struct pair: `plan`, `formInputFields`, `blankDefaults`, `createAdapter`, `updateAdapter`, `jsonRowsFromField`. |
81
+ | [`Constraints`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/Constraints.ts) | `constraintsFromSchema` — native input attributes (`required`, `minlength`/`maxlength`, `min`/`max`, `pattern`, `step`) derived from the schema AST, including into array items; `constraintAt` reads one field's leaf out of the derived tree. |
82
+ | [`Mode`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/Mode.ts) | `FormMode` + `parse` — when a form validates (`onSubmit` / `onBlur` / `onChange`), whether it auto-submits, and when a posted form revalidates (`revalidate`). |
83
+ | [`Result`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/Result.ts) | `FormActionResult` — the framework-neutral submission outcome (success / failure / redirect / error). |
84
+ | [`Validate`](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/src/Validate.ts) | `validateValue` / `validateValueSync` — ad-hoc value/field validation; `FieldValidators` + `runSyncFieldValidator` / `runAsyncFieldValidator` — per-field sync/async validators (debounced, fiber-cancellable). |
77
85
 
78
86
  ## Notes
79
87
 
80
88
  - **Coercion is the schema's job.** An HTML body is all strings; build the schema from coercing leaves (`Schema.FiniteFromString`, `Schema.BooleanFromString`, the `Coerce` helpers) so the parsed object decodes cleanly. The parser never guesses types.
81
89
  - **Errors nest by field.** Leaf fields are `string[]`; nested objects / array indices nest; a branch's own (cross-field) message lives under `_errors`. The tree is plain JSON — it crosses the SSR boundary unchanged.
82
- - **Update semantics.** The `Coerce` helpers honour the value / `null` / `undefined` contract (set / clear / carry-forward) the app uses for partial updates.
83
- - **Constraint derivation reads schema checks, not just optionality.** `constraintsFromSchema` walks each property's `.check()`s for the `meta` annotation Effect Schema's built-in checks carry (`isMinLength`, `isLengthBetween`, `isPattern`, `isGreaterThanOrEqualTo`, `isBetween`, `isMultipleOf`, `isInt`, …), unwraps `optional` / `NullOr` unions, and descends into struct and array-item shapes. A check with no recognised `meta` tag (a bare `Schema.filter` predicate) derives nothing — there's no attribute to guess at. Anything the walk can't (or shouldn't) infer goes through the `overrides` argument, which always wins on conflict.
90
+ - **Update semantics.** The `Coerce` helpers honour the value / `null` / `undefined` contract (set / clear / carry-forward) for partial updates.
91
+ - **Constraint derivation reads schema checks, not just optionality.** `constraintsFromSchema` walks each property's `.check()`s for the `meta` annotation Effect Schema's built-in checks carry (`isMinLength`, `isBetweenLength`, `isPattern`, `isGreaterThanOrEqualTo`, `isBetween`, `isMultipleOf`, `isInt`, …), unwraps `optional` / `NullOr` unions, and descends into struct and array-item shapes. A check with no recognised `meta` tag (a bare `Schema.filter` predicate) derives nothing — there's no attribute to guess at. Anything the walk can't (or shouldn't) infer goes through the `overrides` argument, which always wins on conflict.
84
92
  - **Errors have a source.** `Errors.SourcedErrors` keeps a `client` tree (browser-side validation) parallel to a `server` tree (the last submission), merged for display by `mergeSourced`. `Errors.replaceAt` / `setSourceAt` / `clearSourceAt` update one path in one source without disturbing the rest — what a reactive store needs so a server-reported failure survives a client revalidation of some _other_ field, but clears once its own field changes.
85
93
 
86
- ## File naming
87
-
88
- Source modules use PascalCase file names, matching Effect's own convention (`effect/Schema`, `effect/SchemaParser`). The package has a single entry point: every module is re-exported from the `.` barrel, so there are no subpaths to import from.
89
-
90
- ## Development
91
-
92
- From the repository root:
93
-
94
- ```sh
95
- vp run -r build # build every package with tsdown
96
- vp check # format (Oxfmt), lint (Oxlint), type-check (tsgolint)
97
- vp run -r test # unit tests
98
- ```
94
+ ## License
99
95
 
100
- Or from this directory: `vp test run` / `vp test run --coverage`.
96
+ [MIT](https://github.com/thomasfosterau/effect-forms/blob/trunk/packages/forms/LICENSE)
package/dist/index.d.ts CHANGED
@@ -7,8 +7,6 @@ declare namespace Coerce_d_exports {
7
7
  export { booleanFromCheckbox, emptyToNull, emptyToUndefined, numberFromField, trimToNull, trimToUndefined };
8
8
  }
9
9
  /**
10
- * @copyright Thomas Foster
11
- *
12
10
  * Tri-state coercers for the HTML-form boundary.
13
11
  *
14
12
  * An HTML form submits every value as a string, and an absent optional field
@@ -22,6 +20,7 @@ declare namespace Coerce_d_exports {
22
20
  * framework's `error()` helper (validation failures belong on the schema, not
23
21
  * in a coercer).
24
22
  *
23
+ * @copyright Thomas Foster
25
24
  * @module effect-forms/Coerce
26
25
  */
27
26
  /** Empty string / `undefined` → `undefined`; otherwise the value (skip semantics). */
@@ -143,7 +142,7 @@ declare const createAdapter: (commandPlan: CommandPlan, overrides?: AdaptOverrid
143
142
  */
144
143
  declare const updateAdapter: (commandPlan: CommandPlan, overrides?: AdaptOverrides) => (input: FormInputRecord) => Record<string, unknown>;
145
144
  declare namespace Issues_d_exports {
146
- export { FormIssue, FormPath, LeafFormatter, defaultLeafFormatter, filterOutputToFormIssues, schemaIssuesToFormIssues };
145
+ export { FormIssue, FormPath, LeafFormatter, defaultLeafFormatter, filterOutputToFormIssues$1 as filterOutputToFormIssues, schemaIssuesToFormIssues };
147
146
  }
148
147
  /**
149
148
  * A path into the form's data. String segments address object keys; number
@@ -157,13 +156,18 @@ interface FormIssue {
157
156
  readonly message: string;
158
157
  }
159
158
  /**
160
- * Render one terminal issue leaf into a human-readable message. The default
161
- * defers to the issue's own `toString`, which Effect Schema renders into a
162
- * readable sentence (e.g. `Expected string, got 1`). Override it to localise
163
- * or to map specific leaf `_tag`s to custom copy.
159
+ * Render one terminal issue into a human-readable message. The default is
160
+ * Effect Schema's own formatter (`SchemaIssue.makeFormatterDefault`), which
161
+ * renders a sentence such as `Expected string`. (An issue's `toString` is not
162
+ * a readable rendering in Effect 4.0.0: it prints `[object Object]`.)
163
+ * Override it to localise or to map specific `_tag`s to custom copy.
164
+ *
165
+ * A failed schema check reaches the formatter as the `Filter` node that
166
+ * wraps its leaf, because the check's own message (e.g. `Expected a value
167
+ * with a length of at least 2`) lives on the `Filter`, not on the leaf.
164
168
  */
165
169
  type LeafFormatter = (issue: SchemaIssue.Issue) => string;
166
- /** The default leaf formatter — Effect Schema's own rendering of the leaf. */
170
+ /** The default leaf formatter — Effect Schema's own rendering of the issue. */
167
171
  declare const defaultLeafFormatter: LeafFormatter;
168
172
  /**
169
173
  * Walk an Effect Schema issue tree, emitting one {@link FormIssue} per leaf
@@ -180,7 +184,7 @@ declare function schemaIssuesToFormIssues(issue: SchemaIssue.Issue, format?: Lea
180
184
  * ({@link module:effect-forms/Validate}) so both fold into the same nested
181
185
  * error tree as schema-decode failures.
182
186
  */
183
- declare function filterOutputToFormIssues(output: Schema.FilterOutput): FormIssue[];
187
+ declare function filterOutputToFormIssues$1(output: Schema.FilterOutput): FormIssue[];
184
188
  declare namespace Constraints_d_exports {
185
189
  export { InputConstraint, InputConstraints, constraintAt, constraintsFromSchema, isLeafConstraint };
186
190
  }
@@ -204,6 +208,19 @@ declare const isLeafConstraint: (value: InputConstraint | InputConstraints) => v
204
208
  * Derive the {@link InputConstraints} for a struct schema. Non-struct schemas
205
209
  * yield an empty tree. `overrides` (deep-merged over the derived tree, and
206
210
  * winning on conflict) is the seam for anything the AST walk does not infer.
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * import * as Schema from "effect/Schema"
215
+ * import { Constraints } from "@thomasfosterau/effect-forms"
216
+ *
217
+ * const Signup = Schema.Struct({
218
+ * name: Schema.String.check(Schema.isMinLength(2)),
219
+ * nickname: Schema.optional(Schema.String),
220
+ * })
221
+ * console.log(Constraints.constraintsFromSchema(Signup))
222
+ * // { name: { required: true, minlength: 2 }, nickname: { required: false } }
223
+ * ```
207
224
  */
208
225
  declare function constraintsFromSchema(schema: Schema.Top, overrides?: InputConstraints): InputConstraints;
209
226
  /**
@@ -230,6 +247,16 @@ declare function getAt(obj: unknown, path: FormPath): unknown;
230
247
  * arrays along the path are cloned (structural sharing elsewhere); missing
231
248
  * containers are created as arrays for numeric segments and objects otherwise.
232
249
  * The empty path returns `value`. Pure — `obj` is never mutated.
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * import { Path } from "@thomasfosterau/effect-forms"
254
+ *
255
+ * const before = { authors: [{ name: "Ada" }] }
256
+ * const after = Path.setAt(before, ["authors", 0, "name"], "Grace")
257
+ * console.log(Path.getAt(after, ["authors", 0, "name"])) // "Grace"
258
+ * console.log(before.authors[0]?.name) // "Ada" — the input is untouched
259
+ * ```
233
260
  */
234
261
  declare function setAt<T>(obj: T, path: FormPath, value: unknown): T;
235
262
  /** Whether `path` is `root` or nested beneath it. */
@@ -521,6 +548,14 @@ declare const empty: FormBuilder<Record<never, never>, never>;
521
548
  declare function buildSchema<TFields extends FieldsRecord, R>(self: FormBuilder<TFields, R>): Schema.Codec<DecodedFromFields<TFields>, EncodedFromFields<TFields>, R>;
522
549
  /** The asynchronous refinements carried on a builder, as bare predicates. */
523
550
  declare function asyncRefinements<TFields extends FieldsRecord, R>(self: FormBuilder<TFields, R>): ReadonlyArray<(values: DecodedFromFields<TFields>) => Effect.Effect<Schema.FilterOutput, never, R>>;
551
+ /**
552
+ * Normalise a filter predicate's `Schema.FilterOutput` into a flat list of
553
+ * {@link FormIssue}s (empty on success).
554
+ *
555
+ * @deprecated Use `Issues.filterOutputToFormIssues`; this re-export remains
556
+ * only for callers that imported it from `FormBuilder`.
557
+ */
558
+ declare const filterOutputToFormIssues: typeof filterOutputToFormIssues$1;
524
559
  /**
525
560
  * Run every asynchronous refinement on `self` against `values` and collect
526
561
  * their failures as {@link FormIssue}s (empty when all pass). Runs them
@@ -555,11 +590,17 @@ interface FormState<A, Message = unknown> {
555
590
  }
556
591
  /** Options for {@link make}. */
557
592
  interface MakeFormStateOptions<Message> {
593
+ /** The form's id, distinguishing several forms on one page. Defaults to `"default"`. */
558
594
  readonly id?: string;
595
+ /** Whether the state is valid. Defaults to `true` exactly when `errors` is empty. */
559
596
  readonly valid?: boolean;
597
+ /** Whether the state comes from a submission. Defaults to `false`. */
560
598
  readonly posted?: boolean;
599
+ /** The nested error tree. Defaults to no errors. */
561
600
  readonly errors?: FormErrors;
601
+ /** A status message to render with the form. Omitted by default. */
562
602
  readonly message?: Message;
603
+ /** Native input constraints. Omitted by default. */
563
604
  readonly constraints?: InputConstraints;
564
605
  }
565
606
  /** Build a {@link FormState} from data plus overrides. `valid` defaults to "no errors". */
@@ -618,8 +659,27 @@ interface Form<S extends Schema.Top, Message = unknown> {
618
659
  validate(input: FormInput): Effect.Effect<FormState<S["Type"], Message>, never, S["DecodingServices"]>;
619
660
  }
620
661
  /**
621
- * Bind `schema` to a form. See the module docs for the contract. The returned
622
- * {@link Form} is stateless and reusable across requests.
662
+ * Bind `schema` to a form: `validate(input)` decodes a submission into a
663
+ * {@link FormState} (it never fails — validation failures live in the state;
664
+ * its requirements are the schema's decoding services) and `empty()` seeds a
665
+ * blank form from `defaults`. The returned {@link Form} is stateless and
666
+ * reusable across requests.
667
+ *
668
+ * @example
669
+ * ```ts
670
+ * import * as Effect from "effect/Effect"
671
+ * import * as Schema from "effect/Schema"
672
+ * import { Form } from "@thomasfosterau/effect-forms"
673
+ *
674
+ * const signup = Form.make(
675
+ * Schema.Struct({ name: Schema.String.check(Schema.isMinLength(2)) }),
676
+ * { id: "signup", defaults: { name: "" } },
677
+ * )
678
+ *
679
+ * const state = Effect.runSync(signup.validate(new URLSearchParams({ name: "A" })))
680
+ * console.log(state.valid) // false
681
+ * console.log(state.data) // { name: "A" } — the posted value, kept for re-render
682
+ * ```
623
683
  */
624
684
  declare function make<S extends Schema.Top, Message = unknown>(schema: S, options: MakeFormOptions<S>): Form<S, Message>;
625
685
  /** Options for {@link fromBuilder}. Like {@link MakeFormOptions} but `defaults` is optional. */
@@ -647,8 +707,6 @@ declare namespace FormData_d_exports {
647
707
  export { FormBodySource, formDataToObject, parseFieldPath, requestToObject };
648
708
  }
649
709
  /**
650
- * @copyright Thomas Foster
651
- *
652
710
  * Structured parsing of an HTML form body into a plain object the schema can
653
711
  * decode.
654
712
  *
@@ -664,6 +722,7 @@ declare namespace FormData_d_exports {
664
722
  * `File` values pass through untouched; repeated bare keys collapse into an
665
723
  * array. The parser is total and never throws.
666
724
  *
725
+ * @copyright Thomas Foster
667
726
  * @module effect-forms/FormData
668
727
  */
669
728
  /** Anything that can be parsed into a structured form object. */
@@ -680,6 +739,19 @@ declare function parseFieldPath(name: string): string[];
680
739
  * bracket path notation (see {@link parseFieldPath}); repeated bare keys
681
740
  * become arrays; `File` values pass through. The result is ready to hand to
682
741
  * `Schema.decodeUnknown` with a schema whose leaves coerce the string inputs.
742
+ *
743
+ * @example
744
+ * ```ts
745
+ * import { FormData } from "@thomasfosterau/effect-forms"
746
+ *
747
+ * const body = new URLSearchParams([
748
+ * ["author.name", "Ada"],
749
+ * ["tags[]", "math"],
750
+ * ["tags[]", "engines"],
751
+ * ])
752
+ * console.log(FormData.formDataToObject(body))
753
+ * // { author: { name: "Ada" }, tags: ["math", "engines"] }
754
+ * ```
683
755
  */
684
756
  declare function formDataToObject(source: FormBodySource): Record<string, unknown>;
685
757
  /** Read and parse a `Request`'s form body. Errors (e.g. wrong content-type) reject. */