@thomasfosterau/effect-forms 0.0.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 +19 -23
- package/dist/index.d.ts +34 -13
- package/dist/index.js +56 -33
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
|
|
3
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).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
|
@@ -57,19 +64,20 @@ Modules are exposed as Effect-style namespaces (`Form`, `State`, `Result`, `Erro
|
|
|
57
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`](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).
|
|
58
65
|
- **`empty()`** seeds a blank, unposted form from the defaults — what a `load` returns to render a create form.
|
|
59
66
|
|
|
60
|
-
##
|
|
67
|
+
## API
|
|
61
68
|
|
|
62
69
|
| Module | Purpose |
|
|
63
70
|
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
64
71
|
| [`Form`](src/Form.ts) | `make` — bind a schema to a form (validate + empty); `fromBuilder` — bind a `FormBuilder`. |
|
|
65
72
|
| [`FormBuilder`](src/FormBuilder.ts) | Compose a form field by field: `addField` / `merge` / `refine` / `refineEffect` / `buildSchema`. |
|
|
66
73
|
| [`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`, `
|
|
68
|
-
| [`Errors`](src/Errors.ts) | The nested `FormErrors` tree + `issuesToErrors` / `flattenErrors` / `getError` / `setError` / `mergeErrors` / `remapIndices`; `
|
|
74
|
+
| [`State`](src/State.ts) | `FormState`, `make`, `withMessage`, `withError`, `isValid`, `FormStateSchema` (transport). |
|
|
75
|
+
| [`Errors`](src/Errors.ts) | The nested `FormErrors` tree + `issuesToErrors` / `flattenErrors` / `getError` / `setError` / `mergeErrors` / `remapIndices`; `SourcedErrors` per-source tracking via `replaceAt` / `setSourceAt` / `clearSourceAt` / `mergeSourced`. |
|
|
69
76
|
| [`Issues`](src/Issues.ts) | `FormIssue` + the Effect Schema issue-tree walker. |
|
|
70
77
|
| [`FormData`](src/FormData.ts) | `formDataToObject` / `parseFieldPath` — structured parsing of an HTML body. |
|
|
71
78
|
| [`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
79
|
| [`Coerce`](src/Coerce.ts) | Tri-state (value / `null` / `undefined`) coercers for the form boundary. |
|
|
80
|
+
| [`Command`](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`. |
|
|
73
81
|
| [`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
82
|
| [`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
83
|
| [`Result`](src/Result.ts) | `FormActionResult` — the framework-neutral submission outcome (success / failure / redirect / error). |
|
|
@@ -79,22 +87,10 @@ Modules are exposed as Effect-style namespaces (`Form`, `State`, `Result`, `Erro
|
|
|
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)
|
|
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`, `
|
|
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
|
-
##
|
|
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
|
-
|
|
96
|
+
[MIT](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,19 @@ interface FormIssue {
|
|
|
157
156
|
readonly message: string;
|
|
158
157
|
}
|
|
159
158
|
/**
|
|
160
|
-
* Render one terminal issue
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
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 no
|
|
162
|
+
* longer a readable rendering as of Effect rc.118: it prints
|
|
163
|
+
* `[object Object]`.) Override it to localise or to map specific `_tag`s to
|
|
164
|
+
* custom copy.
|
|
165
|
+
*
|
|
166
|
+
* A failed schema check reaches the formatter as the `Filter` node that
|
|
167
|
+
* wraps its leaf, because the check's own message (e.g. `Expected a value
|
|
168
|
+
* with a length of at least 2`) lives on the `Filter`, not on the leaf.
|
|
164
169
|
*/
|
|
165
170
|
type LeafFormatter = (issue: SchemaIssue.Issue) => string;
|
|
166
|
-
/** The default leaf formatter — Effect Schema's own rendering of the
|
|
171
|
+
/** The default leaf formatter — Effect Schema's own rendering of the issue. */
|
|
167
172
|
declare const defaultLeafFormatter: LeafFormatter;
|
|
168
173
|
/**
|
|
169
174
|
* Walk an Effect Schema issue tree, emitting one {@link FormIssue} per leaf
|
|
@@ -180,7 +185,7 @@ declare function schemaIssuesToFormIssues(issue: SchemaIssue.Issue, format?: Lea
|
|
|
180
185
|
* ({@link module:effect-forms/Validate}) so both fold into the same nested
|
|
181
186
|
* error tree as schema-decode failures.
|
|
182
187
|
*/
|
|
183
|
-
declare function filterOutputToFormIssues(output: Schema.FilterOutput): FormIssue[];
|
|
188
|
+
declare function filterOutputToFormIssues$1(output: Schema.FilterOutput): FormIssue[];
|
|
184
189
|
declare namespace Constraints_d_exports {
|
|
185
190
|
export { InputConstraint, InputConstraints, constraintAt, constraintsFromSchema, isLeafConstraint };
|
|
186
191
|
}
|
|
@@ -521,6 +526,14 @@ declare const empty: FormBuilder<Record<never, never>, never>;
|
|
|
521
526
|
declare function buildSchema<TFields extends FieldsRecord, R>(self: FormBuilder<TFields, R>): Schema.Codec<DecodedFromFields<TFields>, EncodedFromFields<TFields>, R>;
|
|
522
527
|
/** The asynchronous refinements carried on a builder, as bare predicates. */
|
|
523
528
|
declare function asyncRefinements<TFields extends FieldsRecord, R>(self: FormBuilder<TFields, R>): ReadonlyArray<(values: DecodedFromFields<TFields>) => Effect.Effect<Schema.FilterOutput, never, R>>;
|
|
529
|
+
/**
|
|
530
|
+
* Normalise a filter predicate's `Schema.FilterOutput` into a flat list of
|
|
531
|
+
* {@link FormIssue}s (empty on success).
|
|
532
|
+
*
|
|
533
|
+
* @deprecated Use `Issues.filterOutputToFormIssues`; this re-export remains
|
|
534
|
+
* only for callers that imported it from `FormBuilder`.
|
|
535
|
+
*/
|
|
536
|
+
declare const filterOutputToFormIssues: typeof filterOutputToFormIssues$1;
|
|
524
537
|
/**
|
|
525
538
|
* Run every asynchronous refinement on `self` against `values` and collect
|
|
526
539
|
* their failures as {@link FormIssue}s (empty when all pass). Runs them
|
|
@@ -555,11 +568,17 @@ interface FormState<A, Message = unknown> {
|
|
|
555
568
|
}
|
|
556
569
|
/** Options for {@link make}. */
|
|
557
570
|
interface MakeFormStateOptions<Message> {
|
|
571
|
+
/** The form's id, distinguishing several forms on one page. Defaults to `"default"`. */
|
|
558
572
|
readonly id?: string;
|
|
573
|
+
/** Whether the state is valid. Defaults to `true` exactly when `errors` is empty. */
|
|
559
574
|
readonly valid?: boolean;
|
|
575
|
+
/** Whether the state comes from a submission. Defaults to `false`. */
|
|
560
576
|
readonly posted?: boolean;
|
|
577
|
+
/** The nested error tree. Defaults to no errors. */
|
|
561
578
|
readonly errors?: FormErrors;
|
|
579
|
+
/** A status message to render with the form. Omitted by default. */
|
|
562
580
|
readonly message?: Message;
|
|
581
|
+
/** Native input constraints. Omitted by default. */
|
|
563
582
|
readonly constraints?: InputConstraints;
|
|
564
583
|
}
|
|
565
584
|
/** Build a {@link FormState} from data plus overrides. `valid` defaults to "no errors". */
|
|
@@ -618,8 +637,11 @@ interface Form<S extends Schema.Top, Message = unknown> {
|
|
|
618
637
|
validate(input: FormInput): Effect.Effect<FormState<S["Type"], Message>, never, S["DecodingServices"]>;
|
|
619
638
|
}
|
|
620
639
|
/**
|
|
621
|
-
* Bind `schema` to a form
|
|
622
|
-
* {@link
|
|
640
|
+
* Bind `schema` to a form: `validate(input)` decodes a submission into a
|
|
641
|
+
* {@link FormState} (it never fails — validation failures live in the state;
|
|
642
|
+
* its requirements are the schema's decoding services) and `empty()` seeds a
|
|
643
|
+
* blank form from `defaults`. The returned {@link Form} is stateless and
|
|
644
|
+
* reusable across requests.
|
|
623
645
|
*/
|
|
624
646
|
declare function make<S extends Schema.Top, Message = unknown>(schema: S, options: MakeFormOptions<S>): Form<S, Message>;
|
|
625
647
|
/** Options for {@link fromBuilder}. Like {@link MakeFormOptions} but `defaults` is optional. */
|
|
@@ -647,8 +669,6 @@ declare namespace FormData_d_exports {
|
|
|
647
669
|
export { FormBodySource, formDataToObject, parseFieldPath, requestToObject };
|
|
648
670
|
}
|
|
649
671
|
/**
|
|
650
|
-
* @copyright Thomas Foster
|
|
651
|
-
*
|
|
652
672
|
* Structured parsing of an HTML form body into a plain object the schema can
|
|
653
673
|
* decode.
|
|
654
674
|
*
|
|
@@ -664,6 +684,7 @@ declare namespace FormData_d_exports {
|
|
|
664
684
|
* `File` values pass through untouched; repeated bare keys collapse into an
|
|
665
685
|
* array. The parser is total and never throws.
|
|
666
686
|
*
|
|
687
|
+
* @copyright Thomas Foster
|
|
667
688
|
* @module effect-forms/FormData
|
|
668
689
|
*/
|
|
669
690
|
/** Anything that can be parsed into a structured form object. */
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as Schema from "effect/Schema";
|
|
|
3
3
|
import * as SchemaAST from "effect/SchemaAST";
|
|
4
4
|
import * as Effect from "effect/Effect";
|
|
5
5
|
import * as SchemaParser from "effect/SchemaParser";
|
|
6
|
-
import "effect/SchemaIssue";
|
|
6
|
+
import * as SchemaIssue from "effect/SchemaIssue";
|
|
7
7
|
import * as Duration from "effect/Duration";
|
|
8
8
|
//#region src/Coerce.ts
|
|
9
9
|
var Coerce_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -15,8 +15,6 @@ var Coerce_exports = /* @__PURE__ */ __exportAll({
|
|
|
15
15
|
trimToUndefined: () => trimToUndefined
|
|
16
16
|
});
|
|
17
17
|
/**
|
|
18
|
-
* @copyright Thomas Foster
|
|
19
|
-
*
|
|
20
18
|
* Tri-state coercers for the HTML-form boundary.
|
|
21
19
|
*
|
|
22
20
|
* An HTML form submits every value as a string, and an absent optional field
|
|
@@ -30,6 +28,7 @@ var Coerce_exports = /* @__PURE__ */ __exportAll({
|
|
|
30
28
|
* framework's `error()` helper (validation failures belong on the schema, not
|
|
31
29
|
* in a coercer).
|
|
32
30
|
*
|
|
31
|
+
* @copyright Thomas Foster
|
|
33
32
|
* @module effect-forms/Coerce
|
|
34
33
|
*/
|
|
35
34
|
/** Empty string / `undefined` → `undefined`; otherwise the value (skip semantics). */
|
|
@@ -73,8 +72,6 @@ function booleanFromCheckbox(raw) {
|
|
|
73
72
|
//#endregion
|
|
74
73
|
//#region src/Command.ts
|
|
75
74
|
/**
|
|
76
|
-
* @copyright Thomas Foster
|
|
77
|
-
*
|
|
78
75
|
* Command-struct introspection and form derivation.
|
|
79
76
|
*
|
|
80
77
|
* A host app's write surface is a pair of flat command `Schema.Struct`s per
|
|
@@ -102,6 +99,7 @@ function booleanFromCheckbox(raw) {
|
|
|
102
99
|
* (a create-time default, an asymmetric coercion) stay expressible via the
|
|
103
100
|
* per-field {@link AdaptOverrides} escape hatch instead of forcing ejection.
|
|
104
101
|
*
|
|
102
|
+
* @copyright Thomas Foster
|
|
105
103
|
* @module effect-forms/Command
|
|
106
104
|
*/
|
|
107
105
|
var Command_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -441,7 +439,7 @@ function flattenChecks(checks, out) {
|
|
|
441
439
|
const payloadPatches = {
|
|
442
440
|
"effect/schema/isMinLength": (p) => ({ minlength: p.minLength }),
|
|
443
441
|
"effect/schema/isMaxLength": (p) => ({ maxlength: p.maxLength }),
|
|
444
|
-
"effect/schema/
|
|
442
|
+
"effect/schema/isBetweenLength": (p) => ({
|
|
445
443
|
minlength: p.minimum,
|
|
446
444
|
maxlength: p.maximum
|
|
447
445
|
}),
|
|
@@ -621,9 +619,10 @@ const isIndexToken = (token) => token === "" || /^\d+$/.test(token);
|
|
|
621
619
|
/** Create the container a token should live in: array for index/append tokens. */
|
|
622
620
|
const makeContainer = (nextToken) => isIndexToken(nextToken) ? [] : {};
|
|
623
621
|
const setOnContainer = (container, token, value) => {
|
|
624
|
-
if (Array.isArray(container))
|
|
625
|
-
|
|
626
|
-
|
|
622
|
+
if (Array.isArray(container)) {
|
|
623
|
+
if (token === "") container.push(value);
|
|
624
|
+
else container[Number(token)] = value;
|
|
625
|
+
} else container[token] = value;
|
|
627
626
|
};
|
|
628
627
|
const getFromContainer = (container, token) => Array.isArray(container) ? container[Number(token)] : container[token];
|
|
629
628
|
/** Assign `value` deep into `root` along `tokens`, creating containers as needed. */
|
|
@@ -681,8 +680,6 @@ async function requestToObject(request) {
|
|
|
681
680
|
//#endregion
|
|
682
681
|
//#region src/Path.ts
|
|
683
682
|
/**
|
|
684
|
-
* @copyright Thomas Foster
|
|
685
|
-
*
|
|
686
683
|
* Nested get / set along a {@link FormPath}.
|
|
687
684
|
*
|
|
688
685
|
* A form's data is a tree, and the reactive bindings address it by path:
|
|
@@ -700,6 +697,7 @@ async function requestToObject(request) {
|
|
|
700
697
|
* convert to and from the flat `author.name` / `tags[0]` notation the HTML
|
|
701
698
|
* body parser ({@link module:effect-forms/FormData}) uses.
|
|
702
699
|
*
|
|
700
|
+
* @copyright Thomas Foster
|
|
703
701
|
* @module effect-forms/Path
|
|
704
702
|
*/
|
|
705
703
|
var Path_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -1170,11 +1168,11 @@ function defaultEncodedValues(fields) {
|
|
|
1170
1168
|
//#region src/Issues.ts
|
|
1171
1169
|
var Issues_exports = /* @__PURE__ */ __exportAll({
|
|
1172
1170
|
defaultLeafFormatter: () => defaultLeafFormatter,
|
|
1173
|
-
filterOutputToFormIssues: () => filterOutputToFormIssues,
|
|
1171
|
+
filterOutputToFormIssues: () => filterOutputToFormIssues$1,
|
|
1174
1172
|
schemaIssuesToFormIssues: () => schemaIssuesToFormIssues
|
|
1175
1173
|
});
|
|
1176
|
-
/** The default leaf formatter — Effect Schema's own rendering of the
|
|
1177
|
-
const defaultLeafFormatter = (
|
|
1174
|
+
/** The default leaf formatter — Effect Schema's own rendering of the issue. */
|
|
1175
|
+
const defaultLeafFormatter = SchemaIssue.makeFormatterDefault();
|
|
1178
1176
|
/**
|
|
1179
1177
|
* Normalise a `Pointer.path` segment into a {@link FormPath} segment: integer
|
|
1180
1178
|
* keys become numbers (array indices), everything else becomes a string.
|
|
@@ -1184,6 +1182,17 @@ const normaliseSegment = (segment) => {
|
|
|
1184
1182
|
if (typeof segment === "symbol") return segment.toString();
|
|
1185
1183
|
return /^\d+$/.test(segment) ? Number(segment) : segment;
|
|
1186
1184
|
};
|
|
1185
|
+
/** Whether `issue` is a terminal leaf: it wraps no further issues. */
|
|
1186
|
+
const isTerminal = (issue) => {
|
|
1187
|
+
switch (issue._tag) {
|
|
1188
|
+
case "Pointer":
|
|
1189
|
+
case "Composite":
|
|
1190
|
+
case "Filter":
|
|
1191
|
+
case "Encoding": return false;
|
|
1192
|
+
case "AnyOf": return issue.issues.length === 0;
|
|
1193
|
+
default: return true;
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1187
1196
|
/**
|
|
1188
1197
|
* Walk an Effect Schema issue tree, emitting one {@link FormIssue} per leaf
|
|
1189
1198
|
* failure. Path segments accumulate through `Pointer` nodes; `Composite` and
|
|
@@ -1212,6 +1221,15 @@ function schemaIssuesToFormIssues(issue, format = defaultLeafFormatter) {
|
|
|
1212
1221
|
for (const child of current.issues) walk(child, path);
|
|
1213
1222
|
return;
|
|
1214
1223
|
case "Filter":
|
|
1224
|
+
if (isTerminal(current.issue)) {
|
|
1225
|
+
out.push({
|
|
1226
|
+
path,
|
|
1227
|
+
message: format(current)
|
|
1228
|
+
});
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
walk(current.issue, path);
|
|
1232
|
+
return;
|
|
1215
1233
|
case "Encoding":
|
|
1216
1234
|
walk(current.issue, path);
|
|
1217
1235
|
return;
|
|
@@ -1233,7 +1251,7 @@ function schemaIssuesToFormIssues(issue, format = defaultLeafFormatter) {
|
|
|
1233
1251
|
* ({@link module:effect-forms/Validate}) so both fold into the same nested
|
|
1234
1252
|
* error tree as schema-decode failures.
|
|
1235
1253
|
*/
|
|
1236
|
-
function filterOutputToFormIssues(output) {
|
|
1254
|
+
function filterOutputToFormIssues$1(output) {
|
|
1237
1255
|
if (output === void 0 || output === true) return [];
|
|
1238
1256
|
if (output === false) return [{
|
|
1239
1257
|
path: [],
|
|
@@ -1262,8 +1280,6 @@ function filterIssueToFormIssues(issue) {
|
|
|
1262
1280
|
//#endregion
|
|
1263
1281
|
//#region src/FormBuilder.ts
|
|
1264
1282
|
/**
|
|
1265
|
-
* @copyright Thomas Foster
|
|
1266
|
-
*
|
|
1267
1283
|
* Compose a form schema one field at a time, with cross-field refinements.
|
|
1268
1284
|
*
|
|
1269
1285
|
* {@link make} (aliased {@link empty}) starts an empty builder;
|
|
@@ -1284,6 +1300,7 @@ function filterIssueToFormIssues(issue) {
|
|
|
1284
1300
|
* path — {@link asyncRefinements} exposes them and {@link runAsyncRefinements}
|
|
1285
1301
|
* runs them into a {@link FormIssue} list.
|
|
1286
1302
|
*
|
|
1303
|
+
* @copyright Thomas Foster
|
|
1287
1304
|
* @module effect-forms/FormBuilder
|
|
1288
1305
|
*/
|
|
1289
1306
|
var FormBuilder_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -1356,6 +1373,14 @@ function asyncRefinements(self) {
|
|
|
1356
1373
|
return self.refinements.filter((r) => r._tag === "async").map((r) => r.fn);
|
|
1357
1374
|
}
|
|
1358
1375
|
/**
|
|
1376
|
+
* Normalise a filter predicate's `Schema.FilterOutput` into a flat list of
|
|
1377
|
+
* {@link FormIssue}s (empty on success).
|
|
1378
|
+
*
|
|
1379
|
+
* @deprecated Use `Issues.filterOutputToFormIssues`; this re-export remains
|
|
1380
|
+
* only for callers that imported it from `FormBuilder`.
|
|
1381
|
+
*/
|
|
1382
|
+
const filterOutputToFormIssues = filterOutputToFormIssues$1;
|
|
1383
|
+
/**
|
|
1359
1384
|
* Run every asynchronous refinement on `self` against `values` and collect
|
|
1360
1385
|
* their failures as {@link FormIssue}s (empty when all pass). Runs them
|
|
1361
1386
|
* concurrently; requirements propagate on the `R` channel.
|
|
@@ -1363,13 +1388,11 @@ function asyncRefinements(self) {
|
|
|
1363
1388
|
function runAsyncRefinements(self, values) {
|
|
1364
1389
|
const refinements = asyncRefinements(self);
|
|
1365
1390
|
if (refinements.length === 0) return Effect.succeed([]);
|
|
1366
|
-
return Effect.forEach(refinements, (fn) => fn(values), { concurrency: "unbounded" }).pipe(Effect.map((outputs) => outputs.flatMap(filterOutputToFormIssues)));
|
|
1391
|
+
return Effect.forEach(refinements, (fn) => fn(values), { concurrency: "unbounded" }).pipe(Effect.map((outputs) => outputs.flatMap(filterOutputToFormIssues$1)));
|
|
1367
1392
|
}
|
|
1368
1393
|
//#endregion
|
|
1369
1394
|
//#region src/State.ts
|
|
1370
1395
|
/**
|
|
1371
|
-
* @copyright Thomas Foster
|
|
1372
|
-
*
|
|
1373
1396
|
* {@link FormState} — the serialisable snapshot of a form.
|
|
1374
1397
|
*
|
|
1375
1398
|
* This is the single object a
|
|
@@ -1378,6 +1401,7 @@ function runAsyncRefinements(self, values) {
|
|
|
1378
1401
|
* boundary, and {@link FormStateSchema} gives it an Effect Schema so it can be
|
|
1379
1402
|
* decoded back on arrival.
|
|
1380
1403
|
*
|
|
1404
|
+
* @copyright Thomas Foster
|
|
1381
1405
|
* @module effect-forms/State
|
|
1382
1406
|
*/
|
|
1383
1407
|
var State_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -1436,8 +1460,6 @@ function FormStateSchema(data) {
|
|
|
1436
1460
|
//#endregion
|
|
1437
1461
|
//#region src/Form.ts
|
|
1438
1462
|
/**
|
|
1439
|
-
* @copyright Thomas Foster
|
|
1440
|
-
*
|
|
1441
1463
|
* {@link make} — the core entry point: bind an Effect Schema to a form and
|
|
1442
1464
|
* get back a validator and an empty-state factory.
|
|
1443
1465
|
*
|
|
@@ -1448,9 +1470,8 @@ function FormStateSchema(data) {
|
|
|
1448
1470
|
* it through the schema, returning a {@link FormState} — valid with the
|
|
1449
1471
|
* decoded data, or invalid with the nested {@link FormErrors} and the posted
|
|
1450
1472
|
* values preserved so the form re-renders what the user typed. It is an
|
|
1451
|
-
* `Effect`, so a schema requiring decoding services composes naturally
|
|
1452
|
-
*
|
|
1453
|
-
* failures live in the returned state, not the error channel.
|
|
1473
|
+
* `Effect`, so a schema requiring decoding services composes naturally; it
|
|
1474
|
+
* has no error channel — validation failures live in the returned state.
|
|
1454
1475
|
* - `empty()` seeds an unposted, invalid-by-default form from the supplied
|
|
1455
1476
|
* defaults — what a `load` returns to render a blank create form.
|
|
1456
1477
|
*
|
|
@@ -1459,6 +1480,7 @@ function FormStateSchema(data) {
|
|
|
1459
1480
|
* {@link module:effect-forms/Coerce} helpers, …) so a multipart body decodes
|
|
1460
1481
|
* cleanly.
|
|
1461
1482
|
*
|
|
1483
|
+
* @copyright Thomas Foster
|
|
1462
1484
|
* @module effect-forms/Form
|
|
1463
1485
|
*/
|
|
1464
1486
|
var Form_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -1472,8 +1494,11 @@ const toStructured = (input) => {
|
|
|
1472
1494
|
return Effect.succeed(input);
|
|
1473
1495
|
};
|
|
1474
1496
|
/**
|
|
1475
|
-
* Bind `schema` to a form
|
|
1476
|
-
* {@link
|
|
1497
|
+
* Bind `schema` to a form: `validate(input)` decodes a submission into a
|
|
1498
|
+
* {@link FormState} (it never fails — validation failures live in the state;
|
|
1499
|
+
* its requirements are the schema's decoding services) and `empty()` seeds a
|
|
1500
|
+
* blank form from `defaults`. The returned {@link Form} is stateless and
|
|
1501
|
+
* reusable across requests.
|
|
1477
1502
|
*/
|
|
1478
1503
|
function make(schema, options) {
|
|
1479
1504
|
const id = options.id ?? "default";
|
|
@@ -1569,8 +1594,6 @@ function fromBuilder(builder, options = {}) {
|
|
|
1569
1594
|
//#endregion
|
|
1570
1595
|
//#region src/Mode.ts
|
|
1571
1596
|
/**
|
|
1572
|
-
* @copyright Thomas Foster
|
|
1573
|
-
*
|
|
1574
1597
|
* When a form validates (and, optionally, auto-submits).
|
|
1575
1598
|
*
|
|
1576
1599
|
* A reactive form has to decide *when* to run validation as the user types.
|
|
@@ -1595,6 +1618,7 @@ function fromBuilder(builder, options = {}) {
|
|
|
1595
1618
|
* once the form has been posted at least once; `"never"` keeps the classic
|
|
1596
1619
|
* frozen-until-resubmit behaviour.
|
|
1597
1620
|
*
|
|
1621
|
+
* @copyright Thomas Foster
|
|
1598
1622
|
* @module effect-forms/Mode
|
|
1599
1623
|
*/
|
|
1600
1624
|
var Mode_exports = /* @__PURE__ */ __exportAll({ parse: () => parse });
|
|
@@ -1662,8 +1686,6 @@ const errorResult = (message, status = 500) => ({
|
|
|
1662
1686
|
//#endregion
|
|
1663
1687
|
//#region src/Validate.ts
|
|
1664
1688
|
/**
|
|
1665
|
-
* @copyright Thomas Foster
|
|
1666
|
-
*
|
|
1667
1689
|
* Low-level validation helpers — decode a value through a schema and get back
|
|
1668
1690
|
* the {@link FormErrors} tree (or the list of {@link FormIssue}s) instead of a
|
|
1669
1691
|
* raised failure. `Form.make` is the high-level entry; these are for ad
|
|
@@ -1680,6 +1702,7 @@ const errorResult = (message, status = 500) => ({
|
|
|
1680
1702
|
* and interrupting the previous fiber on every new run (see that function's
|
|
1681
1703
|
* docs for why that alone gives debounce + "latest wins" cancellation).
|
|
1682
1704
|
*
|
|
1705
|
+
* @copyright Thomas Foster
|
|
1683
1706
|
* @module effect-forms/Validate
|
|
1684
1707
|
*/
|
|
1685
1708
|
var Validate_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -1725,7 +1748,7 @@ function validateValueSync(schema, input, format) {
|
|
|
1725
1748
|
* cannot fail the Effect error channel.
|
|
1726
1749
|
*/
|
|
1727
1750
|
function runSyncFieldValidator(validator, value) {
|
|
1728
|
-
return filterOutputToFormIssues(validator(value));
|
|
1751
|
+
return filterOutputToFormIssues$1(validator(value));
|
|
1729
1752
|
}
|
|
1730
1753
|
/**
|
|
1731
1754
|
* Build the Effect an {@link AsyncFieldValidator} run is: sleep `debounceMs`
|
|
@@ -1745,7 +1768,7 @@ function runSyncFieldValidator(validator, value) {
|
|
|
1745
1768
|
function runAsyncFieldValidator(validator, value, debounceMs = 0) {
|
|
1746
1769
|
return Effect.gen(function* () {
|
|
1747
1770
|
if (debounceMs > 0) yield* Effect.sleep(Duration.millis(debounceMs));
|
|
1748
|
-
return filterOutputToFormIssues(yield* validator(value));
|
|
1771
|
+
return filterOutputToFormIssues$1(yield* validator(value));
|
|
1749
1772
|
});
|
|
1750
1773
|
}
|
|
1751
1774
|
/**
|