@lucas-barake/effect-form-react 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/FormReact.tsx CHANGED
@@ -5,131 +5,67 @@ 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 * as Predicate from "effect/Predicate"
19
- import type * as Schema from "effect/Schema"
20
- import * as AST from "effect/SchemaAST"
21
- import * as React from "react"
22
- import { createContext, useContext } from "react"
23
- import { useDebounced } from "./internal/use-debounced.js"
24
-
25
- /**
26
- * Form-controlled state passed to field components.
27
- *
28
- * @category Models
29
- */
30
- export interface FieldState<S extends Schema.Schema.Any> {
31
- readonly value: Schema.Schema.Encoded<S>
32
- readonly onChange: (value: Schema.Schema.Encoded<S>) => void
33
- readonly onBlur: () => void
34
- readonly error: Option.Option<string>
35
- readonly isTouched: boolean
36
- readonly isValidating: boolean
37
- readonly isDirty: boolean
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;
38
34
  }
39
35
 
40
- /**
41
- * Props passed to field components.
42
- * Contains form-controlled state in `field` and user-defined props in `props`.
43
- *
44
- * @category Models
45
- */
46
- export interface FieldComponentProps<
47
- S extends Schema.Schema.Any,
48
- P extends Record<string, unknown> = Record<string, never>,
49
- > {
50
- readonly field: FieldState<S>
51
- readonly props: P
36
+ export interface FieldComponentProps<E, P = Record<string, never>> {
37
+ readonly field: FieldState<E>;
38
+ readonly props: P;
52
39
  }
53
40
 
54
- /**
55
- * A bundled field definition + component for reusable form fields.
56
- * Created with `FormReact.makeField`.
57
- *
58
- * @category Models
59
- */
60
- export interface FieldBundle<
61
- K extends string,
62
- S extends Schema.Schema.Any,
63
- P extends Record<string, unknown> = Record<string, never>,
64
- > {
65
- readonly _tag: "FieldBundle"
66
- readonly field: Field.FieldDef<K, S>
67
- readonly component: React.FC<FieldComponentProps<S, P>>
68
- }
41
+ export type FieldComponent<T, P = Record<string, never>> = React.FC<FieldComponentProps<FieldValue<T>, P>>;
42
+
43
+ export type ExtractExtraProps<C> = C extends React.FC<FieldComponentProps<any, infer P>> ? P : Record<string, never>;
69
44
 
70
- const isFieldBundle = (x: unknown): x is FieldBundle<string, Schema.Schema.Any, Record<string, unknown>> =>
71
- Predicate.isTagged(x, "FieldBundle")
72
-
73
- /**
74
- * Extracts the extra props type from a field component.
75
- *
76
- * @category Type-level utilities
77
- */
78
- export type ExtractExtraProps<C> = C extends React.FC<FieldComponentProps<any, infer P>> ? P
79
- : C extends FieldBundle<any, any, infer P> ? P
80
- : Record<string, never>
81
-
82
- /**
83
- * Extracts field component map for array item schemas.
84
- * - For Struct schemas: returns a map of field names to components
85
- * - For primitive schemas: returns a single component
86
- *
87
- * @category Models
88
- */
89
45
  export type ArrayItemComponentMap<S extends Schema.Schema.Any> = S extends Schema.Struct<infer Fields> ? {
90
- readonly [K in keyof Fields]: Fields[K] extends Schema.Schema.Any ? React.FC<FieldComponentProps<Fields[K], any>>
91
- : never
46
+ readonly [K in keyof Fields]: Fields[K] extends Schema.Schema.Any
47
+ ? React.FC<FieldComponentProps<Schema.Schema.Encoded<Fields[K]>, any>>
48
+ : never;
92
49
  }
93
- : React.FC<FieldComponentProps<S, any>>
50
+ : React.FC<FieldComponentProps<Schema.Schema.Encoded<S>, any>>;
94
51
 
95
- /**
96
- * Maps field names to their React components.
97
- *
98
- * @category Models
99
- */
100
52
  export type FieldComponentMap<TFields extends Field.FieldsRecord> = {
101
53
  readonly [K in keyof TFields]: TFields[K] extends Field.FieldDef<any, infer S>
102
- ? React.FC<FieldComponentProps<S, any>> | FieldBundle<any, S, any>
54
+ ? React.FC<FieldComponentProps<Schema.Schema.Encoded<S>, any>>
103
55
  : TFields[K] extends Field.ArrayFieldDef<any, infer S> ? ArrayItemComponentMap<S>
104
- : never
105
- }
56
+ : never;
57
+ };
58
+
59
+ export type FieldRefs<TFields extends Field.FieldsRecord> = FormAtoms.FieldRefs<TFields>;
106
60
 
107
- /**
108
- * Maps field names to their type-safe Field references for setValue operations.
109
- *
110
- * @category Models
111
- */
112
- export type FieldRefs<TFields extends Field.FieldsRecord> = FormAtoms.FieldRefs<TFields>
113
-
114
- /**
115
- * Operations available for array fields.
116
- *
117
- * @category Models
118
- */
119
61
  export interface ArrayFieldOperations<TItem> {
120
- readonly items: ReadonlyArray<TItem>
121
- readonly append: (value?: TItem) => void
122
- readonly remove: (index: number) => void
123
- readonly swap: (indexA: number, indexB: number) => void
124
- 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;
125
67
  }
126
68
 
127
- /**
128
- * The result of building a form, containing all components and utilities needed
129
- * for form rendering and submission.
130
- *
131
- * @category Models
132
- */
133
69
  export type BuiltForm<
134
70
  TFields extends Field.FieldsRecord,
135
71
  R,
@@ -138,198 +74,192 @@ export type BuiltForm<
138
74
  SubmitArgs = void,
139
75
  CM extends FieldComponentMap<TFields> = FieldComponentMap<TFields>,
