@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.
Files changed (62) hide show
  1. package/dist/cjs/Field.js +0 -67
  2. package/dist/cjs/Field.js.map +1 -1
  3. package/dist/cjs/FormAtoms.js +6 -26
  4. package/dist/cjs/FormAtoms.js.map +1 -1
  5. package/dist/cjs/FormBuilder.js +0 -66
  6. package/dist/cjs/FormBuilder.js.map +1 -1
  7. package/dist/cjs/Mode.js +0 -9
  8. package/dist/cjs/Mode.js.map +1 -1
  9. package/dist/cjs/Path.js +0 -34
  10. package/dist/cjs/Path.js.map +1 -1
  11. package/dist/cjs/Validation.js +0 -30
  12. package/dist/cjs/Validation.js.map +1 -1
  13. package/dist/cjs/internal/dirty.js +0 -16
  14. package/dist/cjs/internal/dirty.js.map +1 -1
  15. package/dist/cjs/internal/weak-registry.js +0 -11
  16. package/dist/cjs/internal/weak-registry.js.map +1 -1
  17. package/dist/dts/Field.d.ts +0 -99
  18. package/dist/dts/Field.d.ts.map +1 -1
  19. package/dist/dts/FormAtoms.d.ts +1 -54
  20. package/dist/dts/FormAtoms.d.ts.map +1 -1
  21. package/dist/dts/FormBuilder.d.ts +3 -152
  22. package/dist/dts/FormBuilder.d.ts.map +1 -1
  23. package/dist/dts/Mode.d.ts +0 -33
  24. package/dist/dts/Mode.d.ts.map +1 -1
  25. package/dist/dts/Path.d.ts +0 -34
  26. package/dist/dts/Path.d.ts.map +1 -1
  27. package/dist/dts/Validation.d.ts +0 -37
  28. package/dist/dts/Validation.d.ts.map +1 -1
  29. package/dist/dts/index.d.ts +17 -19
  30. package/dist/dts/index.d.ts.map +1 -1
  31. package/dist/dts/internal/dirty.d.ts +0 -10
  32. package/dist/dts/internal/dirty.d.ts.map +1 -1
  33. package/dist/dts/internal/weak-registry.d.ts +8 -6
  34. package/dist/dts/internal/weak-registry.d.ts.map +1 -1
  35. package/dist/esm/Field.js +0 -66
  36. package/dist/esm/Field.js.map +1 -1
  37. package/dist/esm/FormAtoms.js +7 -27
  38. package/dist/esm/FormAtoms.js.map +1 -1
  39. package/dist/esm/FormBuilder.js +0 -66
  40. package/dist/esm/FormBuilder.js.map +1 -1
  41. package/dist/esm/Mode.js +0 -8
  42. package/dist/esm/Mode.js.map +1 -1
  43. package/dist/esm/Path.js +0 -34
  44. package/dist/esm/Path.js.map +1 -1
  45. package/dist/esm/Validation.js +0 -29
  46. package/dist/esm/Validation.js.map +1 -1
  47. package/dist/esm/index.js +17 -19
  48. package/dist/esm/index.js.map +1 -1
  49. package/dist/esm/internal/dirty.js +0 -15
  50. package/dist/esm/internal/dirty.js.map +1 -1
  51. package/dist/esm/internal/weak-registry.js +0 -11
  52. package/dist/esm/internal/weak-registry.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/Field.ts +0 -99
  55. package/src/FormAtoms.ts +277 -320
  56. package/src/FormBuilder.ts +0 -172
  57. package/src/Mode.ts +0 -33
  58. package/src/Path.ts +0 -35
  59. package/src/Validation.ts +0 -41
  60. package/src/index.ts +22 -19
  61. package/src/internal/dirty.ts +0 -15
  62. package/src/internal/weak-registry.ts +0 -17
package/src/FormAtoms.ts CHANGED
@@ -1,105 +1,86 @@
1
- import * as Atom from "@effect-atom/atom/Atom"
2
- import * as Effect from "effect/Effect"
3
- import { pipe } from "effect/Function"
4
- import * as Option from "effect/Option"
5
- import type * as ParseResult from "effect/ParseResult"
6
- import * as Schema from "effect/Schema"
7
- import * as Field from "./Field.js"
8
- import * as FormBuilder from "./FormBuilder.js"
9
- import { recalculateDirtyFieldsForArray, recalculateDirtySubtree } from "./internal/dirty.js"
10
- import { createWeakRegistry, type WeakRegistry } from "./internal/weak-registry.js"
11
- import { getNestedValue, setNestedValue } from "./Path.js"
12
- import * as Validation from "./Validation.js"
13
-
14
- /**
15
- * Atoms for a single field.
16
- *
17
- * @category Models
18
- */
1
+ import * as Atom from "@effect-atom/atom/Atom";
2
+ import * as Effect from "effect/Effect";
3
+ import { pipe } from "effect/Function";
4
+ import * as Option from "effect/Option";
5
+ import type * as ParseResult from "effect/ParseResult";
6
+ import * as Schema from "effect/Schema";
7
+ import * as Field from "./Field.js";
8
+ import * as FormBuilder from "./FormBuilder.js";
9
+ import { recalculateDirtyFieldsForArray, recalculateDirtySubtree } from "./internal/dirty.js";
10
+ import { createWeakRegistry, type WeakRegistry } from "./internal/weak-registry.js";
11
+ import { getNestedValue, isPathOrParentDirty, setNestedValue } from "./Path.js";
12
+ import * as Validation from "./Validation.js";
13
+
19
14
  export interface FieldAtoms {
20
- readonly valueAtom: Atom.Writable<unknown, unknown>
21
- readonly initialValueAtom: Atom.Atom<unknown>
22
- readonly touchedAtom: Atom.Writable<boolean, boolean>
23
- readonly errorAtom: Atom.Atom<Option.Option<Validation.ErrorEntry>>
15
+ readonly valueAtom: Atom.Writable<unknown, unknown>;
16
+ readonly initialValueAtom: Atom.Atom<unknown>;
17
+ readonly touchedAtom: Atom.Writable<boolean, boolean>;
18
+ readonly errorAtom: Atom.Atom<Option.Option<Validation.ErrorEntry>>;
19
+ readonly isDirtyAtom: Atom.Atom<boolean>;
24
20
  }
