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