140
76
  > = {
141
- readonly values: Atom.Atom<Option.Option<Field.EncodedFromFields<TFields>>>
142
- readonly isDirty: Atom.Atom<boolean>
143
- readonly hasChangedSinceSubmit: Atom.Atom<boolean>
144
- readonly lastSubmittedValues: Atom.Atom<Option.Option<FormBuilder.SubmittedValues<TFields>>>
145
- 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>;
146
82
 
147
- readonly schema: Schema.Schema<Field.DecodedFromFields<TFields>, Field.EncodedFromFields<TFields>, R>
148
- readonly fields: FieldRefs<TFields>
83
+ readonly schema: Schema.Schema<Field.DecodedFromFields<TFields>, Field.EncodedFromFields<TFields>, R>;
84
+ readonly fields: FieldRefs<TFields>;
149
85
 
150
86
  readonly Initialize: React.FC<{
151
- readonly defaultValues: Field.EncodedFromFields<TFields>
152
- readonly children: React.ReactNode
153
- }>
87
+ readonly defaultValues: Field.EncodedFromFields<TFields>;
88
+ readonly children: React.ReactNode;
89
+ }>;
154
90
 
155
- readonly submit: Atom.AtomResultFn<SubmitArgs, A, E | ParseResult.ParseError>
156
- readonly reset: Atom.Writable<void, void>
157
- readonly revertToLastSubmit: Atom.Writable<void, void>
158
- readonly setValues: Atom.Writable<void, Field.EncodedFromFields<TFields>>
159
- readonly setValue: <S>(field: FormBuilder.FieldRef<S>) => Atom.Writable<void, S | ((prev: S) => S)>
160
- readonly getFieldAtom: <S>(field: FormBuilder.FieldRef<S>) => Atom.Atom<Option.Option<S>>
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>>;
161
97
 
162
- readonly mount: Atom.Atom<void>
163
- readonly KeepAlive: React.FC
164
- } & FieldComponents<TFields, CM>
98
+ readonly mount: Atom.Atom<void>;
99
+ readonly KeepAlive: React.FC;
100
+ } & FieldComponents<TFields, CM>;
165
101
 
166
102
  type FieldComponents<TFields extends Field.FieldsRecord, CM extends FieldComponentMap<TFields>> = {
167
103
  readonly [K in keyof TFields]: TFields[K] extends Field.FieldDef<any, any> ? React.FC<ExtractExtraProps<CM[K]>>
168
104
  : TFields[K] extends Field.ArrayFieldDef<any, infer S>
169
105
  ? ArrayFieldComponent<S, ExtractArrayItemExtraProps<CM[K], S>>
170
- : never
171
- }
106
+ : never;
107
+ };
172
108
 
173
109
  type ExtractArrayItemExtraProps<CM, S extends Schema.Schema.Any> = S extends Schema.Struct<infer Fields>
174
- ? { readonly [K in keyof Fields]: CM extends { readonly [P in K]: infer C } ? ExtractExtraProps<C> : never }
110
+ ? { readonly [K in keyof Fields]: CM extends { readonly [P in K]: infer C; } ? ExtractExtraProps<C> : never; }
175
111
  : CM extends React.FC<FieldComponentProps<any, infer P>> ? P
176
- : never
112
+ : never;
177
113
 
178
114
  type ArrayFieldComponent<S extends Schema.Schema.Any, ExtraPropsMap> =
179
115
  & React.FC<{
180
- readonly children: (ops: ArrayFieldOperations<Schema.Schema.Encoded<S>>) => React.ReactNode
116
+ readonly children: (ops: ArrayFieldOperations<Schema.Schema.Encoded<S>>) => React.ReactNode;
181
117
  }>
182
118
  & {
183
119
  readonly Item: React.FC<{
184
- readonly index: number
185
- readonly children: React.ReactNode | ((props: { readonly remove: () => void }) => React.ReactNode)
186
- }>
120
+ readonly index: number;
121
+ readonly children: React.ReactNode | ((props: { readonly remove: () => void; }) => React.ReactNode);
122
+ }>;
187
123
  }
188
124
  & (S extends Schema.Struct<infer Fields> ? {
189
125
  readonly [K in keyof Fields]: React.FC<
190
- ExtraPropsMap extends { readonly [P in K]: infer EP } ? EP : Record<string, never>
191
- >
126
+ ExtraPropsMap extends { readonly [P in K]: infer EP; } ? EP : Record<string, never>
127
+ >;
192
128
  }
193
- : unknown)
129
+ : unknown);
194
130
 
195
131
  interface ArrayItemContextValue {
196
- readonly index: number
197
- readonly parentPath: string
132
+ readonly index: number;
133
+ readonly parentPath: string;
198
134
  }
199
135
 
200
- const ArrayItemContext = createContext<ArrayItemContextValue | null>(null)
201
- const AutoSubmitContext = createContext<(() => void) | null>(null)
136
+ const ArrayItemContext = createContext<ArrayItemContextValue | null>(null);
137
+ const AutoSubmitContext = createContext<(() => void) | null>(null);
202
138
 