25
21
 
26
- /**
27
- * Configuration for creating form atoms.
28
- *
29
- * @category Models
30
- */
31
22
  export interface FormAtomsConfig<TFields extends Field.FieldsRecord, R, A, E, SubmitArgs = void> {
32
- readonly runtime: Atom.AtomRuntime<R, any>
33
- readonly formBuilder: FormBuilder.FormBuilder<TFields, R>
23
+ readonly runtime: Atom.AtomRuntime<R, any>;
24
+ readonly formBuilder: FormBuilder.FormBuilder<TFields, R>;
34
25
  readonly onSubmit: (
35
26
  args: SubmitArgs,
36
27
  ctx: {
37
- readonly decoded: Field.DecodedFromFields<TFields>
38
- readonly encoded: Field.EncodedFromFields<TFields>
39
- readonly get: Atom.FnContext
28
+ readonly decoded: Field.DecodedFromFields<TFields>;
29
+ readonly encoded: Field.EncodedFromFields<TFields>;
30
+ readonly get: Atom.FnContext;
40
31
  },
41
- ) => A | Effect.Effect<A, E, R>
32
+ ) => A | Effect.Effect<A, E, R>;
42
33
  }
43
34
 
44
- /**
45
- * Maps field names to their type-safe Field references for setValue operations.
46
- *
47
- * @category Models
48
- */
49
35
  export type FieldRefs<TFields extends Field.FieldsRecord> = {
50
- readonly [K in keyof TFields]: TFields[K] extends Field.FieldDef<any, infer S> ?
51
- FormBuilder.FieldRef<Schema.Schema.Encoded<S>>
52
- : TFields[K] extends Field.ArrayFieldDef<any, infer S> ?
53
- FormBuilder.FieldRef<ReadonlyArray<Schema.Schema.Encoded<S>>>
54
- : never
55
- }
36
+ readonly [K in keyof TFields]: TFields[K] extends Field.FieldDef<any, infer S>
37
+ ? FormBuilder.FieldRef<Schema.Schema.Encoded<S>>
38
+ : TFields[K] extends Field.ArrayFieldDef<any, infer S>
39
+ ? FormBuilder.FieldRef<ReadonlyArray<Schema.Schema.Encoded<S>>>
40
+ : never;
41
+ };
56
42
 
57
- /**
58
- * The complete form atoms infrastructure.
59
- *
60
- * @category Models
61
- */
62
43
  export interface FormAtoms<TFields extends Field.FieldsRecord, R, A = void, E = never, SubmitArgs = void> {
63
44
  readonly stateAtom: Atom.Writable<
64
45
  Option.Option<FormBuilder.FormState<TFields>>,
65
46
  Option.Option<FormBuilder.FormState<TFields>>
66
- >
67
- readonly errorsAtom: Atom.Writable<Map<string, Validation.ErrorEntry>, Map<string, Validation.ErrorEntry>>
68
- readonly rootErrorAtom: Atom.Atom<Option.Option<string>>
69
- readonly valuesAtom: Atom.Atom<Option.Option<Field.EncodedFromFields<TFields>>>
70
- readonly dirtyFieldsAtom: Atom.Atom<ReadonlySet<string>>
71
- readonly isDirtyAtom: Atom.Atom<boolean>
72
- readonly submitCountAtom: Atom.Atom<number>
73
- readonly lastSubmittedValuesAtom: Atom.Atom<Option.Option<FormBuilder.SubmittedValues<TFields>>>
74
- readonly changedSinceSubmitFieldsAtom: Atom.Atom<ReadonlySet<string>>
75
- readonly hasChangedSinceSubmitAtom: Atom.Atom<boolean>
47
+ >;
48
+ readonly errorsAtom: Atom.Writable<Map<string, Validation.ErrorEntry>, Map<string, Validation.ErrorEntry>>;
49
+ readonly rootErrorAtom: Atom.Atom<Option.Option<string>>;
50
+ readonly valuesAtom: Atom.Atom<Option.Option<Field.EncodedFromFields<TFields>>>;
51
+ readonly dirtyFieldsAtom: Atom.Atom<ReadonlySet<string>>;
52
+ readonly isDirtyAtom: Atom.Atom<boolean>;
53
+ readonly submitCountAtom: Atom.Atom<number>;
54
+ readonly lastSubmittedValuesAtom: Atom.Atom<Option.Option<FormBuilder.SubmittedValues<TFields>>>;
55
+ readonly changedSinceSubmitFieldsAtom: Atom.Atom<ReadonlySet<string>>;
56
+ readonly hasChangedSinceSubmitAtom: Atom.Atom<boolean>;
76
57
 
77
- readonly submitAtom: Atom.AtomResultFn<SubmitArgs, A, E | ParseResult.ParseError>
58
+ readonly submitAtom: Atom.AtomResultFn<SubmitArgs, A, E | ParseResult.ParseError>;
78
59
 
79
- readonly combinedSchema: Schema.Schema<Field.DecodedFromFields<TFields>, Field.EncodedFromFields<TFields>, R>
60
+ readonly combinedSchema: Schema.Schema<Field.DecodedFromFields<TFields>, Field.EncodedFromFields<TFields>, R>;
80
61
 
81
- readonly fieldRefs: FieldRefs<TFields>
62
+ readonly fieldRefs: FieldRefs<TFields>;
82
63
 
83
- readonly validationAtomsRegistry: WeakRegistry<Atom.AtomResultFn<unknown, void, ParseResult.ParseError>>
84
- readonly fieldAtomsRegistry: WeakRegistry<FieldAtoms>
64
+ readonly validationAtomsRegistry: WeakRegistry<Atom.AtomResultFn<unknown, void, ParseResult.ParseError>>;
65
+ readonly fieldAtomsRegistry: WeakRegistry<FieldAtoms>;
85
66
 
86
67
  readonly getOrCreateValidationAtom: (
87
68
  fieldPath: string,
88
69
  schema: Schema.Schema.Any,
89
- ) => Atom.AtomResultFn<unknown, void, ParseResult.ParseError>
70
+ ) => Atom.AtomResultFn<unknown, void, ParseResult.ParseError>;
90
71
 
91
- readonly getOrCreateFieldAtoms: (fieldPath: string) => FieldAtoms
72
+ readonly getOrCreateFieldAtoms: (fieldPath: string) => FieldAtoms;
92
73
 
