@lucas-barake/effect-form-react 0.14.0 → 0.16.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/src/FormReact.tsx CHANGED
@@ -5,65 +5,65 @@ import {
5
5
  useAtomSet,
6
6
  useAtomSubscribe,
7
7
  useAtomValue,
8
- } from "@effect-atom/atom-react"
9
- import * as Atom from "@effect-atom/atom/Atom"
10
- import { Field, FormAtoms, Mode, Validation } from "@lucas-barake/effect-form"
11
- import type * as FormBuilder from "@lucas-barake/effect-form/FormBuilder"
12
- import { getNestedValue, isPathOrParentDirty } from "@lucas-barake/effect-form/Path"
13
- import * as Cause from "effect/Cause"
14
- import type * as Effect from "effect/Effect"
15
- import * as Layer from "effect/Layer"
16
- import * as Option from "effect/Option"
17
- import * as ParseResult from "effect/ParseResult"
18
- import type * as Schema from "effect/Schema"
19
- import * as AST from "effect/SchemaAST"
20
- import * as React from "react"
21
- import { createContext, useContext } from "react"
22
- import { useDebounced } from "./internal/use-debounced.js"
23
-
24
- export type FieldValue<T> = T extends Schema.Schema.Any ? Schema.Schema.Encoded<T> : T
8
+ } from "@effect-atom/atom-react";
9
+ import * as Atom from "@effect-atom/atom/Atom";
10
+ import { Field, FormAtoms, Mode, Validation } from "@lucas-barake/effect-form";
11
+ import type * as FormBuilder from "@lucas-barake/effect-form/FormBuilder";
12
+ import { getNestedValue } from "@lucas-barake/effect-form/Path";
13
+ import * as Cause from "effect/Cause";
14
+ import type * as Effect from "effect/Effect";
15
+ import * as Layer from "effect/Layer";
16
+ import * as Option from "effect/Option";
17
+ import * as ParseResult from "effect/ParseResult";
18
+ import type * as Schema from "effect/Schema";
19
+ import * as AST from "effect/SchemaAST";
20
+ import * as React from "react";
21
+ import { createContext, useContext } from "react";
22
+ import { useDebounced } from "./internal/use-debounced.js";
23
+
24
+ export type FieldValue<T> = T extends Schema.Schema.Any ? Schema.Schema.Encoded<T> : T;
25
25
 
26
26
  export interface FieldState<E> {
27
- readonly value: E
28
- readonly onChange: (value: E) => void
29
- readonly onBlur: () => void
30
- readonly error: Option.Option<string>
31
- readonly isTouched: boolean
32
- readonly isValidating: boolean
33
- readonly isDirty: boolean
27
+ readonly value: E;
28
+ readonly onChange: (value: E) => void;
29
+ readonly onBlur: () => void;
30
+ readonly error: Option.Option<string>;
31
+ readonly isTouched: boolean;
32
+ readonly isValidating: boolean;
33
+ readonly isDirty: boolean;
34
34
  }
35
35
 
36
36
  export interface FieldComponentProps<E, P = Record<string, never>> {
37
- readonly field: FieldState<E>
38
- readonly props: P
37
+ readonly field: FieldState<E>;
38
+ readonly props: P;
39
39
  }
40
40
 
41
- export type FieldComponent<T, P = Record<string, never>> = React.FC<FieldComponentProps<FieldValue<T>, P>>
41
+ export type FieldComponent<T, P = Record<string, never>> = React.FC<FieldComponentProps<FieldValue<T>, P>>;
42
42
 
43
- export type ExtractExtraProps<C> = C extends React.FC<FieldComponentProps<any, infer P>> ? P : Record<string, never>
43
+ export type ExtractExtraProps<C> = C extends React.FC<FieldComponentProps<any, infer P>> ? P : Record<string, never>;
44
44
 
45
45
  export type ArrayItemComponentMap<S extends Schema.Schema.Any> = S extends Schema.Struct<infer Fields> ? {
46
46
  readonly [K in keyof Fields]: Fields[K] extends Schema.Schema.Any
47
47
  ? React.FC<FieldComponentProps<Schema.Schema.Encoded<Fields[K]>, any>>
48
- : never
48
+ : never;
49
49
  }
50
- : React.FC<FieldComponentProps<Schema.Schema.Encoded<S>, any>>
50
+ : React.FC<FieldComponentProps<Schema.Schema.Encoded<S>, any>>;
51
51
 
52
52
  export type FieldComponentMap<TFields extends Field.FieldsRecord> = {
53
53
  readonly [K in keyof TFields]: TFields[K] extends Field.FieldDef<any, infer S>
54
54
  ? React.FC<FieldComponentProps<Schema.Schema.Encoded<S>, any>>
55
55
  : TFields[K] extends Field.ArrayFieldDef<any, infer S> ? ArrayItemComponentMap<S>
56
- : never
57
- }
56
+ : never;
57
+ };
58
58
 
59
- export type FieldRefs<TFields extends Field.FieldsRecord> = FormAtoms.FieldRefs<TFields>
59
+ export type FieldRefs<TFields extends Field.FieldsRecord> = FormAtoms.FieldRefs<TFields>;
60
60
 
61
61
  export interface ArrayFieldOperations<TItem> {
62
- readonly items: ReadonlyArray<TItem>
63
- readonly append: (value?: TItem) => void
64
- readonly remove: (index: number) => void
65
- readonly swap: (indexA: number, indexB: number) => void
66
- readonly move: (from: number, to: number) => void
62
+ readonly items: ReadonlyArray<TItem>;
63
+ readonly append: (value?: TItem) => void;
64
+ readonly remove: (index: number) => void;
65
+ readonly swap: (indexA: number, indexB: number) => void;
66
+ readonly move: (from: number, to: number) => void;
67
67
  }