203
- const makeFieldComponent = <S extends Schema.Schema.Any, P extends Record<string, unknown>>(
139
+ const makeFieldComponent = <S extends Schema.Schema.Any, P>(
204
140
  fieldKey: string,
205
141
  fieldDef: Field.FieldDef<string, S>,
206
142
  errorsAtom: Atom.Writable<Map<string, Validation.ErrorEntry>, Map<string, Validation.ErrorEntry>>,
207
143
  submitCountAtom: Atom.Atom<number>,
208
- dirtyFieldsAtom: Atom.Atom<ReadonlySet<string>>,
209
144
  parsedMode: Mode.ParsedMode,
210
145
  getOrCreateValidationAtom: (
211
146
  fieldPath: string,
212
147
  schema: Schema.Schema.Any,
213
148
  ) => Atom.AtomResultFn<unknown, void, ParseResult.ParseError>,
214
149
  getOrCreateFieldAtoms: (fieldPath: string) => FormAtoms.FieldAtoms,
215
- Component: React.FC<FieldComponentProps<S, P>>,
150
+ Component: React.FC<FieldComponentProps<Schema.Schema.Encoded<S>, P>>,
216
151
  ): React.FC<P> => {
217
152
  const FieldComponent: React.FC<P> = (extraProps) => {
218
- const arrayCtx = useContext(ArrayItemContext)
219
- const autoSubmitOnBlur = useContext(AutoSubmitContext)
220
- const fieldPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey
153
+ const arrayCtx = useContext(ArrayItemContext);
154
+ const autoSubmitOnBlur = useContext(AutoSubmitContext);
155
+ const fieldPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey;
221
156
 
222
- const { errorAtom, touchedAtom, valueAtom } = React.useMemo(
157
+ const { errorAtom, isDirtyAtom, touchedAtom, valueAtom } = React.useMemo(
223
158
  () => getOrCreateFieldAtoms(fieldPath),
224
159
  [fieldPath],
225
- )
160
+ );
226
161
 
227
- const [value, setValue] = useAtom(valueAtom) as [Schema.Schema.Encoded<S>, (v: unknown) => void]
228
- const [isTouched, setTouched] = useAtom(touchedAtom)
229
- const storedError = useAtomValue(errorAtom)
230
- const submitCount = useAtomValue(submitCountAtom)
162
+ const [value, setValue] = useAtom(valueAtom) as [Schema.Schema.Encoded<S>, (v: unknown) => void];
163
+ const [isTouched, setTouched] = useAtom(touchedAtom);
164
+ const storedError = useAtomValue(errorAtom);
165
+ const submitCount = useAtomValue(submitCountAtom);
231
166
 
232
- const validationAtom = React.useMemo(
233
- () => getOrCreateValidationAtom(fieldPath, fieldDef.schema),
234
- [fieldPath],
235
- )
236
- const validationResult = useAtomValue(validationAtom)
237
- const validateImmediate = useAtomSet(validationAtom)
167
+ const validationAtom = React.useMemo(() => getOrCreateValidationAtom(fieldPath, fieldDef.schema), [fieldPath]);
168
+ const validationResult = useAtomValue(validationAtom);
169
+ const validateImmediate = useAtomSet(validationAtom);
238
170
 
239
- const shouldDebounceValidation = parsedMode.validation === "onChange"
240
- && parsedMode.debounce !== null
241
- && !parsedMode.autoSubmit
242
- const validate = useDebounced(validateImmediate, shouldDebounceValidation ? parsedMode.debounce : null)
171
+ const shouldDebounceValidation = parsedMode.validation === "onChange" && parsedMode.debounce !== null &&
172
+ !parsedMode.autoSubmit;
173
+ const validate = useDebounced(validateImmediate, shouldDebounceValidation ? parsedMode.debounce : null);
243
174
 
244
- const prevValueRef = React.useRef(value)
175
+ const prevValueRef = React.useRef(value);
245
176
  React.useEffect(() => {
246
177
  if (prevValueRef.current === value) {
247
- return
178
+ return;
248
179
  }
249
- prevValueRef.current = value
180
+ prevValueRef.current = value;
250
181
 
251
- const shouldValidate = parsedMode.validation === "onChange"
252
- || (parsedMode.validation === "onBlur" && isTouched)
253
- || (parsedMode.validation === "onSubmit" && submitCount > 0)
182
+ const shouldValidate = parsedMode.validation === "onChange" ||
183
+ (parsedMode.validation === "onBlur" && isTouched) ||
184
+ (parsedMode.validation === "onSubmit" && submitCount > 0);
254
185
 
255
186
  if (shouldValidate) {
256
- validate(value)
187
+ validate(value);
257
188
  }
258
- }, [value, isTouched, submitCount, validate])
189
+ }, [value, isTouched, submitCount, validate]);
259
190
 
260
191
  const livePerFieldError: Option.Option<string> = React.useMemo(() => {
261
192
  if (validationResult._tag === "Failure") {
262
- const parseError = Cause.failureOption(validationResult.cause)
193
+ const parseError = Cause.failureOption(validationResult.cause);
263
194
  if (Option.isSome(parseError) && ParseResult.isParseError(parseError.value)) {
264
- return Validation.extractFirstError(parseError.value)
195
+ return Validation.extractFirstError(parseError.value);
265
196
  }
266
197
  }
267
- return Option.none()
268
- }, [validationResult])
198
+ return Option.none();
199
+ }, [validationResult]);
269
200
 
270
- const isValidating = validationResult.waiting
201
+ const isValidating = validationResult.waiting;
271
202
 
272
203
  const validationError: Option.Option<string> = React.useMemo(() => {
273
204
  if (Option.isSome(livePerFieldError)) {
274
- return livePerFieldError
205
+ return livePerFieldError;
275
206
  }
276
207
 
277
208
  if (Option.isSome(storedError)) {
278
209
  // Hide field-sourced errors when validation passes or is pending (async gap).
279
210
  // Refinement errors persist until re-submit - they can't be cleared by typing.
280
211
  const shouldHideStoredError = storedError.value.source === "field" &&
281
- (validationResult._tag === "Success" || isValidating)
212
+ (validationResult._tag === "Success" || isValidating);
282
213
 
283
214
  if (shouldHideStoredError) {
284
- return Option.none()
215
+ return Option.none();
285
216
  }
286
- return Option.some(storedError.value.message)
217
+ return Option.some(storedError.value.message);
287
218
  }
288
219
 
289
- return Option.none()
290
- }, [livePerFieldError, storedError, validationResult, isValidating])
220
+ return Option.none();
221
+ }, [livePerFieldError, storedError, validationResult, isValidating]);
291
222
 
292
223
  const onChange = React.useCallback(
293
224
  (newValue: Schema.Schema.Encoded<S>) => {
294
- setValue(newValue)
225
+ setValue(newValue);
295
226
  },
296
227
  [setValue],
297
- )
228
+ );
298
229
 
299
230
  const onBlur = React.useCallback(() => {
300
- setTouched(true)
231
+ setTouched(true);
301
232
  if (parsedMode.validation === "onBlur") {
302
- validate(value)
233
+ validate(value);
303
234
  }
304
- autoSubmitOnBlur?.()
305
- }, [setTouched, validate, value, autoSubmitOnBlur])
306
-
307
- const dirtyFields = useAtomValue(dirtyFieldsAtom)
308
- const isDirty = React.useMemo(
309
- () => isPathOrParentDirty(dirtyFields, fieldPath),
310
- [dirtyFields, fieldPath],
311
- )
235
+ autoSubmitOnBlur?.();
236
+ }, [setTouched, validate, value, autoSubmitOnBlur]);
237
+
238
+ const isDirty = useAtomValue(isDirtyAtom);
312
239
  const shouldShowError = parsedMode.validation === "onChange"