93
- readonly resetValidationAtoms: (ctx: { set: <R, W>(atom: Atom.Writable<R, W>, value: W) => void }) => void
74
+ readonly resetValidationAtoms: (ctx: { set: <R, W>(atom: Atom.Writable<R, W>, value: W) => void; }) => void;
94
75
 
95
- readonly operations: FormOperations<TFields>
76
+ readonly operations: FormOperations<TFields>;
96
77
 
97
- readonly resetAtom: Atom.Writable<void, void>
98
- readonly revertToLastSubmitAtom: Atom.Writable<void, void>
99
- readonly setValuesAtom: Atom.Writable<void, Field.EncodedFromFields<TFields>>
100
- readonly setValue: <S>(field: FormBuilder.FieldRef<S>) => Atom.Writable<void, S | ((prev: S) => S)>
78
+ readonly resetAtom: Atom.Writable<void, void>;
79
+ readonly revertToLastSubmitAtom: Atom.Writable<void, void>;
80
+ readonly setValuesAtom: Atom.Writable<void, Field.EncodedFromFields<TFields>>;
81
+ readonly setValue: <S>(field: FormBuilder.FieldRef<S>) => Atom.Writable<void, S | ((prev: S) => S)>;
101
82
 
102
- readonly getFieldAtom: <S>(field: FormBuilder.FieldRef<S>) => Atom.Atom<Option.Option<S>>
83
+ readonly getFieldAtom: <S>(field: FormBuilder.FieldRef<S>) => Atom.Atom<Option.Option<S>>;
103
84
 
104
85
  /**
105
86
  * Root anchor atom for the form's dependency graph.
@@ -119,144 +100,110 @@ export interface FormAtoms<TFields extends Field.FieldsRecord, R, A = void, E =
119
100
  * }
120
101
  * ```
121
102
  */
122
- readonly mountAtom: Atom.Atom<void>
103
+ readonly mountAtom: Atom.Atom<void>;
123
104
 
124
- readonly keepAliveActiveAtom: Atom.Writable<boolean, boolean>
105
+ readonly keepAliveActiveAtom: Atom.Writable<boolean, boolean>;
125
106
  }
126
107
 
127
- /**
128
- * Pure state operations for form manipulation.
129
- *
130
- * @category Models
131
- */
132
108
  export interface FormOperations<TFields extends Field.FieldsRecord> {
133
- readonly createInitialState: (defaultValues: Field.EncodedFromFields<TFields>) => FormBuilder.FormState<TFields>
109
+ readonly createInitialState: (defaultValues: Field.EncodedFromFields<TFields>) => FormBuilder.FormState<TFields>;
134
110
 
135
- readonly createResetState: (state: FormBuilder.FormState<TFields>) => FormBuilder.FormState<TFields>
111
+ readonly createResetState: (state: FormBuilder.FormState<TFields>) => FormBuilder.FormState<TFields>;
136
112
 
137
- readonly createSubmitState: (state: FormBuilder.FormState<TFields>) => FormBuilder.FormState<TFields>
113
+ readonly createSubmitState: (state: FormBuilder.FormState<TFields>) => FormBuilder.FormState<TFields>;
138
114
 
139
115
  readonly setFieldValue: (
140
116
  state: FormBuilder.FormState<TFields>,
141
117
  fieldPath: string,
142
118
  value: unknown,
143
- ) => FormBuilder.FormState<TFields>
119
+ ) => FormBuilder.FormState<TFields>;
144
120
 
145
121
  readonly setFormValues: (
146
122
  state: FormBuilder.FormState<TFields>,
147
123
  values: Field.EncodedFromFields<TFields>,
148
- ) => FormBuilder.FormState<TFields>
124
+ ) => FormBuilder.FormState<TFields>;
149
125
 
150
126
  readonly setFieldTouched: (
151
127
  state: FormBuilder.FormState<TFields>,
152
128
  fieldPath: string,
153
129
  touched: boolean,
154
- ) => FormBuilder.FormState<TFields>
130
+ ) => FormBuilder.FormState<TFields>;
155
131
 
156
132
  readonly appendArrayItem: (
157
133
  state: FormBuilder.FormState<TFields>,
158
134
  arrayPath: string,
159
135
  itemSchema: Schema.Schema.Any,
160
136
  value?: unknown,
161
- ) => FormBuilder.FormState<TFields>
137
+ ) => FormBuilder.FormState<TFields>;
162
138
 
163
139
  readonly removeArrayItem: (
164
140
  state: FormBuilder.FormState<TFields>,
165
141
  arrayPath: string,
166
142
  index: number,
167
- ) => FormBuilder.FormState<TFields>
143
+ ) => FormBuilder.FormState<TFields>;
168
144
 
169
145
  readonly swapArrayItems: (
170
146
  state: FormBuilder.FormState<TFields>,
171
147
  arrayPath: string,
172
148
  indexA: number,
173
149
  indexB: number,
174
- ) => FormBuilder.FormState<TFields>
150
+ ) => FormBuilder.FormState<TFields>;
175
151
 
176
152
  readonly moveArrayItem: (
177
153
  state: FormBuilder.FormState<TFields>,
178
154
  arrayPath: string,
179
155
  fromIndex: number,
180
156
  toIndex: number,
181
- ) => FormBuilder.FormState<TFields>
157
+ ) => FormBuilder.FormState<TFields>;
182
158
 
183
- /**
184
- * Reverts values to the last submitted state.
185
- * No-op if form has never been submitted or is already in sync.
186
- */
187
- readonly revertToLastSubmit: (state: FormBuilder.FormState<TFields>) => FormBuilder.FormState<TFields>
159
+ readonly revertToLastSubmit: (state: FormBuilder.FormState<TFields>) => FormBuilder.FormState<TFields>;
188
160
  }
189
161
 