68
68
 
69
69
  export type BuiltForm<
@@ -74,74 +74,74 @@ export type BuiltForm<
74
74
  SubmitArgs = void,
75
75
  CM extends FieldComponentMap<TFields> = FieldComponentMap<TFields>,
76
76
  > = {
77
- readonly values: Atom.Atom<Option.Option<Field.EncodedFromFields<TFields>>>
78
- readonly isDirty: Atom.Atom<boolean>
79
- readonly hasChangedSinceSubmit: Atom.Atom<boolean>
80
- readonly lastSubmittedValues: Atom.Atom<Option.Option<FormBuilder.SubmittedValues<TFields>>>
81
- readonly submitCount: Atom.Atom<number>
77
+ readonly values: Atom.Atom<Option.Option<Field.EncodedFromFields<TFields>>>;
78
+ readonly isDirty: Atom.Atom<boolean>;
79
+ readonly hasChangedSinceSubmit: Atom.Atom<boolean>;
80
+ readonly lastSubmittedValues: Atom.Atom<Option.Option<FormBuilder.SubmittedValues<TFields>>>;
81
+ readonly submitCount: Atom.Atom<number>;
82
82
 
83
- readonly schema: Schema.Schema<Field.DecodedFromFields<TFields>, Field.EncodedFromFields<TFields>, R>
84
- readonly fields: FieldRefs<TFields>
83
+ readonly schema: Schema.Schema<Field.DecodedFromFields<TFields>, Field.EncodedFromFields<TFields>, R>;
84
+ readonly fields: FieldRefs<TFields>;
85
85
 
86
86
  readonly Initialize: React.FC<{
87
- readonly defaultValues: Field.EncodedFromFields<TFields>
88
- readonly children: React.ReactNode
89
- }>
90
-
91
- readonly submit: Atom.AtomResultFn<SubmitArgs, A, E | ParseResult.ParseError>
92
- readonly reset: Atom.Writable<void, void>
93
- readonly revertToLastSubmit: Atom.Writable<void, void>
94
- readonly setValues: Atom.Writable<void, Field.EncodedFromFields<TFields>>
95
- readonly setValue: <S>(field: FormBuilder.FieldRef<S>) => Atom.Writable<void, S | ((prev: S) => S)>
96
- readonly getFieldAtom: <S>(field: FormBuilder.FieldRef<S>) => Atom.Atom<Option.Option<S>>
97
-
98
- readonly mount: Atom.Atom<void>
99
- readonly KeepAlive: React.FC
100
- } & FieldComponents<TFields, CM>
87
+ readonly defaultValues: Field.EncodedFromFields<TFields>;
88
+ readonly children: React.ReactNode;
89
+ }>;
90
+
91
+ readonly submit: Atom.AtomResultFn<SubmitArgs, A, E | ParseResult.ParseError>;
92
+ readonly reset: Atom.Writable<void, void>;
93
+ readonly revertToLastSubmit: Atom.Writable<void, void>;
94
+ readonly setValues: Atom.Writable<void, Field.EncodedFromFields<TFields>>;
95
+ readonly setValue: <S>(field: FormBuilder.FieldRef<S>) => Atom.Writable<void, S | ((prev: S) => S)>;
96
+ readonly getFieldAtom: <S>(field: FormBuilder.FieldRef<S>) => Atom.Atom<Option.Option<S>>;
97
+ readonly getFieldIsDirty: (field: FormBuilder.FieldRef<any>) => Atom.Atom<boolean>;
98
+
99
+ readonly mount: Atom.Atom<void>;
100
+ readonly KeepAlive: React.FC;
101
+ } & FieldComponents<TFields, CM>;
101
102
 
102
103
  type FieldComponents<TFields extends Field.FieldsRecord, CM extends FieldComponentMap<TFields>> = {
103
104
  readonly [K in keyof TFields]: TFields[K] extends Field.FieldDef<any, any> ? React.FC<ExtractExtraProps<CM[K]>>
104
105
  : TFields[K] extends Field.ArrayFieldDef<any, infer S>
105
106
  ? ArrayFieldComponent<S, ExtractArrayItemExtraProps<CM[K], S>>
106
- : never
107
- }
107
+ : never;
108
+ };
108
109
 
109
110
  type ExtractArrayItemExtraProps<CM, S extends Schema.Schema.Any> = S extends Schema.Struct<infer Fields>
110
- ? { readonly [K in keyof Fields]: CM extends { readonly [P in K]: infer C } ? ExtractExtraProps<C> : never }
111
+ ? { readonly [K in keyof Fields]: CM extends { readonly [P in K]: infer C; } ? ExtractExtraProps<C> : never; }
111
112
  : CM extends React.FC<FieldComponentProps<any, infer P>> ? P
112
- : never
113
+ : never;
113
114
 
114
115
  type ArrayFieldComponent<S extends Schema.Schema.Any, ExtraPropsMap> =
115
116
  & React.FC<{
116
- readonly children: (ops: ArrayFieldOperations<Schema.Schema.Encoded<S>>) => React.ReactNode
117
+ readonly children: (ops: ArrayFieldOperations<Schema.Schema.Encoded<S>>) => React.ReactNode;
117
118
  }>
118
119
  & {
119
120
  readonly Item: React.FC<{
120
- readonly index: number
121
- readonly children: React.ReactNode | ((props: { readonly remove: () => void }) => React.ReactNode)
122
- }>
121
+ readonly index: number;
122
+ readonly children: React.ReactNode | ((props: { readonly remove: () => void; }) => React.ReactNode);
123
+ }>;
123
124
  }