313
- ? (isDirty || submitCount > 0)
240
+ ? isDirty || submitCount > 0
314
241
  : parsedMode.validation === "onBlur"
315
- ? (isTouched || submitCount > 0)
316
- : submitCount > 0
317
-
318
- const fieldState: FieldState<S> = React.useMemo(() => ({
319
- value,
320
- onChange,
321
- onBlur,
322
- error: shouldShowError ? validationError : Option.none<string>(),
323
- isTouched,
324
- isValidating,
325
- isDirty,
326
- }), [value, onChange, onBlur, shouldShowError, validationError, isTouched, isValidating, isDirty])
327
-
328
- return <Component field={fieldState} props={extraProps} />
329
- }
330
-
331
- return React.memo(FieldComponent) as React.FC<P>
332
- }
242
+ ? isTouched || submitCount > 0
243
+ : submitCount > 0;
244
+
245
+ const fieldState: FieldState<Schema.Schema.Encoded<S>> = React.useMemo(
246
+ () => ({
247
+ value,
248
+ onChange,
249
+ onBlur,
250
+ error: shouldShowError ? validationError : Option.none<string>(),
251
+ isTouched,
252
+ isValidating,
253
+ isDirty,
254
+ }),
255
+ [value, onChange, onBlur, shouldShowError, validationError, isTouched, isValidating, isDirty],
256
+ );
257
+
258
+ return <Component field={fieldState} props={extraProps} />;
259
+ };
260
+
261
+ return React.memo(FieldComponent) as React.FC<P>;
262
+ };
333
263
 
334
264
  const makeArrayFieldComponent = <S extends Schema.Schema.Any>(
335
265
  fieldKey: string,
@@ -347,130 +277,126 @@ const makeArrayFieldComponent = <S extends Schema.Schema.Any>(
347
277
  operations: FormAtoms.FormOperations<any>,
348
278
  componentMap: ArrayItemComponentMap<S>,
349
279
  ): ArrayFieldComponent<S, any> => {
350
- const isStructSchema = AST.isTypeLiteral(def.itemSchema.ast)
280
+ const isStructSchema = AST.isTypeLiteral(def.itemSchema.ast);
351
281
 
352
282
  const ArrayWrapper: React.FC<{
353
- readonly children: (ops: ArrayFieldOperations<Schema.Schema.Encoded<S>>) => React.ReactNode
283
+ readonly children: (ops: ArrayFieldOperations<Schema.Schema.Encoded<S>>) => React.ReactNode;
354
284
  }> = ({ children }) => {
355
- const arrayCtx = useContext(ArrayItemContext)
356
- const [formStateOption, setFormState] = useAtom(stateAtom)
357
- const formState = Option.getOrThrow(formStateOption)
285
+ const arrayCtx = useContext(ArrayItemContext);
286
+ const [formStateOption, setFormState] = useAtom(stateAtom);
287
+ const formState = Option.getOrThrow(formStateOption);
358
288
 
359
- const fieldPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey
289
+ const fieldPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey;
360
290
  const items = React.useMemo(
361
291
  () => (getNestedValue(formState.values, fieldPath) ?? []) as ReadonlyArray<Schema.Schema.Encoded<S>>,
362
292
  [formState.values, fieldPath],
363
- )
293
+ );
364
294
 
365
295
  const append = React.useCallback(
366
296
  (value?: Schema.Schema.Encoded<S>) => {
367
297
  setFormState((prev) => {
368
- if (Option.isNone(prev)) return prev
369
- return Option.some(operations.appendArrayItem(prev.value, fieldPath, def.itemSchema, value))
370
- })
298
+ if (Option.isNone(prev)) return prev;
299
+ return Option.some(operations.appendArrayItem(prev.value, fieldPath, def.itemSchema, value));
300
+ });
371
301
  },
372
302
  [fieldPath, setFormState],
373
- )
303
+ );
374
304
 
375
305
  const remove = React.useCallback(
376
306
  (index: number) => {
377
307
  setFormState((prev) => {
378
- if (Option.isNone(prev)) return prev
379
- return Option.some(operations.removeArrayItem(prev.value, fieldPath, index))
380
- })
308
+ if (Option.isNone(prev)) return prev;
309
+ return Option.some(operations.removeArrayItem(prev.value, fieldPath, index));
310
+ });
381
311
  },
382
312
  [fieldPath, setFormState],
383
- )
313
+ );
384
314
 
385
315
  const swap = React.useCallback(
386
316
  (indexA: number, indexB: number) => {
387
317
  setFormState((prev) => {
388
- if (Option.isNone(prev)) return prev
389
- return Option.some(operations.swapArrayItems(prev.value, fieldPath, indexA, indexB))
390
- })
318
+ if (Option.isNone(prev)) return prev;
319
+ return Option.some(operations.swapArrayItems(prev.value, fieldPath, indexA, indexB));
320
+ });
391
321
  },
392
322
  [fieldPath, setFormState],
393
- )
323
+ );
394
324
 
395
325
  const move = React.useCallback(
396
326
  (from: number, to: number) => {
397
327
  setFormState((prev) => {
398
- if (Option.isNone(prev)) return prev
399
- return Option.some(operations.moveArrayItem(prev.value, fieldPath, from, to))
400
- })
328
+ if (Option.isNone(prev)) return prev;
329
+ return Option.some(operations.moveArrayItem(prev.value, fieldPath, from, to));
330
+ });
401
331
  },
402
332
  [fieldPath, setFormState],
403
- )
333
+ );
404
334
 
405
- return <>{children({ items, append, remove, swap, move })}</>
406
- }
335
+ return <>{children({ items, append, remove, swap, move })}</>;
336
+ };
407
337
 