190
- /**
191
- * Creates the complete form atoms infrastructure.
192
- *
193
- * @example
194
- * ```ts
195
- * import * as FormAtoms from "@lucas-barake/effect-form/FormAtoms"
196
- * import * as Form from "@lucas-barake/effect-form"
197
- * import * as Atom from "@effect-atom/atom/Atom"
198
- * import * as Layer from "effect/Layer"
199
- *
200
- * const runtime = Atom.runtime(Layer.empty)
201
- *
202
- * const loginForm = FormBuilder.empty
203
- * .addField(FormBuilder.makeField("email", Schema.String))
204
- * .addField(FormBuilder.makeField("password", Schema.String))
205
- *
206
- * const atoms = FormAtoms.make({
207
- * runtime,
208
- * formBuilder: loginForm,
209
- * parsedMode: { validation: "onChange", debounce: 300, autoSubmit: false }
210
- * })
211
- * ```
212
- *
213
- * @category Constructors
214
- */
215
162
  export const make = <TFields extends Field.FieldsRecord, R, A, E, SubmitArgs = void>(
216
163
  config: FormAtomsConfig<TFields, R, A, E, SubmitArgs>,
217
164
  ): FormAtoms<TFields, R, A, E, SubmitArgs> => {
218
- const { formBuilder, runtime } = config
219
- const { fields } = formBuilder
165
+ const { formBuilder, runtime } = config;
166
+ const { fields } = formBuilder;
220
167
 
221
- const combinedSchema = FormBuilder.buildSchema(formBuilder)
168
+ const combinedSchema = FormBuilder.buildSchema(formBuilder);
222
169
 
223
- const stateAtom = Atom.make(Option.none<FormBuilder.FormState<TFields>>()).pipe(Atom.setIdleTTL(0))
224
- const errorsAtom = Atom.make<Map<string, Validation.ErrorEntry>>(new Map()).pipe(Atom.setIdleTTL(0))
170
+ const stateAtom = Atom.make(Option.none<FormBuilder.FormState<TFields>>()).pipe(Atom.setIdleTTL(0));
171
+ const errorsAtom = Atom.make<Map<string, Validation.ErrorEntry>>(new Map()).pipe(Atom.setIdleTTL(0));
225
172
 
226
173
  const rootErrorAtom = Atom.readable((get) => {
227
- const errors = get(errorsAtom)
228
- const entry = errors.get("")
229
- return entry ? Option.some(entry.message) : Option.none<string>()
230
- }).pipe(Atom.setIdleTTL(0))
174
+ const errors = get(errorsAtom);
175
+ const entry = errors.get("");
176
+ return entry ? Option.some(entry.message) : Option.none<string>();
177
+ }).pipe(Atom.setIdleTTL(0));
231
178
 
232
179
  const valuesAtom = Atom.readable((get) => Option.map(get(stateAtom), (state) => state.values)).pipe(
233
180
  Atom.setIdleTTL(0),
234
- )
181
+ );
235
182
 
236
183
  const dirtyFieldsAtom = Atom.readable((get) =>
237
184
  Option.match(get(stateAtom), {
238
185
  onNone: () => new Set<string>(),
239
186
  onSome: (state) => state.dirtyFields,
240
187
  })
241
- ).pipe(Atom.setIdleTTL(0))
188
+ ).pipe(Atom.setIdleTTL(0));
242
189
 
243
190
  const isDirtyAtom = Atom.readable((get) =>
244
191
  Option.match(get(stateAtom), {
245
192
  onNone: () => false,
246
193
  onSome: (state) => state.dirtyFields.size > 0,
247
194
  })
248
- ).pipe(Atom.setIdleTTL(0))
195
+ ).pipe(Atom.setIdleTTL(0));
249
196
 
250
197
  const submitCountAtom = Atom.readable((get) =>
251
198
  Option.match(get(stateAtom), {
252
199
  onNone: () => 0,
253
200
  onSome: (state) => state.submitCount,
254
201
  })
255
- ).pipe(Atom.setIdleTTL(0))
202
+ ).pipe(Atom.setIdleTTL(0));
256
203
 
257
204
  const lastSubmittedValuesAtom = Atom.readable((get) =>
258
205
  Option.flatMap(get(stateAtom), (state) => state.lastSubmittedValues)
259
- ).pipe(Atom.setIdleTTL(0))
206
+ ).pipe(Atom.setIdleTTL(0));
260
207
 
261
208
  const changedSinceSubmitFieldsAtom = Atom.readable((get) =>
262
209
  Option.match(get(stateAtom), {
@@ -267,136 +214,147 @@ export const make = <TFields extends Field.FieldsRecord, R, A, E, SubmitArgs = v
267
214
  onSome: (lastSubmitted) => recalculateDirtySubtree(new Set(), lastSubmitted.encoded, state.values, ""),
268
215
  }),
269
216
  })
270
- ).pipe(Atom.setIdleTTL(0))
217
+ ).pipe(Atom.setIdleTTL(0));
271
218
 
272
219
  const hasChangedSinceSubmitAtom = Atom.readable((get) =>
273
220
  Option.match(get(stateAtom), {
274
221
  onNone: () => false,
275
222
  onSome: (state) => {
276
- if (Option.isNone(state.lastSubmittedValues)) return false
277
- if (state.values === state.lastSubmittedValues.value.encoded) return false
278
- return get(changedSinceSubmitFieldsAtom).size > 0
223
+ if (Option.isNone(state.lastSubmittedValues)) return false;
224
+ if (state.values === state.lastSubmittedValues.value.encoded) return false;
225
+ return get(changedSinceSubmitFieldsAtom).size > 0;
279
226
  },
280
227
  })
281
- ).pipe(Atom.setIdleTTL(0))
228
+ ).pipe(Atom.setIdleTTL(0));
282
229
 
283
- const validationAtomsRegistry = createWeakRegistry<Atom.AtomResultFn<unknown, void, ParseResult.ParseError>>()
284
- const fieldAtomsRegistry = createWeakRegistry<FieldAtoms>()
285
- const publicFieldAtomRegistry = createWeakRegistry<Atom.Atom<Option.Option<unknown>>>()
230
+ const validationAtomsRegistry = createWeakRegistry<Atom.AtomResultFn<unknown, void, ParseResult.ParseError>>();
231
+ const fieldAtomsRegistry = createWeakRegistry<FieldAtoms>();
232
+ const publicFieldAtomRegistry = createWeakRegistry<Atom.Atom<Option.Option<unknown>>>();
286
233
 