124
125
  & (S extends Schema.Struct<infer Fields> ? {
125
126
  readonly [K in keyof Fields]: React.FC<
126
- ExtraPropsMap extends { readonly [P in K]: infer EP } ? EP : Record<string, never>
127
- >
127
+ ExtraPropsMap extends { readonly [P in K]: infer EP; } ? EP : Record<string, never>
128
+ >;
128
129
  }
129
- : unknown)
130
+ : unknown);
130
131
 
131
132
  interface ArrayItemContextValue {
132
- readonly index: number
133
- readonly parentPath: string
133
+ readonly index: number;
134
+ readonly parentPath: string;
134
135
  }
135
136
 
136
- const ArrayItemContext = createContext<ArrayItemContextValue | null>(null)
137
- const AutoSubmitContext = createContext<(() => void) | null>(null)
137
+ const ArrayItemContext = createContext<ArrayItemContextValue | null>(null);
138
+ const AutoSubmitContext = createContext<(() => void) | null>(null);
138
139
 
139
140
  const makeFieldComponent = <S extends Schema.Schema.Any, P>(
140
141
  fieldKey: string,
141
142
  fieldDef: Field.FieldDef<string, S>,
142
143
  errorsAtom: Atom.Writable<Map<string, Validation.ErrorEntry>, Map<string, Validation.ErrorEntry>>,
143
144
  submitCountAtom: Atom.Atom<number>,
144
- dirtyFieldsAtom: Atom.Atom<ReadonlySet<string>>,
145
145
  parsedMode: Mode.ParsedMode,
146
146
  getOrCreateValidationAtom: (
147
147
  fieldPath: string,
@@ -151,121 +151,116 @@ const makeFieldComponent = <S extends Schema.Schema.Any, P>(
151
151
  Component: React.FC<FieldComponentProps<Schema.Schema.Encoded<S>, P>>,
152
152
  ): React.FC<P> => {
153
153
  const FieldComponent: React.FC<P> = (extraProps) => {
154
- const arrayCtx = useContext(ArrayItemContext)
155
- const autoSubmitOnBlur = useContext(AutoSubmitContext)
156
- const fieldPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey
154
+ const arrayCtx = useContext(ArrayItemContext);
155
+ const autoSubmitOnBlur = useContext(AutoSubmitContext);
156
+ const fieldPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey;
157
157
 
158
- const { errorAtom, touchedAtom, valueAtom } = React.useMemo(
158
+ const { errorAtom, isDirtyAtom, touchedAtom, valueAtom } = React.useMemo(
159
159
  () => getOrCreateFieldAtoms(fieldPath),
160
160
  [fieldPath],
161
- )
161
+ );
162
162
 
163
- const [value, setValue] = useAtom(valueAtom) as [Schema.Schema.Encoded<S>, (v: unknown) => void]
164
- const [isTouched, setTouched] = useAtom(touchedAtom)
165
- const storedError = useAtomValue(errorAtom)
166
- const submitCount = useAtomValue(submitCountAtom)
163
+ const [value, setValue] = useAtom(valueAtom) as [Schema.Schema.Encoded<S>, (v: unknown) => void];
164
+ const [isTouched, setTouched] = useAtom(touchedAtom);
165
+ const storedError = useAtomValue(errorAtom);
166
+ const submitCount = useAtomValue(submitCountAtom);
167
167
 
168
- const validationAtom = React.useMemo(
169
- () => getOrCreateValidationAtom(fieldPath, fieldDef.schema),
170
- [fieldPath],
171
- )
172
- const validationResult = useAtomValue(validationAtom)
173
- const validateImmediate = useAtomSet(validationAtom)
168
+ const validationAtom = React.useMemo(() => getOrCreateValidationAtom(fieldPath, fieldDef.schema), [fieldPath]);
169
+ const validationResult = useAtomValue(validationAtom);
170
+ const validateImmediate = useAtomSet(validationAtom);
174
171
 
175
- const shouldDebounceValidation = parsedMode.validation === "onChange"
176
- && parsedMode.debounce !== null
177
- && !parsedMode.autoSubmit
178
- const validate = useDebounced(validateImmediate, shouldDebounceValidation ? parsedMode.debounce : null)
172
+ const shouldDebounceValidation = parsedMode.validation === "onChange" && parsedMode.debounce !== null &&
173
+ !parsedMode.autoSubmit;
174
+ const validate = useDebounced(validateImmediate, shouldDebounceValidation ? parsedMode.debounce : null);
179
175
 
180
- const prevValueRef = React.useRef(value)
176
+ const prevValueRef = React.useRef(value);
181
177
  React.useEffect(() => {
182
178
  if (prevValueRef.current === value) {
183
- return
179
+ return;
184
180
  }
185
- prevValueRef.current = value
181
+ prevValueRef.current = value;
186
182
 
187
- const shouldValidate = parsedMode.validation === "onChange"
188
- || (parsedMode.validation === "onBlur" && isTouched)
189
- || (parsedMode.validation === "onSubmit" && submitCount > 0)
183
+ const shouldValidate = parsedMode.validation === "onChange" ||
184
+ (parsedMode.validation === "onBlur" && isTouched) ||
185
+ (parsedMode.validation === "onSubmit" && submitCount > 0);
190
186
 
191
187
  if (shouldValidate) {
192
- validate(value)
188
+ validate(value);
193
189
  }
194
- }, [value, isTouched, submitCount, validate])
190
+ }, [value, isTouched, submitCount, validate]);
195
191
 
196
192
  const livePerFieldError: Option.Option<string> = React.useMemo(() => {
197
193
  if (validationResult._tag === "Failure") {
198
- const parseError = Cause.failureOption(validationResult.cause)
194
+ const parseError = Cause.failureOption(validationResult.cause);
199
195
  if (Option.isSome(parseError) && ParseResult.isParseError(parseError.value)) {
200
- return Validation.extractFirstError(parseError.value)
196
+ return Validation.extractFirstError(parseError.value);
201
197
  }
202
198
  }
203
- return Option.none()
204
- }, [validationResult])
199
+ return Option.none();
200
+ }, [validationResult]);
205
201
 