408
338
  const ItemWrapper: React.FC<{
409
- readonly index: number
410
- readonly children: React.ReactNode | ((props: { readonly remove: () => void }) => React.ReactNode)
339
+ readonly index: number;
340
+ readonly children: React.ReactNode | ((props: { readonly remove: () => void; }) => React.ReactNode);
411
341
  }> = ({ children, index }) => {
412
- const arrayCtx = useContext(ArrayItemContext)
413
- const setFormState = useAtomSet(stateAtom)
342
+ const arrayCtx = useContext(ArrayItemContext);
343
+ const setFormState = useAtomSet(stateAtom);
414
344
 
415
- const parentPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey
416
- const itemPath = `${parentPath}[${index}]`
345
+ const parentPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey;
346
+ const itemPath = `${parentPath}[${index}]`;
417
347
 
418
348
  const remove = React.useCallback(() => {
419
349
  setFormState((prev) => {
420
- if (Option.isNone(prev)) return prev
421
- return Option.some(operations.removeArrayItem(prev.value, parentPath, index))
422
- })
423
- }, [parentPath, index, setFormState])
350
+ if (Option.isNone(prev)) return prev;
351
+ return Option.some(operations.removeArrayItem(prev.value, parentPath, index));
352
+ });
353
+ }, [parentPath, index, setFormState]);
424
354
 
425
355
  return (
426
356
  <ArrayItemContext.Provider value={{ index, parentPath: itemPath }}>
427
357
  {typeof children === "function" ? children({ remove }) : children}
428
358
  </ArrayItemContext.Provider>
429
- )
430
- }
359
+ );
360
+ };
431
361
 
432
- const itemFieldComponents: Record<string, React.FC> = {}
362
+ const itemFieldComponents: Record<string, React.FC> = {};
433
363
 
434
364
  if (isStructSchema) {
435
- const ast = def.itemSchema.ast as AST.TypeLiteral
365
+ const ast = def.itemSchema.ast as AST.TypeLiteral;
436
366
  for (const prop of ast.propertySignatures) {
437
- const itemKey = prop.name as string
438
- const itemSchema = { ast: prop.type } as Schema.Schema.Any
439
- const itemDef = Field.makeField(itemKey, itemSchema)
440
- const itemComponent = (componentMap as Record<string, React.FC<FieldComponentProps<any, any>>>)[itemKey]
367
+ const itemKey = prop.name as string;
368
+ const itemSchema = { ast: prop.type } as Schema.Schema.Any;
369
+ const itemDef = Field.makeField(itemKey, itemSchema);
370
+ const itemComponent = (componentMap as Record<string, React.FC<FieldComponentProps<any, any>>>)[itemKey];
441
371
  itemFieldComponents[itemKey] = makeFieldComponent(
442
372
  itemKey,
443
373
  itemDef,
444
374
  errorsAtom,
445
375
  submitCountAtom,
446
- dirtyFieldsAtom,
447
376
  parsedMode,
448
377
  getOrCreateValidationAtom,
449
378
  getOrCreateFieldAtoms,
450
379
  itemComponent,
451
- )
380
+ );
452
381
  }
453
382
  }
454
383
 
455
384
  const properties: Record<string, unknown> = {
456
385
  Item: ItemWrapper,
457
386
  ...itemFieldComponents,
458
- }
387
+ };
459
388
 
460
389
  return new Proxy(ArrayWrapper, {
461
390
  get(target, prop) {
462
391
  if (prop in properties) {
463
- return properties[prop as string]
392
+ return properties[prop as string];
464
393
  }
465
- return Reflect.get(target, prop)
394
+ return Reflect.get(target, prop);
466
395
  },
467
- }) as ArrayFieldComponent<S, any>
468
- }
396
+ }) as ArrayFieldComponent<S, any>;
397
+ };
469
398
 
470
- const makeFieldComponents = <
471
- TFields extends Field.FieldsRecord,
472
- CM extends FieldComponentMap<TFields>,
473
- >(
399
+ const makeFieldComponents = <TFields extends Field.FieldsRecord, CM extends FieldComponentMap<TFields>>(
474
400
  fields: TFields,
475
401
  stateAtom: Atom.Writable<
476
402
  Option.Option<FormBuilder.FormState<TFields>>,
@@ -488,11 +414,11 @@ const makeFieldComponents = <
488
414
  operations: FormAtoms.FormOperations<TFields>,
489
415
  componentMap: CM,
490
416
  ): FieldComponents<TFields, CM> => {
491
- const components: Record<string, any> = {}
417
+ const components: Record<string, any> = {};
492
418
 
493
419
  for (const [key, def] of Object.entries(fields)) {
494
420
  if (Field.isArrayFieldDef(def)) {
495
- const arrayComponentMap = (componentMap as Record<string, any>)[key]
421
+ const arrayComponentMap = (componentMap as Record<string, any>)[key];
496
422
  components[key] = makeArrayFieldComponent(
497
423
  key,
498
424
  def as Field.ArrayFieldDef<string, Schema.Schema.Any>,
@@ -505,74 +431,25 @@ const makeFieldComponents = <
505
431
  getOrCreateFieldAtoms,
506
432
  operations,
507
433
  arrayComponentMap,
508
- )
434
+ );
509
435
  } else if (Field.isFieldDef(def)) {
510
- const componentOrBundle = (componentMap as Record<string, unknown>)[key]
511
- const fieldComponent = isFieldBundle(componentOrBundle)
512
- ? componentOrBundle.component
513
- : componentOrBundle as React.FC<FieldComponentProps<any, any>>
436
+ const fieldComponent = (componentMap as Record<string, React.FC<FieldComponentProps<any, any>>>)[key];
514
437
  components[key] = makeFieldComponent(
515
438
  key,
516
439
  def,
517
440
  errorsAtom,
518
441
  submitCountAtom,
519
- dirtyFieldsAtom,
520
442
  parsedMode,
521
443
  getOrCreateValidationAtom,
522
444
  getOrCreateFieldAtoms,
523
445
  fieldComponent,
524
- )
446
+ );
525
447
  }
526
448
  }
527
449
 
528
- return components as FieldComponents<TFields, CM>
529
- }
450
+ return components as FieldComponents<TFields, CM>;
451
+ };
530
452
 