287
234
  const getOrCreateValidationAtom = (
288
235
  fieldPath: string,
289
236
  schema: Schema.Schema.Any,
290
237
  ): Atom.AtomResultFn<unknown, void, ParseResult.ParseError> => {
291
- const existing = validationAtomsRegistry.get(fieldPath)
292
- if (existing) return existing
238
+ const existing = validationAtomsRegistry.get(fieldPath);
239
+ if (existing) return existing;
293
240
 
294
- const validationAtom = runtime.fn<unknown>()((value: unknown) =>
295
- pipe(
296
- Schema.decodeUnknown(schema)(value) as Effect.Effect<unknown, ParseResult.ParseError, R>,
297
- Effect.asVoid,
241
+ const validationAtom = runtime
242
+ .fn<unknown>()((value: unknown) =>
243
+ pipe(Schema.decodeUnknown(schema)(value) as Effect.Effect<unknown, ParseResult.ParseError, R>, Effect.asVoid)
298
244
  )
299
- ).pipe(Atom.setIdleTTL(0)) as Atom.AtomResultFn<unknown, void, ParseResult.ParseError>
245
+ .pipe(Atom.setIdleTTL(0)) as Atom.AtomResultFn<unknown, void, ParseResult.ParseError>;
300
246
 
301
- validationAtomsRegistry.set(fieldPath, validationAtom)
302
- return validationAtom
303
- }
247
+ validationAtomsRegistry.set(fieldPath, validationAtom);
248
+ return validationAtom;
249
+ };
304
250
 
305
251
  const getOrCreateFieldAtoms = (fieldPath: string): FieldAtoms => {
306
- const existing = fieldAtomsRegistry.get(fieldPath)
307
- if (existing) return existing
252
+ const existing = fieldAtomsRegistry.get(fieldPath);
253
+ if (existing) return existing;
308
254
 
309
255
  const valueAtom = Atom.writable(
310
256
  (get) => getNestedValue(Option.getOrThrow(get(stateAtom)).values, fieldPath),
311
257
  (ctx, value) => {
312
- const currentState = Option.getOrThrow(ctx.get(stateAtom))
313
- ctx.set(stateAtom, Option.some(operations.setFieldValue(currentState, fieldPath, value)))
258
+ const currentState = Option.getOrThrow(ctx.get(stateAtom));
259
+ ctx.set(stateAtom, Option.some(operations.setFieldValue(currentState, fieldPath, value)));
314
260
  },
315
- ).pipe(Atom.setIdleTTL(0))
261
+ ).pipe(Atom.setIdleTTL(0));
316
262
 
317
- const initialValueAtom = Atom.readable(
318
- (get) => getNestedValue(Option.getOrThrow(get(stateAtom)).initialValues, fieldPath),
319
- ).pipe(Atom.setIdleTTL(0))
263
+ const initialValueAtom = Atom.readable((get) =>
264
+ getNestedValue(Option.getOrThrow(get(stateAtom)).initialValues, fieldPath)
265
+ ).pipe(Atom.setIdleTTL(0));
320
266
 
321
267
  const touchedAtom = Atom.writable(
322
268
  (get) => (getNestedValue(Option.getOrThrow(get(stateAtom)).touched, fieldPath) ?? false) as boolean,
323
269
  (ctx, value) => {
324
- const currentState = Option.getOrThrow(ctx.get(stateAtom))
270
+ const currentState = Option.getOrThrow(ctx.get(stateAtom));
325
271
  ctx.set(
326
272
  stateAtom,
327
273
  Option.some({
328
274
  ...currentState,
329
275
  touched: setNestedValue(currentState.touched, fieldPath, value),
330
276
  }),
331
- )
277
+ );
332
278
  },
333
- ).pipe(Atom.setIdleTTL(0))
279
+ ).pipe(Atom.setIdleTTL(0));
334
280
 
335
281
  const errorAtom = Atom.readable((get) => {
336
- const errors = get(errorsAtom)
337
- const entry = errors.get(fieldPath)
338
- return entry ? Option.some(entry) : Option.none<Validation.ErrorEntry>()
339
- }).pipe(Atom.setIdleTTL(0))
282
+ const errors = get(errorsAtom);
283
+ const entry = errors.get(fieldPath);
284
+ return entry ? Option.some(entry) : Option.none<Validation.ErrorEntry>();
285
+ }).pipe(Atom.setIdleTTL(0));
286
+
287
+ const isDirtyAtom = Atom.readable((get) =>
288
+ isPathOrParentDirty(
289
+ Option.match(get(stateAtom), {
290
+ onNone: () => new Set<string>(),
291
+ onSome: (state) => state.dirtyFields,
292
+ }),
293
+ fieldPath,
294
+ )
295
+ ).pipe(Atom.setIdleTTL(0));
340
296
 
341
- const atoms: FieldAtoms = { valueAtom, initialValueAtom, touchedAtom, errorAtom }
342
- fieldAtomsRegistry.set(fieldPath, atoms)
343
- return atoms
344
- }
297
+ const atoms: FieldAtoms = { valueAtom, initialValueAtom, touchedAtom, errorAtom, isDirtyAtom };
298
+ fieldAtomsRegistry.set(fieldPath, atoms);
299
+ return atoms;
300
+ };
345
301
 