206
- const isValidating = validationResult.waiting
202
+ const isValidating = validationResult.waiting;
207
203
 
208
204
  const validationError: Option.Option<string> = React.useMemo(() => {
209
205
  if (Option.isSome(livePerFieldError)) {
210
- return livePerFieldError
206
+ return livePerFieldError;
211
207
  }
212
208
 
213
209
  if (Option.isSome(storedError)) {
214
210
  // Hide field-sourced errors when validation passes or is pending (async gap).
215
211
  // Refinement errors persist until re-submit - they can't be cleared by typing.
216
212
  const shouldHideStoredError = storedError.value.source === "field" &&
217
- (validationResult._tag === "Success" || isValidating)
213
+ (validationResult._tag === "Success" || isValidating);
218
214
 
219
215
  if (shouldHideStoredError) {
220
- return Option.none()
216
+ return Option.none();
221
217
  }
222
- return Option.some(storedError.value.message)
218
+ return Option.some(storedError.value.message);
223
219
  }
224
220
 
225
- return Option.none()
226
- }, [livePerFieldError, storedError, validationResult, isValidating])
221
+ return Option.none();
222
+ }, [livePerFieldError, storedError, validationResult, isValidating]);
227
223
 
228
224
  const onChange = React.useCallback(
229
225
  (newValue: Schema.Schema.Encoded<S>) => {
230
- setValue(newValue)
226
+ setValue(newValue);
231
227
  },
232
228
  [setValue],
233
- )
229
+ );
234
230
 
235
231
  const onBlur = React.useCallback(() => {
236
- setTouched(true)
232
+ setTouched(true);
237
233
  if (parsedMode.validation === "onBlur") {
238
- validate(value)
234
+ validate(value);
239
235
  }
240
- autoSubmitOnBlur?.()
241
- }, [setTouched, validate, value, autoSubmitOnBlur])
242
-
243
- const dirtyFields = useAtomValue(dirtyFieldsAtom)
244
- const isDirty = React.useMemo(
245
- () => isPathOrParentDirty(dirtyFields, fieldPath),
246
- [dirtyFields, fieldPath],
247
- )
236
+ autoSubmitOnBlur?.();
237
+ }, [setTouched, validate, value, autoSubmitOnBlur]);
238
+
239
+ const isDirty = useAtomValue(isDirtyAtom);
248
240
  const shouldShowError = parsedMode.validation === "onChange"
249
- ? (isDirty || submitCount > 0)
241
+ ? isDirty || submitCount > 0
250
242
  : parsedMode.validation === "onBlur"
251
- ? (isTouched || submitCount > 0)
252
- : submitCount > 0
253
-
254
- const fieldState: FieldState<Schema.Schema.Encoded<S>> = React.useMemo(() => ({
255
- value,
256
- onChange,
257
- onBlur,
258
- error: shouldShowError ? validationError : Option.none<string>(),
259
- isTouched,
260
- isValidating,
261
- isDirty,
262
- }), [value, onChange, onBlur, shouldShowError, validationError, isTouched, isValidating, isDirty])
263
-
264
- return <Component field={fieldState} props={extraProps} />
265
- }
266
-
267
- return React.memo(FieldComponent) as React.FC<P>
268
- }
243
+ ? isTouched || submitCount > 0
244
+ : submitCount > 0;
245
+
246
+ const fieldState: FieldState<Schema.Schema.Encoded<S>> = React.useMemo(
247
+ () => ({
248
+ value,
249
+ onChange,
250
+ onBlur,
251
+ error: shouldShowError ? validationError : Option.none<string>(),
252
+ isTouched,
253
+ isValidating,
254
+ isDirty,
255
+ }),
256
+ [value, onChange, onBlur, shouldShowError, validationError, isTouched, isValidating, isDirty],
257
+ );
258
+
259
+ return <Component field={fieldState} props={extraProps} />;
260
+ };
261
+
262
+ return React.memo(FieldComponent) as React.FC<P>;
263
+ };
269
264
 