531
- /**
532
- * Creates a React form from a FormBuilder.
533
- *
534
- * @example
535
- * ```tsx
536
- * import { FormBuilder } from "@lucas-barake/effect-form"
537
- * import { FormReact } from "@lucas-barake/effect-form-react"
538
- * import { useAtomValue, useAtomSet } from "@effect-atom/atom-react"
539
- * import * as Schema from "effect/Schema"
540
- *
541
- * const loginFormBuilder = FormBuilder.empty
542
- * .addField("email", Schema.String)
543
- * .addField("password", Schema.String)
544
- *
545
- * // Runtime is optional for forms without service requirements
546
- * const loginForm = FormReact.make(loginFormBuilder, {
547
- * fields: { email: TextInput, password: PasswordInput },
548
- * onSubmit: (_, { decoded }) => Effect.log(`Login: ${decoded.email}`),
549
- * })
550
- *
551
- * // Subscribe to atoms anywhere in the tree
552
- * function SubmitButton() {
553
- * const isDirty = useAtomValue(loginForm.isDirty)
554
- * const submit = useAtomValue(loginForm.submit)
555
- * const callSubmit = useAtomSet(loginForm.submit)
556
- * return (
557
- * <button onClick={() => callSubmit()} disabled={!isDirty || submit.waiting}>
558
- * {submit.waiting ? "Validating..." : "Login"}
559
- * </button>
560
- * )
561
- * }
562
- *
563
- * function LoginPage() {
564
- * return (
565
- * <loginForm.Initialize defaultValues={{ email: "", password: "" }}>
566
- * <loginForm.email />
567
- * <loginForm.password />
568
- * <SubmitButton />
569
- * </loginForm.Initialize>
570
- * )
571
- * }
572
- * ```
573
- *
574
- * @category Constructors
575
- */
576
453
  export const make: {
577
454
  <
578
455
  TFields extends Field.FieldsRecord,
@@ -583,19 +460,19 @@ export const make: {
583
460
  >(
584
461
  self: FormBuilder.FormBuilder<TFields, never>,
585
462
  options: {
586
- readonly runtime?: Atom.AtomRuntime<never, never>
587
- readonly fields: CM
588
- readonly mode?: SubmitArgs extends void ? Mode.FormMode : Mode.FormModeWithoutAutoSubmit
463
+ readonly runtime?: Atom.AtomRuntime<never, never>;
464
+ readonly fields: CM;
465
+ readonly mode?: SubmitArgs extends void ? Mode.FormMode : Mode.FormModeWithoutAutoSubmit;
589
466
  readonly onSubmit: (
590
467
  args: SubmitArgs,
591
468
  ctx: {
592
- readonly decoded: Field.DecodedFromFields<TFields>
593
- readonly encoded: Field.EncodedFromFields<TFields>
594
- readonly get: Atom.FnContext
469
+ readonly decoded: Field.DecodedFromFields<TFields>;
470
+ readonly encoded: Field.EncodedFromFields<TFields>;
471
+ readonly get: Atom.FnContext;
595
472
  },
596
- ) => A | Effect.Effect<A, E, never>
473
+ ) => A | Effect.Effect<A, E, never>;
597
474
  },
598
- ): BuiltForm<TFields, never, A, E, SubmitArgs, CM>
475
+ ): BuiltForm<TFields, never, A, E, SubmitArgs, CM>;
599
476
 
600
477
  <
601
478
  TFields extends Field.FieldsRecord,
@@ -608,30 +485,30 @@ export const make: {
608
485
  >(
609
486
  self: FormBuilder.FormBuilder<TFields, R>,
610
487
  options: {
611
- readonly runtime: Atom.AtomRuntime<R, ER>
612
- readonly fields: CM
613
- readonly mode?: SubmitArgs extends void ? Mode.FormMode : Mode.FormModeWithoutAutoSubmit
488
+ readonly runtime: Atom.AtomRuntime<R, ER>;
489
+ readonly fields: CM;
490
+ readonly mode?: SubmitArgs extends void ? Mode.FormMode : Mode.FormModeWithoutAutoSubmit;
614
491
  readonly onSubmit: (
615
492
  args: SubmitArgs,
616
493
  ctx: {
617
- readonly decoded: Field.DecodedFromFields<TFields>
618
- readonly encoded: Field.EncodedFromFields<TFields>
619
- readonly get: Atom.FnContext
494
+ readonly decoded: Field.DecodedFromFields<TFields>;
495
+ readonly encoded: Field.EncodedFromFields<TFields>;
496
+ readonly get: Atom.FnContext;
620
497
  },
621
- ) => A | Effect.Effect<A, E, R>
498
+ ) => A | Effect.Effect<A, E, R>;
622
499
  },
623
- ): BuiltForm<TFields, R, A, E, SubmitArgs, CM>
500
+ ): BuiltForm<TFields, R, A, E, SubmitArgs, CM>;
624
501
  } = (self: any, options: any): any => {
625
- const { fields: components, mode, onSubmit, runtime: providedRuntime } = options
626
- const runtime = providedRuntime ?? Atom.runtime(Layer.empty)
627
- const parsedMode = Mode.parse(mode)
628
- const { fields } = self
502
+ const { fields: components, mode, onSubmit, runtime: providedRuntime } = options;
503
+ const runtime = providedRuntime ?? Atom.runtime(Layer.empty);
504
+ const parsedMode = Mode.parse(mode);
505
+ const { fields } = self;
629
506
 
630
507
  const formAtoms = FormAtoms.make({
631
508
  formBuilder: self,
632
509
  runtime,
633
510
  onSubmit,
634
- })
511
+ });
635
512
 
636
513
  const {
637
514
  combinedSchema,
@@ -656,37 +533,40 @@ export const make: {
656
533
  submitAtom,
657
534
  submitCountAtom,
658
535
  valuesAtom,
659
- } = formAtoms
536
+ } = formAtoms;
660
537
 