346
- const resetValidationAtoms = (ctx: { set: <R, W>(atom: Atom.Writable<R, W>, value: W) => void }) => {
302
+ const resetValidationAtoms = (ctx: { set: <R, W>(atom: Atom.Writable<R, W>, value: W) => void; }) => {
347
303
  for (const validationAtom of validationAtomsRegistry.values()) {
348
- ctx.set(validationAtom, Atom.Reset)
304
+ ctx.set(validationAtom, Atom.Reset);
349
305
  }
350
- validationAtomsRegistry.clear()
351
- fieldAtomsRegistry.clear()
352
- }
353
-
354
- const submitAtom = runtime.fn<SubmitArgs>()((args, get) =>
355
- Effect.gen(function*() {
356
- const state = get(stateAtom)
357
- if (Option.isNone(state)) return yield* Effect.die("Form not initialized")
358
- const values = state.value.values
359
- get.set(errorsAtom, new Map())
360
- const decoded = yield* pipe(
361
- Schema.decodeUnknown(combinedSchema, { errors: "all" })(values) as Effect.Effect<
362
- Field.DecodedFromFields<TFields>,
363
- ParseResult.ParseError,
364
- R
365
- >,
366
- Effect.tapError((parseError) =>
367
- Effect.sync(() => {
368
- const routedErrors = Validation.routeErrorsWithSource(parseError)
369
- get.set(errorsAtom, routedErrors)
370
- get.set(stateAtom, Option.some(operations.createSubmitState(state.value)))
371
- })
372
- ),
373
- )
374
- const submitState = operations.createSubmitState(state.value)
375
- get.set(
376
- stateAtom,
377
- Option.some({
378
- ...submitState,
379
- lastSubmittedValues: Option.some({ encoded: values, decoded }),
380
- }),
381
- )
382
- const result = config.onSubmit(args, { decoded, encoded: values, get })
383
- if (Effect.isEffect(result)) {
384
- return yield* (result as Effect.Effect<A, E, R>)
385
- }
386
- return result as A
387
- })
388
- ).pipe(Atom.setIdleTTL(0)) as Atom.AtomResultFn<SubmitArgs, A, E | ParseResult.ParseError>
306
+ validationAtomsRegistry.clear();
307
+ fieldAtomsRegistry.clear();
308
+ };
309
+
310
+ const submitAtom = runtime
311
+ .fn<SubmitArgs>()((args, get) =>
312
+ Effect.gen(function*() {
313
+ const state = get(stateAtom);
314
+ if (Option.isNone(state)) return yield* Effect.die("Form not initialized");
315
+ const values = state.value.values;
316
+ get.set(errorsAtom, new Map());
317
+ const decoded = yield* pipe(
318
+ Schema.decodeUnknown(combinedSchema, { errors: "all" })(values) as Effect.Effect<
319
+ Field.DecodedFromFields<TFields>,
320
+ ParseResult.ParseError,
321
+ R
322
+ >,
323
+ Effect.tapError((parseError) =>
324
+ Effect.sync(() => {
325
+ const routedErrors = Validation.routeErrorsWithSource(parseError);
326
+ get.set(errorsAtom, routedErrors);
327
+ get.set(stateAtom, Option.some(operations.createSubmitState(state.value)));
328
+ })
329
+ ),
330
+ );
331
+ const submitState = operations.createSubmitState(state.value);
332
+ get.set(
333
+ stateAtom,
334
+ Option.some({
335
+ ...submitState,
336
+ lastSubmittedValues: Option.some({ encoded: values, decoded }),
337
+ }),
338
+ );
339
+ const result = config.onSubmit(args, { decoded, encoded: values, get });
340
+ if (Effect.isEffect(result)) {
341
+ return yield* result as Effect.Effect<A, E, R>;
342
+ }
343
+ return result as A;
344
+ })
345
+ )
346
+ .pipe(Atom.setIdleTTL(0)) as Atom.AtomResultFn<SubmitArgs, A, E | ParseResult.ParseError>;
389
347
 
390
348
  const fieldRefs = Object.fromEntries(
391
349
  Object.keys(fields).map((key) => [key, FormBuilder.makeFieldRef(key)]),
392
- ) as FieldRefs<TFields>
350
+ ) as FieldRefs<TFields>;
393
351
 
394
352
  const operations: FormOperations<TFields> = {
395
353
  createInitialState: (defaultValues) => ({
396
354
  values: defaultValues,
397
355
  initialValues: defaultValues,
398
356
  lastSubmittedValues: Option.none(),
399
- touched: Field.createTouchedRecord(fields, false) as { readonly [K in keyof TFields]: boolean },
357
+ touched: Field.createTouchedRecord(fields, false) as { readonly [K in keyof TFields]: boolean; },
400
358
  submitCount: 0,
401
359
  dirtyFields: new Set(),
402
360
  }),
@@ -405,201 +363,200 @@ export const make = <TFields extends Field.FieldsRecord, R, A, E, SubmitArgs = v
405
363
  values: state.initialValues,
406
364
  initialValues: state.initialValues,
407
365
  lastSubmittedValues: Option.none(),
408
- touched: Field.createTouchedRecord(fields, false) as { readonly [K in keyof TFields]: boolean },
366
+ touched: Field.createTouchedRecord(fields, false) as { readonly [K in keyof TFields]: boolean; },
409
367
  submitCount: 0,
410
368
  dirtyFields: new Set(),
411
369
  }),
412
370
 
413
371
  createSubmitState: (state) => ({
414
372
  ...state,
415
- touched: Field.createTouchedRecord(fields, true) as { readonly [K in keyof TFields]: boolean },
373
+ touched: Field.createTouchedRecord(fields, true) as { readonly [K in keyof TFields]: boolean; },
416
374
  submitCount: state.submitCount + 1,
417
375
  }),
418
376
 
419
377
  setFieldValue: (state, fieldPath, value) => {
420
- const newValues = setNestedValue(state.values, fieldPath, value)
421
- const newDirtyFields = recalculateDirtySubtree(
422
- state.dirtyFields,
423
- state.initialValues,
424
- newValues,
425
- fieldPath,
426
- )
378
+ const newValues = setNestedValue(state.values, fieldPath, value);
379
+ const newDirtyFields = recalculateDirtySubtree(state.dirtyFields, state.initialValues, newValues, fieldPath);
427
380
  return {
428
381
  ...state,
429
382
  values: newValues as Field.EncodedFromFields<TFields>,
430
383
  dirtyFields: newDirtyFields,
431
- }
384
+ };
432
385
  },
433
386
 
434
387
  setFormValues: (state, values) => {
435
- const newDirtyFields = recalculateDirtySubtree(
436
- state.dirtyFields,
437
- state.initialValues,
438
- values,
439
- "",
440
- )
388
+ const newDirtyFields = recalculateDirtySubtree(state.dirtyFields, state.initialValues, values, "");
441
389
  return {
442
390
  ...state,
443
391
  values,
444
392
  dirtyFields: newDirtyFields,
445
- }
393
+ };
446
394
  },
447
395
 
448
396
  setFieldTouched: (state, fieldPath, touched) => ({
449
397
  ...state,
450
- touched: setNestedValue(state.touched, fieldPath, touched) as { readonly [K in keyof TFields]: boolean },
398
+ touched: setNestedValue(state.touched, fieldPath, touched) as { readonly [K in keyof TFields]: boolean; },
451
399
  }),
452
400
 