270
265
  const makeArrayFieldComponent = <S extends Schema.Schema.Any>(
271
266
  fieldKey: string,
@@ -283,130 +278,126 @@ const makeArrayFieldComponent = <S extends Schema.Schema.Any>(
283
278
  operations: FormAtoms.FormOperations<any>,
284
279
  componentMap: ArrayItemComponentMap<S>,
285
280
  ): ArrayFieldComponent<S, any> => {
286
- const isStructSchema = AST.isTypeLiteral(def.itemSchema.ast)
281
+ const isStructSchema = AST.isTypeLiteral(def.itemSchema.ast);
287
282
 
288
283
  const ArrayWrapper: React.FC<{
289
- readonly children: (ops: ArrayFieldOperations<Schema.Schema.Encoded<S>>) => React.ReactNode
284
+ readonly children: (ops: ArrayFieldOperations<Schema.Schema.Encoded<S>>) => React.ReactNode;
290
285
  }> = ({ children }) => {
291
- const arrayCtx = useContext(ArrayItemContext)
292
- const [formStateOption, setFormState] = useAtom(stateAtom)
293
- const formState = Option.getOrThrow(formStateOption)
286
+ const arrayCtx = useContext(ArrayItemContext);
287
+ const [formStateOption, setFormState] = useAtom(stateAtom);
288
+ const formState = Option.getOrThrow(formStateOption);
294
289
 
295
- const fieldPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey
290
+ const fieldPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey;
296
291
  const items = React.useMemo(
297
292
  () => (getNestedValue(formState.values, fieldPath) ?? []) as ReadonlyArray<Schema.Schema.Encoded<S>>,
298
293
  [formState.values, fieldPath],
299
- )
294
+ );
300
295
 
301
296
  const append = React.useCallback(
302
297
  (value?: Schema.Schema.Encoded<S>) => {
303
298
  setFormState((prev) => {
304
- if (Option.isNone(prev)) return prev
305
- return Option.some(operations.appendArrayItem(prev.value, fieldPath, def.itemSchema, value))
306
- })
299
+ if (Option.isNone(prev)) return prev;
300
+ return Option.some(operations.appendArrayItem(prev.value, fieldPath, def.itemSchema, value));
301
+ });
307
302
  },
308
303
  [fieldPath, setFormState],
309
- )
304
+ );
310
305
 
311
306
  const remove = React.useCallback(
312
307
  (index: number) => {
313
308
  setFormState((prev) => {
314
- if (Option.isNone(prev)) return prev
315
- return Option.some(operations.removeArrayItem(prev.value, fieldPath, index))
316
- })
309
+ if (Option.isNone(prev)) return prev;
310
+ return Option.some(operations.removeArrayItem(prev.value, fieldPath, index));
311
+ });
317
312
  },
318
313
  [fieldPath, setFormState],
319
- )
314
+ );
320
315
 
321
316
  const swap = React.useCallback(
322
317
  (indexA: number, indexB: number) => {
323
318
  setFormState((prev) => {
324
- if (Option.isNone(prev)) return prev
325
- return Option.some(operations.swapArrayItems(prev.value, fieldPath, indexA, indexB))
326
- })
319
+ if (Option.isNone(prev)) return prev;
320
+ return Option.some(operations.swapArrayItems(prev.value, fieldPath, indexA, indexB));
321
+ });
327
322
  },
328
323
  [fieldPath, setFormState],
329
- )
324
+ );
330
325
 
331
326
  const move = React.useCallback(
332
327
  (from: number, to: number) => {
333
328
  setFormState((prev) => {
334
- if (Option.isNone(prev)) return prev
335
- return Option.some(operations.moveArrayItem(prev.value, fieldPath, from, to))
336
- })
329
+ if (Option.isNone(prev)) return prev;
330
+ return Option.some(operations.moveArrayItem(prev.value, fieldPath, from, to));
331
+ });
337
332
  },
338
333
  [fieldPath, setFormState],
339
- )
334
+ );
340
335
 
341
- return <>{children({ items, append, remove, swap, move })}</>
342
- }
336
+ return <>{children({ items, append, remove, swap, move })}</>;
337
+ };
343
338
 
344
339
  const ItemWrapper: React.FC<{
345
- readonly index: number
346
- readonly children: React.ReactNode | ((props: { readonly remove: () => void }) => React.ReactNode)
340
+ readonly index: number;
341
+ readonly children: React.ReactNode | ((props: { readonly remove: () => void; }) => React.ReactNode);
347
342
  }> = ({ children, index }) => {
348
- const arrayCtx = useContext(ArrayItemContext)
349
- const setFormState = useAtomSet(stateAtom)
343
+ const arrayCtx = useContext(ArrayItemContext);
344
+ const setFormState = useAtomSet(stateAtom);
350
345
 
351
- const parentPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey
352
- const itemPath = `${parentPath}[${index}]`
346
+ const parentPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey;
347
+ const itemPath = `${parentPath}[${index}]`;
353
348
 
354
349
  const remove = React.useCallback(() => {
355
350
  setFormState((prev) => {
356
- if (Option.isNone(prev)) return prev
357
- return Option.some(operations.removeArrayItem(prev.value, parentPath, index))
358
- })
359
- }, [parentPath, index, setFormState])
351
+ if (Option.isNone(prev)) return prev;
352
+ return Option.some(operations.removeArrayItem(prev.value, parentPath, index));
353
+ });
354
+ }, [parentPath, index, setFormState]);
360
355
 
361
356
  return (
362
357
  <ArrayItemContext.Provider value={{ index, parentPath: itemPath }}>
363
358
  {typeof children === "function" ? children({ remove }) : children}
364
359
  </ArrayItemContext.Provider>
365
- )
366
- }
360
+ );
361
+ };
367
362
 
368
- const itemFieldComponents: Record<string, React.FC> = {}
363
+ const itemFieldComponents: Record<string, React.FC> = {};
369
364
 
370
365
  if (isStructSchema) {
371
- const ast = def.itemSchema.ast as AST.TypeLiteral
366
+ const ast = def.itemSchema.ast as AST.TypeLiteral;
372
367
  for (const prop of ast.propertySignatures) {
373
- const itemKey = prop.name as string
374
- const itemSchema = { ast: prop.type } as Schema.Schema.Any
375
- const itemDef = Field.makeField(itemKey, itemSchema)
376
- const itemComponent = (componentMap as Record<string, React.FC<FieldComponentProps<any, any>>>)[itemKey]
368
+ const itemKey = prop.name as string;
369
+ const itemSchema = { ast: prop.type } as Schema.Schema.Any;
370
+ const itemDef = Field.makeField(itemKey, itemSchema);
371
+ const itemComponent = (componentMap as Record<string, React.FC<FieldComponentProps<any, any>>>)[itemKey];
377
372
  itemFieldComponents[itemKey] = makeFieldComponent(
378
373
  itemKey,
379
374
  itemDef,
380
375
  errorsAtom,
381
376
  submitCountAtom,
382
- dirtyFieldsAtom,
383
377
  parsedMode,
384
378
  getOrCreateValidationAtom,
385
379
  getOrCreateFieldAtoms,
386
380
  itemComponent,
387
- )
381
+ );
388
382
  }