661
538
  const InitializeComponent: React.FC<{
662
- readonly defaultValues: any
663
- readonly children: React.ReactNode
539
+ readonly defaultValues: any;
540
+ readonly children: React.ReactNode;
664
541
  }> = ({ children, defaultValues }) => {
665
- const registry = React.useContext(RegistryContext)
666
- const state = useAtomValue(stateAtom)
667
- const setFormState = useAtomSet(stateAtom)
668
- const callSubmit = useAtomSet(submitAtom)
669
- const isInitializedRef = React.useRef(false)
542
+ const registry = React.useContext(RegistryContext);
543
+ const state = useAtomValue(stateAtom);
544
+ const setFormState = useAtomSet(stateAtom);
545
+ const callSubmit = useAtomSet(submitAtom);
546
+ const isInitializedRef = React.useRef(false);
670
547
 
671
548
  React.useEffect(() => {
672
- const isKeptAlive = registry.get(keepAliveActiveAtom)
673
- const currentState = registry.get(stateAtom)
549
+ const isKeptAlive = registry.get(keepAliveActiveAtom);
550
+ const currentState = registry.get(stateAtom);
674
551
 
675
552
  if (!isKeptAlive) {
676
- setFormState(Option.some(operations.createInitialState(defaultValues)))
553
+ setFormState(Option.some(operations.createInitialState(defaultValues)));
677
554
  } else if (Option.isNone(currentState)) {
678
- setFormState(Option.some(operations.createInitialState(defaultValues)))
555
+ setFormState(Option.some(operations.createInitialState(defaultValues)));
679
556
  }
680
557
 
681
- isInitializedRef.current = true
558
+ isInitializedRef.current = true;
682
559
  // eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only
683
- }, [registry])
560
+ }, [registry]);
684
561
 
685
- const debouncedAutoSubmit = useDebounced(() => {
686
- const stateOption = registry.get(stateAtom)
687
- if (Option.isNone(stateOption)) return
688
- callSubmit(undefined)
689
- }, parsedMode.autoSubmit && parsedMode.validation === "onChange" ? parsedMode.debounce : null)
562
+ const debouncedAutoSubmit = useDebounced(
563
+ () => {
564
+ const stateOption = registry.get(stateAtom);
565
+ if (Option.isNone(stateOption)) return;
566
+ callSubmit(undefined);
567
+ },
568
+ parsedMode.autoSubmit && parsedMode.validation === "onChange" ? parsedMode.debounce : null,
569
+ );
690
570
 
691
571
  // ─────────────────────────────────────────────────────────────────────────────
692
572
  // Auto-Submit Coordination
@@ -699,75 +579,75 @@ export const make: {
699
579
  // metadata updates (submitCount, lastSubmittedValues).
700
580
  // ─────────────────────────────────────────────────────────────────────────────
701
581
 
702
- const lastValuesRef = React.useRef<unknown>(null)
703
- const pendingChangesRef = React.useRef(false)
704
- const wasSubmittingRef = React.useRef(false)
582
+ const lastValuesRef = React.useRef<unknown>(null);
583
+ const pendingChangesRef = React.useRef(false);
584
+ const wasSubmittingRef = React.useRef(false);
705
585
 
706
586
  useAtomSubscribe(
707
587
  stateAtom,
708
588
  React.useCallback(() => {
709
- if (!isInitializedRef.current) return
589
+ if (!isInitializedRef.current) return;
710
590
 
711
- const state = registry.get(stateAtom)
712
- if (Option.isNone(state)) return
713
- const currentValues = state.value.values
591
+ const state = registry.get(stateAtom);
592
+ if (Option.isNone(state)) return;
593
+ const currentValues = state.value.values;
714
594
 
715
595
  // Reference equality filters out submit metadata changes.
716
596
  // Works because setFieldValue creates new values object (immutable update).
717
- if (currentValues === lastValuesRef.current) return
718
- lastValuesRef.current = currentValues
597
+ if (currentValues === lastValuesRef.current) return;
598
+ lastValuesRef.current = currentValues;
719
599
 
720
- if (!parsedMode.autoSubmit || parsedMode.validation !== "onChange") return
600
+ if (!parsedMode.autoSubmit || parsedMode.validation !== "onChange") return;
721
601
 
722
- const submitResult = registry.get(submitAtom)
602
+ const submitResult = registry.get(submitAtom);
723
603
  if (submitResult.waiting) {
724
- pendingChangesRef.current = true
604
+ pendingChangesRef.current = true;
725
605
  } else {
726
- debouncedAutoSubmit()
606
+ debouncedAutoSubmit();
727
607
  }
728
608
  }, [debouncedAutoSubmit, registry]),
729
609
  { immediate: false },
730
- )
610
+ );
731
611
 
732
612
  useAtomSubscribe(
733
613
  submitAtom,
734
614
  React.useCallback(
735
615
  (result) => {
736
- if (!parsedMode.autoSubmit || parsedMode.validation !== "onChange") return
616
+ if (!parsedMode.autoSubmit || parsedMode.validation !== "onChange") return;
737
617
 
738
- const isSubmitting = result.waiting
739
- const wasSubmitting = wasSubmittingRef.current
740
- wasSubmittingRef.current = isSubmitting
618
+ const isSubmitting = result.waiting;
619
+ const wasSubmitting = wasSubmittingRef.current;
620
+ wasSubmittingRef.current = isSubmitting;
741
621
 
742
622
  // Flush queued changes when submit completes
743
623
  if (wasSubmitting && !isSubmitting) {
744
624
  if (pendingChangesRef.current) {
745
- pendingChangesRef.current = false
746
- debouncedAutoSubmit()
625
+ pendingChangesRef.current = false;
626
+ debouncedAutoSubmit();
747
627
  }
748
628
  }
749
629
  },
750
630
  [debouncedAutoSubmit],
751
631
  ),
752
632
  { immediate: false },
753
- )
633
+ );
754
634
 
