@lucas-barake/effect-form 0.13.0 → 0.15.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/dist/cjs/Field.js +0 -67
- package/dist/cjs/Field.js.map +1 -1
- package/dist/cjs/FormAtoms.js +6 -26
- package/dist/cjs/FormAtoms.js.map +1 -1
- package/dist/cjs/FormBuilder.js +0 -66
- package/dist/cjs/FormBuilder.js.map +1 -1
- package/dist/cjs/Mode.js +0 -9
- package/dist/cjs/Mode.js.map +1 -1
- package/dist/cjs/Path.js +0 -34
- package/dist/cjs/Path.js.map +1 -1
- package/dist/cjs/Validation.js +0 -30
- package/dist/cjs/Validation.js.map +1 -1
- package/dist/cjs/internal/dirty.js +0 -16
- package/dist/cjs/internal/dirty.js.map +1 -1
- package/dist/cjs/internal/weak-registry.js +0 -11
- package/dist/cjs/internal/weak-registry.js.map +1 -1
- package/dist/dts/Field.d.ts +0 -99
- package/dist/dts/Field.d.ts.map +1 -1
- package/dist/dts/FormAtoms.d.ts +1 -54
- package/dist/dts/FormAtoms.d.ts.map +1 -1
- package/dist/dts/FormBuilder.d.ts +3 -152
- package/dist/dts/FormBuilder.d.ts.map +1 -1
- package/dist/dts/Mode.d.ts +0 -33
- package/dist/dts/Mode.d.ts.map +1 -1
- package/dist/dts/Path.d.ts +0 -34
- package/dist/dts/Path.d.ts.map +1 -1
- package/dist/dts/Validation.d.ts +0 -37
- package/dist/dts/Validation.d.ts.map +1 -1
- package/dist/dts/index.d.ts +17 -19
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/internal/dirty.d.ts +0 -10
- package/dist/dts/internal/dirty.d.ts.map +1 -1
- package/dist/dts/internal/weak-registry.d.ts +8 -6
- package/dist/dts/internal/weak-registry.d.ts.map +1 -1
- package/dist/esm/Field.js +0 -66
- package/dist/esm/Field.js.map +1 -1
- package/dist/esm/FormAtoms.js +7 -27
- package/dist/esm/FormAtoms.js.map +1 -1
- package/dist/esm/FormBuilder.js +0 -66
- package/dist/esm/FormBuilder.js.map +1 -1
- package/dist/esm/Mode.js +0 -8
- package/dist/esm/Mode.js.map +1 -1
- package/dist/esm/Path.js +0 -34
- package/dist/esm/Path.js.map +1 -1
- package/dist/esm/Validation.js +0 -29
- package/dist/esm/Validation.js.map +1 -1
- package/dist/esm/index.js +17 -19
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal/dirty.js +0 -15
- package/dist/esm/internal/dirty.js.map +1 -1
- package/dist/esm/internal/weak-registry.js +0 -11
- package/dist/esm/internal/weak-registry.js.map +1 -1
- package/package.json +1 -1
- package/src/Field.ts +0 -99
- package/src/FormAtoms.ts +277 -320
- package/src/FormBuilder.ts +0 -172
- package/src/Mode.ts +0 -33
- package/src/Path.ts +0 -35
- package/src/Validation.ts +0 -41
- package/src/index.ts +22 -19
- package/src/internal/dirty.ts +0 -15
- package/src/internal/weak-registry.ts +0 -17
package/src/FormBuilder.ts
CHANGED
|
@@ -14,72 +14,31 @@ import type {
|
|
|
14
14
|
} from "./Field.js"
|
|
15
15
|
import { isArrayFieldDef, isFieldDef, makeField } from "./Field.js"
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* @category Models
|
|
19
|
-
*/
|
|
20
17
|
export interface SubmittedValues<TFields extends FieldsRecord> {
|
|
21
18
|
readonly encoded: EncodedFromFields<TFields>
|
|
22
19
|
readonly decoded: DecodedFromFields<TFields>
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
/**
|
|
26
|
-
* Unique identifier for Field references.
|
|
27
|
-
*
|
|
28
|
-
* @category Symbols
|
|
29
|
-
* @internal
|
|
30
|
-
*/
|
|
31
22
|
export const FieldTypeId: unique symbol = Symbol.for("@lucas-barake/effect-form/Field")
|
|
32
23
|
|
|
33
|
-
/**
|
|
34
|
-
* @category Symbols
|
|
35
|
-
* @internal
|
|
36
|
-
*/
|
|
37
24
|
export type FieldTypeId = typeof FieldTypeId
|
|
38
25
|
|
|
39
|
-
/**
|
|
40
|
-
* A field reference carrying type and path info for type-safe setValue operations.
|
|
41
|
-
*
|
|
42
|
-
* @category Models
|
|
43
|
-
*/
|
|
44
26
|
export interface FieldRef<S> {
|
|
45
27
|
readonly [FieldTypeId]: FieldTypeId
|
|
46
28
|
readonly _S: S
|
|
47
29
|
readonly key: string
|
|
48
30
|
}
|
|
49
31
|
|
|
50
|
-
/**
|
|
51
|
-
* Creates a field reference for type-safe setValue operations.
|
|
52
|
-
*
|
|
53
|
-
* @category Constructors
|
|
54
|
-
* @internal
|
|
55
|
-
*/
|
|
56
32
|
export const makeFieldRef = <S>(key: string): FieldRef<S> => ({
|
|
57
33
|
[FieldTypeId]: FieldTypeId,
|
|
58
34
|
_S: undefined as any,
|
|
59
35
|
key,
|
|
60
36
|
})
|
|
61
37
|
|
|
62
|
-
// ================================
|
|
63
|
-
// FormBuilder
|
|
64
|
-
// ================================
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Unique identifier for FormBuilder instances.
|
|
68
|
-
*
|
|
69
|
-
* @category Symbols
|
|
70
|
-
*/
|
|
71
38
|
export const TypeId: unique symbol = Symbol.for("@lucas-barake/effect-form/Form")
|
|
72
39
|
|
|
73
|
-
/**
|
|
74
|
-
* @category Symbols
|
|
75
|
-
*/
|
|
76
40
|
export type TypeId = typeof TypeId
|
|
77
41
|
|
|
78
|
-
/**
|
|
79
|
-
* The state of a form at runtime.
|
|
80
|
-
*
|
|
81
|
-
* @category Models
|
|
82
|
-
*/
|
|
83
42
|
export interface FormState<TFields extends FieldsRecord> {
|
|
84
43
|
readonly values: EncodedFromFields<TFields>
|
|
85
44
|
readonly initialValues: EncodedFromFields<TFields>
|
|
@@ -101,123 +60,38 @@ interface AsyncRefinement {
|
|
|
101
60
|
|
|
102
61
|
type Refinement = SyncRefinement | AsyncRefinement
|
|
103
62
|
|
|
104
|
-
/**
|
|
105
|
-
* A builder for constructing type-safe forms with Effect Schema validation.
|
|
106
|
-
*
|
|
107
|
-
* **Details**
|
|
108
|
-
*
|
|
109
|
-
* FormBuilder uses a fluent API pattern to define form fields. Each field
|
|
110
|
-
* includes a Schema for validation. The builder accumulates field definitions
|
|
111
|
-
* and context requirements (`R`) from schemas that use Effect services.
|
|
112
|
-
*
|
|
113
|
-
* @category Models
|
|
114
|
-
*/
|
|
115
63
|
export interface FormBuilder<TFields extends FieldsRecord, R> {
|
|
116
64
|
readonly [TypeId]: TypeId
|
|
117
65
|
readonly fields: TFields
|
|
118
66
|
readonly refinements: ReadonlyArray<Refinement>
|
|
119
67
|
readonly _R?: R
|
|
120
68
|
|
|
121
|
-
/**
|
|
122
|
-
* Adds a scalar field to the form builder.
|
|
123
|
-
*
|
|
124
|
-
* @example
|
|
125
|
-
* ```ts
|
|
126
|
-
* const NameField = Field.makeField("name", Schema.String)
|
|
127
|
-
* const form = FormBuilder.empty.addField(NameField)
|
|
128
|
-
* ```
|
|
129
|
-
*/
|
|
130
69
|
addField<K extends string, S extends Schema.Schema.Any>(
|
|
131
70
|
this: FormBuilder<TFields, R>,
|
|
132
71
|
field: FieldDef<K, S>,
|
|
133
72
|
): FormBuilder<TFields & { readonly [key in K]: FieldDef<K, S> }, R | Schema.Schema.Context<S>>
|
|
134
73
|
|
|
135
|
-
/**
|
|
136
|
-
* Adds an array field to the form builder.
|
|
137
|
-
*
|
|
138
|
-
* @example
|
|
139
|
-
* ```ts
|
|
140
|
-
* const ItemsField = Field.makeArrayField("items", Schema.Struct({ name: Schema.String }))
|
|
141
|
-
* const form = FormBuilder.empty.addField(ItemsField)
|
|
142
|
-
* ```
|
|
143
|
-
*/
|
|
144
74
|
addField<K extends string, S extends Schema.Schema.Any>(
|
|
145
75
|
this: FormBuilder<TFields, R>,
|
|
146
76
|
field: ArrayFieldDef<K, S>,
|
|
147
77
|
): FormBuilder<TFields & { readonly [key in K]: ArrayFieldDef<K, S> }, R | Schema.Schema.Context<S>>
|
|
148
78
|
|
|
149
|
-
/**
|
|
150
|
-
* Adds a scalar field using inline key and schema.
|
|
151
|
-
* Shorthand for `Field.makeField(key, schema)` when field reuse is not needed.
|
|
152
|
-
*
|
|
153
|
-
* @example
|
|
154
|
-
* ```ts
|
|
155
|
-
* const form = FormBuilder.empty
|
|
156
|
-
* .addField("email", Schema.String)
|
|
157
|
-
* .addField("age", Schema.Number)
|
|
158
|
-
* ```
|
|
159
|
-
*/
|
|
160
79
|
addField<K extends string, S extends Schema.Schema.Any>(
|
|
161
80
|
this: FormBuilder<TFields, R>,
|
|
162
81
|
key: K,
|
|
163
82
|
schema: S,
|
|
164
83
|
): FormBuilder<TFields & { readonly [key in K]: FieldDef<K, S> }, R | Schema.Schema.Context<S>>
|
|
165
84
|
|
|
166
|
-
/**
|
|
167
|
-
* Merges another FormBuilder's fields into this one.
|
|
168
|
-
* Useful for composing reusable field groups.
|
|
169
|
-
*
|
|
170
|
-
* @example
|
|
171
|
-
* ```ts
|
|
172
|
-
* const addressFields = FormBuilder.empty
|
|
173
|
-
* .addField("street", Schema.String)
|
|
174
|
-
* .addField("city", Schema.String)
|
|
175
|
-
*
|
|
176
|
-
* const userForm = FormBuilder.empty
|
|
177
|
-
* .addField("name", Schema.String)
|
|
178
|
-
* .merge(addressFields)
|
|
179
|
-
* ```
|
|
180
|
-
*/
|
|
181
85
|
merge<TFields2 extends FieldsRecord, R2>(
|
|
182
86
|
this: FormBuilder<TFields, R>,
|
|
183
87
|
other: FormBuilder<TFields2, R2>,
|
|
184
88
|
): FormBuilder<TFields & TFields2, R | R2>
|
|
185
89
|
|
|
186
|
-
/**
|
|
187
|
-
* Adds a synchronous cross-field validation refinement to the form.
|
|
188
|
-
*
|
|
189
|
-
* @example
|
|
190
|
-
* ```ts
|
|
191
|
-
* const form = FormBuilder.empty
|
|
192
|
-
* .addField("password", Schema.String)
|
|
193
|
-
* .addField("confirmPassword", Schema.String)
|
|
194
|
-
* .refine((values) => {
|
|
195
|
-
* if (values.password !== values.confirmPassword) {
|
|
196
|
-
* return { path: ["confirmPassword"], message: "Passwords must match" }
|
|
197
|
-
* }
|
|
198
|
-
* })
|
|
199
|
-
* ```
|
|
200
|
-
*/
|
|
201
90
|
refine(
|
|
202
91
|
this: FormBuilder<TFields, R>,
|
|
203
92
|
predicate: (values: DecodedFromFields<TFields>) => Schema.FilterOutput,
|
|
204
93
|
): FormBuilder<TFields, R>
|
|
205
94
|
|
|
206
|
-
/**
|
|
207
|
-
* Adds an asynchronous cross-field validation refinement to the form.
|
|
208
|
-
*
|
|
209
|
-
* @example
|
|
210
|
-
* ```ts
|
|
211
|
-
* const form = FormBuilder.empty
|
|
212
|
-
* .addField("username", Schema.String)
|
|
213
|
-
* .refineEffect((values) =>
|
|
214
|
-
* Effect.gen(function* () {
|
|
215
|
-
* const taken = yield* checkUsername(values.username)
|
|
216
|
-
* if (taken) return { path: ["username"], message: "Already taken" }
|
|
217
|
-
* })
|
|
218
|
-
* )
|
|
219
|
-
* ```
|
|
220
|
-
*/
|
|
221
95
|
refineEffect<RD>(
|
|
222
96
|
this: FormBuilder<TFields, R>,
|
|
223
97
|
predicate: (values: DecodedFromFields<TFields>) => Effect.Effect<Schema.FilterOutput, never, RD>,
|
|
@@ -274,49 +148,8 @@ const FormBuilderProto = {
|
|
|
274
148
|
},
|
|
275
149
|
}
|
|
276
150
|
|
|
277
|
-
/**
|
|
278
|
-
* Checks if a value is a `FormBuilder`.
|
|
279
|
-
*
|
|
280
|
-
* @example
|
|
281
|
-
* ```ts
|
|
282
|
-
* import { FormBuilder } from "@lucas-barake/effect-form"
|
|
283
|
-
*
|
|
284
|
-
* const builder = FormBuilder.empty
|
|
285
|
-
*
|
|
286
|
-
* console.log(FormBuilder.isFormBuilder(builder))
|
|
287
|
-
* // Output: true
|
|
288
|
-
*
|
|
289
|
-
* console.log(FormBuilder.isFormBuilder({}))
|
|
290
|
-
* // Output: false
|
|
291
|
-
* ```
|
|
292
|
-
*
|
|
293
|
-
* @category Guards
|
|
294
|
-
*/
|
|
295
151
|
export const isFormBuilder = (u: unknown): u is FormBuilder<any, any> => Predicate.hasProperty(u, TypeId)
|
|
296
152
|
|
|
297
|
-
/**
|
|
298
|
-
* An empty `FormBuilder` to start building a form.
|
|
299
|
-
*
|
|
300
|
-
* **Details**
|
|
301
|
-
*
|
|
302
|
-
* This is the entry point for building a form. Use method chaining to add
|
|
303
|
-
* fields and then build the form with a React adapter.
|
|
304
|
-
*
|
|
305
|
-
* @example
|
|
306
|
-
* ```ts
|
|
307
|
-
* import { Field, FormBuilder } from "@lucas-barake/effect-form"
|
|
308
|
-
* import * as Schema from "effect/Schema"
|
|
309
|
-
*
|
|
310
|
-
* const EmailField = Field.makeField("email", Schema.String)
|
|
311
|
-
* const PasswordField = Field.makeField("password", Schema.String)
|
|
312
|
-
*
|
|
313
|
-
* const loginForm = FormBuilder.empty
|
|
314
|
-
* .addField(EmailField)
|
|
315
|
-
* .addField(PasswordField)
|
|
316
|
-
* ```
|
|
317
|
-
*
|
|
318
|
-
* @category Constructors
|
|
319
|
-
*/
|
|
320
153
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
321
154
|
export const empty: FormBuilder<{}, never> = (() => {
|
|
322
155
|
const self = Object.create(FormBuilderProto)
|
|
@@ -325,11 +158,6 @@ export const empty: FormBuilder<{}, never> = (() => {
|
|
|
325
158
|
return self
|
|
326
159
|
})()
|
|
327
160
|
|
|
328
|
-
/**
|
|
329
|
-
* Builds a combined Schema from a FormBuilder's field definitions.
|
|
330
|
-
*
|
|
331
|
-
* @category Schema
|
|
332
|
-
*/
|
|
333
161
|
export const buildSchema = <TFields extends FieldsRecord, R>(
|
|
334
162
|
self: FormBuilder<TFields, R>,
|
|
335
163
|
): Schema.Schema<DecodedFromFields<TFields>, EncodedFromFields<TFields>, R> => {
|
package/src/Mode.ts
CHANGED
|
@@ -1,22 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Form validation mode configuration.
|
|
3
|
-
*/
|
|
4
1
|
import * as Duration from "effect/Duration"
|
|
5
2
|
|
|
6
|
-
/**
|
|
7
|
-
* Controls when field validation is triggered and whether form auto-submits.
|
|
8
|
-
*
|
|
9
|
-
* Simple modes (string):
|
|
10
|
-
* - `"onSubmit"`: Validation only runs when the form is submitted (default)
|
|
11
|
-
* - `"onBlur"`: Validation runs when a field loses focus
|
|
12
|
-
* - `"onChange"`: Validation runs on every value change (sync)
|
|
13
|
-
*
|
|
14
|
-
* Object modes (with options):
|
|
15
|
-
* - `{ onChange: { debounce, autoSubmit? } }`: Debounced validation, optional auto-submit
|
|
16
|
-
* - `{ onBlur: { autoSubmit: true } }`: Validate on blur, auto-submit when valid
|
|
17
|
-
*
|
|
18
|
-
* @category Models
|
|
19
|
-
*/
|
|
20
3
|
export type FormMode =
|
|
21
4
|
| "onSubmit"
|
|
22
5
|
| "onBlur"
|
|
@@ -25,34 +8,18 @@ export type FormMode =
|
|
|
25
8
|
| { readonly onBlur: { readonly autoSubmit: true } }
|
|
26
9
|
| { readonly onChange: { readonly debounce: Duration.DurationInput; readonly autoSubmit: true } }
|
|
27
10
|
|
|
28
|
-
/**
|
|
29
|
-
* Form mode without auto-submit options.
|
|
30
|
-
* Used when SubmitArgs is not void, since auto-submit cannot provide custom arguments.
|
|
31
|
-
*
|
|
32
|
-
* @category Models
|
|
33
|
-
*/
|
|
34
11
|
export type FormModeWithoutAutoSubmit =
|
|
35
12
|
| "onSubmit"
|
|
36
13
|
| "onBlur"
|
|
37
14
|
| "onChange"
|
|
38
15
|
| { readonly onChange: { readonly debounce: Duration.DurationInput; readonly autoSubmit?: false } }
|
|
39
16
|
|
|
40
|
-
/**
|
|
41
|
-
* Parsed form mode with resolved values.
|
|
42
|
-
*
|
|
43
|
-
* @category Models
|
|
44
|
-
*/
|
|
45
17
|
export interface ParsedMode {
|
|
46
18
|
readonly validation: "onSubmit" | "onBlur" | "onChange"
|
|
47
19
|
readonly debounce: number | null
|
|
48
20
|
readonly autoSubmit: boolean
|
|
49
21
|
}
|
|
50
22
|
|
|
51
|
-
/**
|
|
52
|
-
* Parses a FormMode into a normalized ParsedMode.
|
|
53
|
-
*
|
|
54
|
-
* @category Parsing
|
|
55
|
-
*/
|
|
56
23
|
export const parse = (mode: FormMode = "onSubmit"): ParsedMode => {
|
|
57
24
|
if (typeof mode === "string") {
|
|
58
25
|
return { validation: mode, debounce: null, autoSubmit: false }
|
package/src/Path.ts
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Path utilities for form field operations.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
1
|
const BRACKET_NOTATION_REGEX = /\[(\d+)\]/g
|
|
6
2
|
|
|
7
|
-
/**
|
|
8
|
-
* Converts a schema path array to a dot/bracket notation string.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* schemaPathToFieldPath(["items", 0, "name"]) // "items[0].name"
|
|
12
|
-
*/
|
|
13
3
|
export const schemaPathToFieldPath = (path: ReadonlyArray<PropertyKey>): string => {
|
|
14
4
|
if (path.length === 0) return ""
|
|
15
5
|
|
|
@@ -25,21 +15,9 @@ export const schemaPathToFieldPath = (path: ReadonlyArray<PropertyKey>): string
|
|
|
25
15
|
return result
|
|
26
16
|
}
|
|
27
17
|
|
|
28
|
-
/**
|
|
29
|
-
* Checks if a path matches a root path or is a descendant of it.
|
|
30
|
-
* Handles both dot notation (root.child) and bracket notation (root[0]).
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* isPathUnderRoot("items[0].name", "items[0]") // true
|
|
34
|
-
* isPathUnderRoot("items[0].name", "items") // true
|
|
35
|
-
* isPathUnderRoot("other", "items") // false
|
|
36
|
-
*/
|
|
37
18
|
export const isPathUnderRoot = (path: string, rootPath: string): boolean =>
|
|
38
19
|
path === rootPath || path.startsWith(rootPath + ".") || path.startsWith(rootPath + "[")
|
|
39
20
|
|
|
40
|
-
/**
|
|
41
|
-
* Checks if a field path or any of its parent paths are in the dirty set.
|
|
42
|
-
*/
|
|
43
21
|
export const isPathOrParentDirty = (dirtyFields: ReadonlySet<string>, path: string): boolean => {
|
|
44
22
|
if (dirtyFields.has(path)) return true
|
|
45
23
|
|
|
@@ -58,12 +36,6 @@ export const isPathOrParentDirty = (dirtyFields: ReadonlySet<string>, path: stri
|
|
|
58
36
|
return false
|
|
59
37
|
}
|
|
60
38
|
|
|
61
|
-
/**
|
|
62
|
-
* Gets a nested value from an object using dot/bracket notation path.
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* getNestedValue({ items: [{ name: "A" }] }, "items[0].name") // "A"
|
|
66
|
-
*/
|
|
67
39
|
export const getNestedValue = (obj: unknown, path: string): unknown => {
|
|
68
40
|
if (path === "") return obj
|
|
69
41
|
const parts = path.replace(BRACKET_NOTATION_REGEX, ".$1").split(".")
|
|
@@ -75,13 +47,6 @@ export const getNestedValue = (obj: unknown, path: string): unknown => {
|
|
|
75
47
|
return current
|
|
76
48
|
}
|
|
77
49
|
|
|
78
|
-
/**
|
|
79
|
-
* Sets a nested value in an object immutably using dot/bracket notation path.
|
|
80
|
-
*
|
|
81
|
-
* @example
|
|
82
|
-
* setNestedValue({ items: [{ name: "A" }] }, "items[0].name", "B")
|
|
83
|
-
* // { items: [{ name: "B" }] }
|
|
84
|
-
*/
|
|
85
50
|
export const setNestedValue = <T>(obj: T, path: string, value: unknown): T => {
|
|
86
51
|
if (path === "") return value as T
|
|
87
52
|
const parts = path.replace(BRACKET_NOTATION_REGEX, ".$1").split(".")
|
package/src/Validation.ts
CHANGED
|
@@ -1,25 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation utilities for form error handling.
|
|
3
|
-
*/
|
|
4
1
|
import * as Option from "effect/Option"
|
|
5
2
|
import * as ParseResult from "effect/ParseResult"
|
|
6
3
|
import type * as AST from "effect/SchemaAST"
|
|
7
4
|
import { schemaPathToFieldPath } from "./Path.js"
|
|
8
5
|
|
|
9
|
-
/**
|
|
10
|
-
* Source of a validation error.
|
|
11
|
-
* - 'field': Error from field schema validation (e.g., minLength, pattern)
|
|
12
|
-
* - 'refinement': Error from cross-field refinement (e.g., password !== confirm)
|
|
13
|
-
*
|
|
14
|
-
* @category Models
|
|
15
|
-
*/
|
|
16
6
|
export type ErrorSource = "field" | "refinement"
|
|
17
7
|
|
|
18
|
-
/**
|
|
19
|
-
* A validation error entry with its source.
|
|
20
|
-
*
|
|
21
|
-
* @category Models
|
|
22
|
-
*/
|
|
23
8
|
export interface ErrorEntry {
|
|
24
9
|
readonly message: string
|
|
25
10
|
readonly source: ErrorSource
|
|
@@ -35,10 +20,6 @@ const getBaseAST = (ast: AST.AST): AST.AST => {
|
|
|
35
20
|
}
|
|
36
21
|
}
|
|
37
22
|
|
|
38
|
-
/**
|
|
39
|
-
* Returns true if the AST represents a composite type where refinements indicate cross-field validation.
|
|
40
|
-
* Covers: Schema.Struct, Class, Tuple, Union, Suspend.
|
|
41
|
-
*/
|
|
42
23
|
const isCompositeType = (ast: AST.AST): boolean => {
|
|
43
24
|
const base = getBaseAST(ast)
|
|
44
25
|
switch (base._tag) {
|
|
@@ -53,11 +34,6 @@ const isCompositeType = (ast: AST.AST): boolean => {
|
|
|
53
34
|
}
|
|
54
35
|
}
|
|
55
36
|
|
|
56
|
-
/**
|
|
57
|
-
* Extracts the first error message from a ParseError.
|
|
58
|
-
*
|
|
59
|
-
* @category Error Handling
|
|
60
|
-
*/
|
|
61
37
|
export const extractFirstError = (error: ParseResult.ParseError): Option.Option<string> => {
|
|
62
38
|
const issues = ParseResult.ArrayFormatter.formatErrorSync(error)
|
|
63
39
|
if (issues.length === 0) {
|
|
@@ -66,12 +42,6 @@ export const extractFirstError = (error: ParseResult.ParseError): Option.Option<
|
|
|
66
42
|
return Option.some(issues[0].message)
|
|
67
43
|
}
|
|
68
44
|
|
|
69
|
-
/**
|
|
70
|
-
* Routes validation errors from a ParseError to a map of field paths to error messages.
|
|
71
|
-
* Used for cross-field validation where schema errors need to be displayed on specific fields.
|
|
72
|
-
*
|
|
73
|
-
* @category Error Handling
|
|
74
|
-
*/
|
|
75
45
|
export const routeErrors = (error: ParseResult.ParseError): Map<string, string> => {
|
|
76
46
|
const result = new Map<string, string>()
|
|
77
47
|
const issues = ParseResult.ArrayFormatter.formatErrorSync(error)
|
|
@@ -140,17 +110,6 @@ const determineErrorSources = (error: ParseResult.ParseError): Map<string, Error
|
|
|
140
110
|
return sources
|
|
141
111
|
}
|
|
142
112
|
|
|
143
|
-
/**
|
|
144
|
-
* Routes validation errors with source tracking.
|
|
145
|
-
*
|
|
146
|
-
* Source determination:
|
|
147
|
-
* - `kind: "Predicate"` on composite types → "refinement" (cross-field validation)
|
|
148
|
-
* - All other errors → "field" (per-field schema validation)
|
|
149
|
-
*
|
|
150
|
-
* Empty string key ("") stores root-level errors (refinements without specific paths).
|
|
151
|
-
*
|
|
152
|
-
* @category Error Handling
|
|
153
|
-
*/
|
|
154
113
|
export const routeErrorsWithSource = (error: ParseResult.ParseError): Map<string, ErrorEntry> => {
|
|
155
114
|
const result = new Map<string, ErrorEntry>()
|
|
156
115
|
const formattedIssues = ParseResult.ArrayFormatter.formatErrorSync(error)
|
package/src/index.ts
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
* Field definitions for type-safe forms.
|
|
3
|
-
*/
|
|
1
|
+
|
|
4
2
|
export * as Field from "./Field.js"
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
* Root anchor atom for the form's dependency graph.
|
|
6
|
+
* Mount this atom to keep all form state alive even when field components unmount.
|
|
7
|
+
*
|
|
8
|
+
* Useful for:
|
|
9
|
+
* - Multi-step wizards where steps unmount but state should persist
|
|
10
|
+
* - Conditional fields (toggles) where state should survive visibility changes
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* // Keep form state alive at wizard root level
|
|
15
|
+
* function Wizard() {
|
|
16
|
+
* useAtomMount(step1Form.mount)
|
|
17
|
+
* useAtomMount(step2Form.mount)
|
|
18
|
+
* return currentStep === 1 ? <Step1 /> : <Step2 />
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
11
22
|
export * as FormAtoms from "./FormAtoms.js"
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
* @category Models
|
|
15
|
-
*/
|
|
24
|
+
|
|
16
25
|
export * as FormBuilder from "./FormBuilder.js"
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
* Form validation mode configuration.
|
|
20
|
-
*/
|
|
27
|
+
|
|
21
28
|
export * as Mode from "./Mode.js"
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
* Path utilities for form field operations.
|
|
25
|
-
*/
|
|
30
|
+
|
|
26
31
|
export * as Path from "./Path.js"
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
* Validation utilities for form error handling.
|
|
30
|
-
*/
|
|
33
|
+
|
|
31
34
|
export * as Validation from "./Validation.js"
|
package/src/internal/dirty.ts
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Internal dirty tracking algorithms.
|
|
3
|
-
*
|
|
4
|
-
* @internal
|
|
5
|
-
*/
|
|
6
1
|
import * as Equal from "effect/Equal"
|
|
7
2
|
import * as Utils from "effect/Utils"
|
|
8
3
|
import { getNestedValue, isPathUnderRoot } from "../Path.js"
|
|
9
4
|
|
|
10
|
-
/**
|
|
11
|
-
* Recalculates dirty fields for an array after mutation.
|
|
12
|
-
* Clears all paths under the array and re-evaluates each item.
|
|
13
|
-
*/
|
|
14
5
|
export const recalculateDirtyFieldsForArray = (
|
|
15
6
|
dirtyFields: ReadonlySet<string>,
|
|
16
7
|
initialValues: unknown,
|
|
@@ -50,12 +41,6 @@ export const recalculateDirtyFieldsForArray = (
|
|
|
50
41
|
return nextDirty
|
|
51
42
|
}
|
|
52
43
|
|
|
53
|
-
/**
|
|
54
|
-
* Recalculates dirty fields for a subtree after value change.
|
|
55
|
-
* Clears the rootPath and all children, then re-evaluates recursively.
|
|
56
|
-
*
|
|
57
|
-
* @param rootPath - Empty string for full form, or a specific path for targeted update
|
|
58
|
-
*/
|
|
59
44
|
export const recalculateDirtySubtree = (
|
|
60
45
|
currentDirty: ReadonlySet<string>,
|
|
61
46
|
allInitial: unknown,
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Internal WeakRef-based registry for caching atoms.
|
|
3
|
-
*
|
|
4
|
-
* @internal
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* A registry that uses WeakRef to allow garbage collection of cached values.
|
|
9
|
-
*
|
|
10
|
-
* @internal
|
|
11
|
-
*/
|
|
12
1
|
export interface WeakRegistry<V extends object> {
|
|
13
2
|
readonly get: (key: string) => V | undefined
|
|
14
3
|
readonly set: (key: string, value: V) => void
|
|
@@ -17,12 +6,6 @@ export interface WeakRegistry<V extends object> {
|
|
|
17
6
|
readonly values: () => IterableIterator<V>
|
|
18
7
|
}
|
|
19
8
|
|
|
20
|
-
/**
|
|
21
|
-
* Creates a WeakRef-based registry with automatic cleanup via FinalizationRegistry.
|
|
22
|
-
* Falls back to a regular Map in environments without WeakRef support.
|
|
23
|
-
*
|
|
24
|
-
* @internal
|
|
25
|
-
*/
|
|
26
9
|
export const createWeakRegistry = <V extends object>(): WeakRegistry<V> => {
|
|
27
10
|
if (typeof WeakRef === "undefined" || typeof FinalizationRegistry === "undefined") {
|
|
28
11
|
const map = new Map<string, V>()
|