389
383
  }
390
384
 
391
385
  const properties: Record<string, unknown> = {
392
386
  Item: ItemWrapper,
393
387
  ...itemFieldComponents,
394
- }
388
+ };
395
389
 
396
390
  return new Proxy(ArrayWrapper, {
397
391
  get(target, prop) {
398
392
  if (prop in properties) {
399
- return properties[prop as string]
393
+ return properties[prop as string];
400
394
  }
401
- return Reflect.get(target, prop)
395
+ return Reflect.get(target, prop);
402
396
  },
403
- }) as ArrayFieldComponent<S, any>
404
- }
397
+ }) as ArrayFieldComponent<S, any>;
398
+ };
405
399
 
406
- const makeFieldComponents = <
407
- TFields extends Field.FieldsRecord,
408
- CM extends FieldComponentMap<TFields>,
409
- >(
400
+ const makeFieldComponents = <TFields extends Field.FieldsRecord, CM extends FieldComponentMap<TFields>>(
410
401
  fields: TFields,
411
402
  stateAtom: Atom.Writable<
412
403
  Option.Option<FormBuilder.FormState<TFields>>,
@@ -424,11 +415,11 @@ const makeFieldComponents = <
424
415
  operations: FormAtoms.FormOperations<TFields>,
425
416
  componentMap: CM,
426
417
  ): FieldComponents<TFields, CM> => {
427
- const components: Record<string, any> = {}
418
+ const components: Record<string, any> = {};
428
419
 
429
420
  for (const [key, def] of Object.entries(fields)) {
430
421
  if (Field.isArrayFieldDef(def)) {
431
- const arrayComponentMap = (componentMap as Record<string, any>)[key]
422
+ const arrayComponentMap = (componentMap as Record<string, any>)[key];
432
423
  components[key] = makeArrayFieldComponent(
433
424
  key,
434
425
  def as Field.ArrayFieldDef<string, Schema.Schema.Any>,
@@ -441,25 +432,24 @@ const makeFieldComponents = <
441
432
  getOrCreateFieldAtoms,
442
433
  operations,
443
434
  arrayComponentMap,
444
- )
435
+ );
445
436
  } else if (Field.isFieldDef(def)) {
446
- const fieldComponent = (componentMap as Record<string, React.FC<FieldComponentProps<any, any>>>)[key]
437
+ const fieldComponent = (componentMap as Record<string, React.FC<FieldComponentProps<any, any>>>)[key];
447
438
  components[key] = makeFieldComponent(
448
439
  key,
449
440
  def,
450
441
  errorsAtom,
451
442
  submitCountAtom,
452
- dirtyFieldsAtom,
453
443
  parsedMode,
454
444
  getOrCreateValidationAtom,
455
445
  getOrCreateFieldAtoms,
456
446
  fieldComponent,
457
- )
447
+ );
458
448
  }
459
449
  }
460
450
 
461
- return components as FieldComponents<TFields, CM>
462
- }
451
+ return components as FieldComponents<TFields, CM>;
452
+ };
463
453
 
464
454
  export const make: {
465
455
  <
@@ -471,19 +461,19 @@ export const make: {
471
461
  >(
472
462
  self: FormBuilder.FormBuilder<TFields, never>,
473
463
  options: {
474
- readonly runtime?: Atom.AtomRuntime<never, never>
475
- readonly fields: CM
476
- readonly mode?: SubmitArgs extends void ? Mode.FormMode : Mode.FormModeWithoutAutoSubmit
464
+ readonly runtime?: Atom.AtomRuntime<never, never>;
465
+ readonly fields: CM;
466
+ readonly mode?: SubmitArgs extends void ? Mode.FormMode : Mode.FormModeWithoutAutoSubmit;
477
467
  readonly onSubmit: (
478
468
  args: SubmitArgs,
479
469
  ctx: {
480
- readonly decoded: Field.DecodedFromFields<TFields>
481
- readonly encoded: Field.EncodedFromFields<TFields>
482
- readonly get: Atom.FnContext
470
+ readonly decoded: Field.DecodedFromFields<TFields>;
471
+ readonly encoded: Field.EncodedFromFields<TFields>;
472
+ readonly get: Atom.FnContext;
483
473
  },
484
- ) => A | Effect.Effect<A, E, never>
474
+ ) => A | Effect.Effect<A, E, never>;
485
475
  },
486
- ): BuiltForm<TFields, never, A, E, SubmitArgs, CM>
476
+ ): BuiltForm<TFields, never, A, E, SubmitArgs, CM>;
487
477
 
488
478
  <
489
479
  TFields extends Field.FieldsRecord,