755
635
  const onBlurAutoSubmit = React.useCallback(() => {
756
- if (!parsedMode.autoSubmit || parsedMode.validation !== "onBlur") return
636
+ if (!parsedMode.autoSubmit || parsedMode.validation !== "onBlur") return;
757
637
 
758
- const stateOption = registry.get(stateAtom)
759
- if (Option.isNone(stateOption)) return
638
+ const stateOption = registry.get(stateAtom);
639
+ if (Option.isNone(stateOption)) return;
760
640
 
761
- const { lastSubmittedValues, values } = stateOption.value
762
- if (Option.isSome(lastSubmittedValues) && values === lastSubmittedValues.value.encoded) return
641
+ const { lastSubmittedValues, values } = stateOption.value;
642
+ if (Option.isSome(lastSubmittedValues) && values === lastSubmittedValues.value.encoded) return;
763
643
 
764
- callSubmit(undefined)
765
- }, [registry, callSubmit])
644
+ callSubmit(undefined);
645
+ }, [registry, callSubmit]);
766
646
 
767
- if (Option.isNone(state)) return null
647
+ if (Option.isNone(state)) return null;
768
648
 
769
- return <AutoSubmitContext.Provider value={onBlurAutoSubmit}>{children}</AutoSubmitContext.Provider>
770
- }
649
+ return <AutoSubmitContext.Provider value={onBlurAutoSubmit}>{children}</AutoSubmitContext.Provider>;
650
+ };
771
651
 
772
652
  const fieldComponents = makeFieldComponents(
773
653
  fields,
@@ -780,19 +660,19 @@ export const make: {
780
660
  getOrCreateFieldAtoms,
781
661
  operations,
782
662
  components,
783
- )
663
+ );
784
664
 
785
665
  const KeepAlive: React.FC = () => {
786
- const setKeepAliveActive = useAtomSet(keepAliveActiveAtom)
666
+ const setKeepAliveActive = useAtomSet(keepAliveActiveAtom);
787
667
 
788
668
  React.useLayoutEffect(() => {
789
- setKeepAliveActive(true)
790
- return () => setKeepAliveActive(false)
791
- }, [setKeepAliveActive])
669
+ setKeepAliveActive(true);
670
+ return () => setKeepAliveActive(false);
671
+ }, [setKeepAliveActive]);
792
672
 
793
- useAtomMount(mountAtom)
794
- return null
795
- }
673
+ useAtomMount(mountAtom);
674
+ return null;
675
+ };
796
676
 
797
677
  return {
798
678
  values: valuesAtom,
@@ -813,106 +693,5 @@ export const make: {
813
693
  mount: mountAtom,
814
694
  KeepAlive,
815
695
  ...fieldComponents,
816
- }
817
- }
818
-
819
- /**
820
- * A curried helper that infers the schema type from a field definition.
821
- * Provides ergonomic type inference when defining field components.
822
- *
823
- * @example
824
- * ```tsx
825
- * import { Field, FormReact } from "@lucas-barake/effect-form-react"
826
- *
827
- * const EmailField = Field.makeField("email", Schema.String)
828
- * const TextInput = FormReact.forField(EmailField)(({ field }) => (
829
- * <input value={field.value} onChange={e => field.onChange(e.target.value)} />
830
- * ))
831
- *
832
- * // With extra props - just specify the props type
833
- * const TextInput = FormReact.forField(EmailField)<{ placeholder?: string }>(({ field, props }) => (
834
- * <input value={field.value} placeholder={props.placeholder} ... />
835
- * ))
836
- * ```
837
- *
838
- * @category Constructors
839
- */
840
- export const forField = <K extends string, S extends Schema.Schema.Any>(
841
- _field: Field.FieldDef<K, S>,
842
- ): <P extends Record<string, unknown> = Record<string, never>>(
843
- component: React.FC<FieldComponentProps<S, P>>,
844
- ) => React.FC<FieldComponentProps<S, P>> =>
845
- (component) => component
846
-
847
- /**
848
- * Creates a bundled field definition + component for reusable form fields.
849
- * Reduces boilerplate when you need both a field and its component together.
850
- *
851
- * Uses a curried API for better type inference - the schema type is captured
852
- * first, so you only need to specify the extra props type (if any).
853
- *
854
- * @example
855
- * ```tsx
856
- * import { FormReact } from "@lucas-barake/effect-form-react"
857
- * import * as Schema from "effect/Schema"
858
- *
859
- * // Define field + component in one place (no extra props)
860
- * const NameInput = FormReact.makeField({
861
- * key: "name",
862
- * schema: Schema.String.pipe(Schema.nonEmptyString()),
863
- * })(({ field }) => (
864
- * <input
865
- * value={field.value}
866
- * onChange={(e) => field.onChange(e.target.value)}
867
- * onBlur={field.onBlur}
868
- * />
869
- * ))
870
- *
871
- * // With extra props - specify only the props type
872
- * const EmailInput = FormReact.makeField({
873
- * key: "email",
874
- * schema: Schema.String,
875
- * })<{ placeholder: string }>(({ field, props }) => (
876
- * <input
877
- * value={field.value}
878
- * onChange={(e) => field.onChange(e.target.value)}
879
- * placeholder={props.placeholder}
880
- * />
881
- * ))
882
- *
883
- * // Use in form builder
884
- * const formBuilder = FormBuilder.empty.addField(NameInput.field)
885
- *
886
- * // Use in make()
887
- * const form = FormReact.make(formBuilder, {
888
- * runtime,
889
- * fields: { name: NameInput },
890
- * onSubmit: (_, { decoded }) => Effect.log(decoded.name),
891
- * })
892
- * ```
893
- *
894
- * @category Constructors
895
- */
896
- export const makeField = <K extends string, S extends Schema.Schema.Any>(options: {
897
- readonly key: K
898
- readonly schema: S
899
- }): <P extends Record<string, unknown> = Record<string, never>>(
900
- component: React.FC<FieldComponentProps<S, P>>,
901
- ) => FieldBundle<K, S, P> => {
902
- const field = Field.makeField(options.key, options.schema)
903
- return (component) => {
904
- if (!component.displayName) {
905
- const displayName = `${options.key.charAt(0).toUpperCase()}${options.key.slice(1)}Field`
906
- try {
907
- ;(component as any).displayName = displayName
908
- } catch {
909
- // Ignore - some environments freeze function properties
910
- }
911
- }
912
- return {
913
- _tag: "FieldBundle",
914
- field,
915
- component,
916
- }
917
- }
918
- }
696
+ };
697
+ };