453
401
  appendArrayItem: (state, arrayPath, itemSchema, value) => {
454
- const newItem = value ?? Field.getDefaultFromSchema(itemSchema)
455
- const currentItems = (getNestedValue(state.values, arrayPath) ?? []) as ReadonlyArray<unknown>
456
- const newItems = [...currentItems, newItem]
402
+ const newItem = value ?? Field.getDefaultFromSchema(itemSchema);
403
+ const currentItems = (getNestedValue(state.values, arrayPath) ?? []) as ReadonlyArray<unknown>;
404
+ const newItems = [...currentItems, newItem];
457
405
  return {
458
406
  ...state,
459
407
  values: setNestedValue(state.values, arrayPath, newItems) as Field.EncodedFromFields<TFields>,
460
408
  dirtyFields: recalculateDirtyFieldsForArray(state.dirtyFields, state.initialValues, arrayPath, newItems),
461
- }
409
+ };
462
410
  },
463
411
 
464
412
  removeArrayItem: (state, arrayPath, index) => {
465
- const currentItems = (getNestedValue(state.values, arrayPath) ?? []) as ReadonlyArray<unknown>
466
- const newItems = currentItems.filter((_, i) => i !== index)
413
+ const currentItems = (getNestedValue(state.values, arrayPath) ?? []) as ReadonlyArray<unknown>;
414
+ const newItems = currentItems.filter((_, i) => i !== index);
467
415
  return {
468
416
  ...state,
469
417
  values: setNestedValue(state.values, arrayPath, newItems) as Field.EncodedFromFields<TFields>,
470
418
  dirtyFields: recalculateDirtyFieldsForArray(state.dirtyFields, state.initialValues, arrayPath, newItems),
471
- }
419
+ };
472
420
  },
473
421
 
474
422
  swapArrayItems: (state, arrayPath, indexA, indexB) => {
475
- const currentItems = (getNestedValue(state.values, arrayPath) ?? []) as ReadonlyArray<unknown>
423
+ const currentItems = (getNestedValue(state.values, arrayPath) ?? []) as ReadonlyArray<unknown>;
476
424
  if (
477
- indexA < 0 || indexA >= currentItems.length ||
478
- indexB < 0 || indexB >= currentItems.length ||
425
+ indexA < 0 ||
426
+ indexA >= currentItems.length ||
427
+ indexB < 0 ||
428
+ indexB >= currentItems.length ||
479
429
  indexA === indexB
480
430
  ) {
481
- return state
431
+ return state;
482
432
  }
483
- const newItems = [...currentItems]
484
- const temp = newItems[indexA]
485
- newItems[indexA] = newItems[indexB]
486
- newItems[indexB] = temp
433
+ const newItems = [...currentItems];
434
+ const temp = newItems[indexA];
435
+ newItems[indexA] = newItems[indexB];
436
+ newItems[indexB] = temp;
487
437
  return {
488
438
  ...state,
489
439
  values: setNestedValue(state.values, arrayPath, newItems) as Field.EncodedFromFields<TFields>,
490
440
  dirtyFields: recalculateDirtyFieldsForArray(state.dirtyFields, state.initialValues, arrayPath, newItems),
491
- }
441
+ };
492
442
  },
493
443
 
494
444
  moveArrayItem: (state, arrayPath, fromIndex, toIndex) => {
495
- const currentItems = (getNestedValue(state.values, arrayPath) ?? []) as ReadonlyArray<unknown>
445
+ const currentItems = (getNestedValue(state.values, arrayPath) ?? []) as ReadonlyArray<unknown>;
496
446
  if (
497
- fromIndex < 0 || fromIndex >= currentItems.length ||
498
- toIndex < 0 || toIndex > currentItems.length ||
447
+ fromIndex < 0 ||
448
+ fromIndex >= currentItems.length ||
449
+ toIndex < 0 ||
450
+ toIndex > currentItems.length ||
499
451
  fromIndex === toIndex
500
452
  ) {
501
- return state
453
+ return state;
502
454
  }
503
- const newItems = [...currentItems]
504
- const [item] = newItems.splice(fromIndex, 1)
505
- newItems.splice(toIndex, 0, item)
455
+ const newItems = [...currentItems];
456
+ const [item] = newItems.splice(fromIndex, 1);
457
+ newItems.splice(toIndex, 0, item);
506
458
  return {
507
459
  ...state,
508
460
  values: setNestedValue(state.values, arrayPath, newItems) as Field.EncodedFromFields<TFields>,
509
461
  dirtyFields: recalculateDirtyFieldsForArray(state.dirtyFields, state.initialValues, arrayPath, newItems),
510
- }
462
+ };
511
463
  },
512
464
 