@@ -496,30 +486,30 @@ export const make: {
496
486
  >(
497
487
  self: FormBuilder.FormBuilder<TFields, R>,
498
488
  options: {
499
- readonly runtime: Atom.AtomRuntime<R, ER>
500
- readonly fields: CM
501
- readonly mode?: SubmitArgs extends void ? Mode.FormMode : Mode.FormModeWithoutAutoSubmit
489
+ readonly runtime: Atom.AtomRuntime<R, ER>;
490
+ readonly fields: CM;
491
+ readonly mode?: SubmitArgs extends void ? Mode.FormMode : Mode.FormModeWithoutAutoSubmit;
502
492
  readonly onSubmit: (
503
493
  args: SubmitArgs,
504
494
  ctx: {
505
- readonly decoded: Field.DecodedFromFields<TFields>
506
- readonly encoded: Field.EncodedFromFields<TFields>
507
- readonly get: Atom.FnContext
495
+ readonly decoded: Field.DecodedFromFields<TFields>;
496
+ readonly encoded: Field.EncodedFromFields<TFields>;
497
+ readonly get: Atom.FnContext;
508
498
  },
509
- ) => A | Effect.Effect<A, E, R>
499
+ ) => A | Effect.Effect<A, E, R>;
510
500
  },
511
- ): BuiltForm<TFields, R, A, E, SubmitArgs, CM>
501
+ ): BuiltForm<TFields, R, A, E, SubmitArgs, CM>;
512
502
  } = (self: any, options: any): any => {
513
- const { fields: components, mode, onSubmit, runtime: providedRuntime } = options
514
- const runtime = providedRuntime ?? Atom.runtime(Layer.empty)
515
- const parsedMode = Mode.parse(mode)
516
- const { fields } = self
503
+ const { fields: components, mode, onSubmit, runtime: providedRuntime } = options;
504
+ const runtime = providedRuntime ?? Atom.runtime(Layer.empty);
505
+ const parsedMode = Mode.parse(mode);
506
+ const { fields } = self;
517
507
 
518
508
  const formAtoms = FormAtoms.make({
519
509
  formBuilder: self,
520
510
  runtime,
521
511
  onSubmit,
522
- })
512
+ });
523
513
 
524
514
  const {
525
515
  combinedSchema,
@@ -527,6 +517,7 @@ export const make: {
527
517
  errorsAtom,
528
518
  fieldRefs,
529
519
  getFieldAtom,
520
+ getFieldIsDirty,
530
521
  getOrCreateFieldAtoms,
531
522
  getOrCreateValidationAtom,
532
523
  hasChangedSinceSubmitAtom,
@@ -544,37 +535,40 @@ export const make: {
544
535
  submitAtom,
545
536
  submitCountAtom,
546
537
  valuesAtom,
547
- } = formAtoms
538
+ } = formAtoms;
548
539
 
549
540
  const InitializeComponent: React.FC<{
550
- readonly defaultValues: any
551
- readonly children: React.ReactNode
541
+ readonly defaultValues: any;
542
+ readonly children: React.ReactNode;
552
543
  }> = ({ children, defaultValues }) => {
553
- const registry = React.useContext(RegistryContext)
554
- const state = useAtomValue(stateAtom)
555
- const setFormState = useAtomSet(stateAtom)
556
- const callSubmit = useAtomSet(submitAtom)
557
- const isInitializedRef = React.useRef(false)
544
+ const registry = React.useContext(RegistryContext);
545
+ const state = useAtomValue(stateAtom);
546
+ const setFormState = useAtomSet(stateAtom);
547
+ const callSubmit = useAtomSet(submitAtom);
548
+ const isInitializedRef = React.useRef(false);
558
549
 
559
550
  React.useEffect(() => {
560
- const isKeptAlive = registry.get(keepAliveActiveAtom)
561
- const currentState = registry.get(stateAtom)
551
+ const isKeptAlive = registry.get(keepAliveActiveAtom);
552
+ const currentState = registry.get(stateAtom);
562
553
 
563
554
  if (!isKeptAlive) {
564
- setFormState(Option.some(operations.createInitialState(defaultValues)))
555
+ setFormState(Option.some(operations.createInitialState(defaultValues)));
565
556
  } else if (Option.isNone(currentState)) {
566
- setFormState(Option.some(operations.createInitialState(defaultValues)))
557
+ setFormState(Option.some(operations.createInitialState(defaultValues)));
567
558
  }
568
559
 
569
- isInitializedRef.current = true
560
+ isInitializedRef.current = true;
570
561
  // eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only
571
- }, [registry])
562
+ }, [registry]);
572
563
 
573
- const debouncedAutoSubmit = useDebounced(() => {
574
- const stateOption = registry.get(stateAtom)
575
- if (Option.isNone(stateOption)) return
576
- callSubmit(undefined)
577
- }, parsedMode.autoSubmit && parsedMode.validation === "onChange" ? parsedMode.debounce : null)
564
+ const debouncedAutoSubmit = useDebounced(
565
+ () => {
566
+ const stateOption = registry.get(stateAtom);
567
+ if (Option.isNone(stateOption)) return;
568
+ callSubmit(undefined);
569
+ },
570
+ parsedMode.autoSubmit && parsedMode.validation === "onChange" ? parsedMode.debounce : null,
571
+ );
578
572
 
579
573
  // ─────────────────────────────────────────────────────────────────────────────
580
574
  // Auto-Submit Coordination
@@ -587,75 +581,75 @@ export const make: {
587
581
  // metadata updates (submitCount, lastSubmittedValues).
588
582
  // ─────────────────────────────────────────────────────────────────────────────
589
583
 
590
- const lastValuesRef = React.useRef<unknown>(null)
591
- const pendingChangesRef = React.useRef(false)
592
- const wasSubmittingRef = React.useRef(false)
584
+ const lastValuesRef = React.useRef<unknown>(null);
585
+ const pendingChangesRef = React.useRef(false);
586
+ const wasSubmittingRef = React.useRef(false);
593
587
 
594
588
  useAtomSubscribe(
595
589
  stateAtom,
596
590
  React.useCallback(() => {
597
- if (!isInitializedRef.current) return
591
+ if (!isInitializedRef.current) return;
598
592
 
599
- const state = registry.get(stateAtom)
600
- if (Option.isNone(state)) return
601
- const currentValues = state.value.values
593
+ const state = registry.get(stateAtom);
594
+ if (Option.isNone(state)) return;
595
+ const currentValues = state.value.values;
602
596
 
603
597
  // Reference equality filters out submit metadata changes.
604
598
  // Works because setFieldValue creates new values object (immutable update).
605
- if (currentValues === lastValuesRef.current) return
606
- lastValuesRef.current = currentValues
599
+ if (currentValues === lastValuesRef.current) return;
600
+ lastValuesRef.current = currentValues;
607
601
 
608
- if (!parsedMode.autoSubmit || parsedMode.validation !== "onChange") return
602
+ if (!parsedMode.autoSubmit || parsedMode.validation !== "onChange") return;
609
603
 
610
- const submitResult = registry.get(submitAtom)
604
+ const submitResult = registry.get(submitAtom);
611
605
  if (submitResult.waiting) {
612
- pendingChangesRef.current = true
606
+ pendingChangesRef.current = true;
613
607
  } else {
614
- debouncedAutoSubmit()
608
+ debouncedAutoSubmit();
615
609
  }
616
610
  }, [debouncedAutoSubmit, registry]),
617
611
  { immediate: false },
618
- )
612
+ );
619
613
 
620
614
  useAtomSubscribe(
621
615
  submitAtom,
622
616
  React.useCallback(
623
617
  (result) => {
624
- if (!parsedMode.autoSubmit || parsedMode.validation !== "onChange") return
618
+ if (!parsedMode.autoSubmit || parsedMode.validation !== "onChange") return;
625
619
 
626
- const isSubmitting = result.waiting
627
- const wasSubmitting = wasSubmittingRef.current
628
- wasSubmittingRef.current = isSubmitting
620
+ const isSubmitting = result.waiting;
621
+ const wasSubmitting = wasSubmittingRef.current;
622
+ wasSubmittingRef.current = isSubmitting;
629
623
 
630
624
  // Flush queued changes when submit completes
631
625
  if (wasSubmitting && !isSubmitting) {
632
626
  if (pendingChangesRef.current) {
633
- pendingChangesRef.current = false
634
- debouncedAutoSubmit()
627
+ pendingChangesRef.current = false;
628
+ debouncedAutoSubmit();
635
629
  }
636
630
  }
637
631
  },
638
632
  [debouncedAutoSubmit],
639
633
  ),
640
634
  { immediate: false },
641
- )
635
+ );
642
636
 
643
637
  const onBlurAutoSubmit = React.useCallback(() => {
644
- if (!parsedMode.autoSubmit || parsedMode.validation !== "onBlur") return
638
+ if (!parsedMode.autoSubmit || parsedMode.validation !== "onBlur") return;
645
639
 
646
- const stateOption = registry.get(stateAtom)
647
- if (Option.isNone(stateOption)) return
640
+ const stateOption = registry.get(stateAtom);
641
+ if (Option.isNone(stateOption)) return;
648
642
 
649
- const { lastSubmittedValues, values } = stateOption.value
650
- if (Option.isSome(lastSubmittedValues) && values === lastSubmittedValues.value.encoded) return
643
+ const { lastSubmittedValues, values } = stateOption.value;
644
+ if (Option.isSome(lastSubmittedValues) && values === lastSubmittedValues.value.encoded) return;
651
645
 
652
- callSubmit(undefined)
653
- }, [registry, callSubmit])
646
+ callSubmit(undefined);
647
+ }, [registry, callSubmit]);
654
648
 
655
- if (Option.isNone(state)) return null
649
+ if (Option.isNone(state)) return null;
656
650
 
657
- return <AutoSubmitContext.Provider value={onBlurAutoSubmit}>{children}</AutoSubmitContext.Provider>
658
- }
651
+ return <AutoSubmitContext.Provider value={onBlurAutoSubmit}>{children}</AutoSubmitContext.Provider>;
652
+ };
659
653
 
660
654
  const fieldComponents = makeFieldComponents(
661
655
  fields,
@@ -668,19 +662,19 @@ export const make: {
668
662
  getOrCreateFieldAtoms,
669
663
  operations,
670
664
  components,
671
- )
665
+ );
672
666
 
673
667
  const KeepAlive: React.FC = () => {
674
- const setKeepAliveActive = useAtomSet(keepAliveActiveAtom)
668
+ const setKeepAliveActive = useAtomSet(keepAliveActiveAtom);
675
669
 
676
670
  React.useLayoutEffect(() => {
677
- setKeepAliveActive(true)
678
- return () => setKeepAliveActive(false)
679
- }, [setKeepAliveActive])
671
+ setKeepAliveActive(true);
672
+ return () => setKeepAliveActive(false);
673
+ }, [setKeepAliveActive]);
680
674
 
681
- useAtomMount(mountAtom)
682
- return null
683
- }
675
+ useAtomMount(mountAtom);
676
+ return null;
677
+ };
684
678
 
685
679
  return {
686
680
  values: valuesAtom,
@@ -698,8 +692,9 @@ export const make: {
698
692
  setValues: setValuesAtom,
699
693
  setValue,
700
694
  getFieldAtom,
695
+ getFieldIsDirty,
701
696
  mount: mountAtom,
702
697
  KeepAlive,
703
698
  ...fieldComponents,
704
- }
705
- }
699
+ };
700
+ };