513
465
  revertToLastSubmit: (state) => {
514
466
  if (Option.isNone(state.lastSubmittedValues)) {
515
- return state
467
+ return state;
516
468
  }
517
469
 
518
- const lastEncoded = state.lastSubmittedValues.value.encoded
470
+ const lastEncoded = state.lastSubmittedValues.value.encoded;
519
471
  if (state.values === lastEncoded) {
520
- return state
472
+ return state;
521
473
  }
522
474
 
523
- const newDirtyFields = recalculateDirtySubtree(
524
- state.dirtyFields,
525
- state.initialValues,
526
- lastEncoded,
527
- "",
528
- )
475
+ const newDirtyFields = recalculateDirtySubtree(state.dirtyFields, state.initialValues, lastEncoded, "");
529
476
 
530
477
  return {
531
478
  ...state,
532
479
  values: lastEncoded,
533
480
  dirtyFields: newDirtyFields,
534
- }
481
+ };
482
+ },
483
+ };
484
+
485
+ const resetAtom = Atom.fnSync<void>()(
486
+ (_: void, get) => {
487
+ const state = get(stateAtom);
488
+ if (Option.isNone(state)) return;
489
+ get.set(stateAtom, Option.some(operations.createResetState(state.value)));
490
+ get.set(errorsAtom, new Map());
491
+ resetValidationAtoms(get);
492
+ get.set(submitAtom, Atom.Reset);
493
+ },
494
+ { initialValue: undefined as void },
495
+ ).pipe(Atom.setIdleTTL(0));
496
+
497
+ const revertToLastSubmitAtom = Atom.fnSync<void>()(
498
+ (_: void, get) => {
499
+ const state = get(stateAtom);
500
+ if (Option.isNone(state)) return;
501
+ get.set(stateAtom, Option.some(operations.revertToLastSubmit(state.value)));
502
+ get.set(errorsAtom, new Map());
535
503
  },
536
- }
537
-
538
- const resetAtom = Atom.fnSync<void>()((_: void, get) => {
539
- const state = get(stateAtom)
540
- if (Option.isNone(state)) return
541
- get.set(stateAtom, Option.some(operations.createResetState(state.value)))
542
- get.set(errorsAtom, new Map())
543
- resetValidationAtoms(get)
544
- get.set(submitAtom, Atom.Reset)
545
- }, { initialValue: undefined as void }).pipe(Atom.setIdleTTL(0))
546
-
547
- const revertToLastSubmitAtom = Atom.fnSync<void>()((_: void, get) => {
548
- const state = get(stateAtom)
549
- if (Option.isNone(state)) return
550
- get.set(stateAtom, Option.some(operations.revertToLastSubmit(state.value)))
551
- get.set(errorsAtom, new Map())
552
- }, { initialValue: undefined as void }).pipe(Atom.setIdleTTL(0))
553
-
554
- const setValuesAtom = Atom.fnSync<Field.EncodedFromFields<TFields>>()((_values, get) => {
555
- const state = get(stateAtom)
556
- if (Option.isNone(state)) return
557
- get.set(stateAtom, Option.some(operations.setFormValues(state.value, _values)))
558
- get.set(errorsAtom, new Map())
559
- }, { initialValue: undefined as void }).pipe(Atom.setIdleTTL(0))
560
-
561
- const setValueAtomsRegistry = createWeakRegistry<Atom.Writable<void, any>>()
504
+ { initialValue: undefined as void },
505
+ ).pipe(Atom.setIdleTTL(0));
506
+
507
+ const setValuesAtom = Atom.fnSync<Field.EncodedFromFields<TFields>>()(
508
+ (_values, get) => {
509
+ const state = get(stateAtom);
510
+ if (Option.isNone(state)) return;
511
+ get.set(stateAtom, Option.some(operations.setFormValues(state.value, _values)));
512
+ get.set(errorsAtom, new Map());
513
+ },
514
+ { initialValue: undefined as void },
515
+ ).pipe(Atom.setIdleTTL(0));
516
+
517
+ const setValueAtomsRegistry = createWeakRegistry<Atom.Writable<void, any>>();
562
518
 
563
519
  const setValue = <S>(field: FormBuilder.FieldRef<S>): Atom.Writable<void, S | ((prev: S) => S)> => {
564
- const cached = setValueAtomsRegistry.get(field.key)
565
- if (cached) return cached
520
+ const cached = setValueAtomsRegistry.get(field.key);
521
+ if (cached) return cached;
566
522
 
567
- const atom = Atom.fnSync<S | ((prev: S) => S)>()((update, get) => {
568
- const state = get(stateAtom)
569
- if (Option.isNone(state)) return
523
+ const atom = Atom.fnSync<S | ((prev: S) => S)>()(
524
+ (update, get) => {
525
+ const state = get(stateAtom);
526
+ if (Option.isNone(state)) return;
570
527
 
571
- const currentValue = getNestedValue(state.value.values, field.key) as S
572
- const newValue = typeof update === "function"
573
- ? (update as (prev: S) => S)(currentValue)
574
- : update
528
+ const currentValue = getNestedValue(state.value.values, field.key) as S;
529
+ const newValue = typeof update === "function" ? (update as (prev: S) => S)(currentValue) : update;
575
530
 
576
- get.set(stateAtom, Option.some(operations.setFieldValue(state.value, field.key, newValue)))
577
- // Don't clear errors - display logic handles showing/hiding based on source + validation state
578
- }, { initialValue: undefined as void }).pipe(Atom.setIdleTTL(0))
531
+ get.set(stateAtom, Option.some(operations.setFieldValue(state.value, field.key, newValue)));
532
+ // Don't clear errors - display logic handles showing/hiding based on source + validation state
533
+ },
534
+ { initialValue: undefined as void },
535
+ ).pipe(Atom.setIdleTTL(0));
579
536
 
580
- setValueAtomsRegistry.set(field.key, atom)
581
- return atom
582
- }
537
+ setValueAtomsRegistry.set(field.key, atom);
538
+ return atom;
539
+ };
583
540
 
584
541
  const getFieldAtom = <S>(field: FormBuilder.FieldRef<S>): Atom.Atom<Option.Option<S>> => {
585
- const existing = publicFieldAtomRegistry.get(field.key)
586
- if (existing) return existing as Atom.Atom<Option.Option<S>>
542
+ const existing = publicFieldAtomRegistry.get(field.key);
543
+ if (existing) return existing as Atom.Atom<Option.Option<S>>;
587
544
 
588
545
  const safeAtom = Atom.readable((get) =>
589
546
  Option.map(get(stateAtom), (state) => getNestedValue(state.values, field.key) as S)
590
- ).pipe(Atom.setIdleTTL(0))
547
+ ).pipe(Atom.setIdleTTL(0));
591
548
 
592
- publicFieldAtomRegistry.set(field.key, safeAtom)
593
- return safeAtom
594
- }
549
+ publicFieldAtomRegistry.set(field.key, safeAtom);
550
+ return safeAtom;
551
+ };
595
552
 
596
553
  const mountAtom = Atom.readable((get) => {
597
- get(stateAtom)
598
- get(errorsAtom)
599
- get(submitAtom)
600
- }).pipe(Atom.setIdleTTL(0))
554
+ get(stateAtom);
555
+ get(errorsAtom);
556
+ get(submitAtom);
557
+ }).pipe(Atom.setIdleTTL(0));
601
558
 
602
- const keepAliveActiveAtom = Atom.make(false).pipe(Atom.setIdleTTL(0))
559
+ const keepAliveActiveAtom = Atom.make(false).pipe(Atom.setIdleTTL(0));
603
560
 
604
561
  return {
605
562
  stateAtom,
@@ -628,5 +585,5 @@ export const make = <TFields extends Field.FieldsRecord, R, A, E, SubmitArgs = v
628
585
  getFieldAtom,
629
586
  mountAtom,
630
587
  keepAliveActiveAtom,
631
- } as FormAtoms<TFields, R, A, E, SubmitArgs>
632
- }
588
+ } as FormAtoms<TFields, R, A, E, SubmitArgs>;
589
+ };