@lucas-barake/effect-form-react 0.1.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.
@@ -0,0 +1,177 @@
1
+ import * as Atom from "@effect-atom/atom/Atom";
2
+ import type * as Result from "@effect-atom/atom/Result";
3
+ import { Form, FormAtoms, Mode } from "@lucas-barake/effect-form";
4
+ import type * as Effect from "effect/Effect";
5
+ import * as Option from "effect/Option";
6
+ import type * as Schema from "effect/Schema";
7
+ import * as React from "react";
8
+ /**
9
+ * Props passed to field components.
10
+ *
11
+ * @since 1.0.0
12
+ * @category Models
13
+ */
14
+ export interface FieldComponentProps<S extends Schema.Schema.Any> {
15
+ readonly value: Schema.Schema.Encoded<S>;
16
+ readonly onChange: (value: Schema.Schema.Encoded<S>) => void;
17
+ readonly onBlur: () => void;
18
+ readonly error: Option.Option<string>;
19
+ readonly isTouched: boolean;
20
+ readonly isValidating: boolean;
21
+ readonly isDirty: boolean;
22
+ }
23
+ /**
24
+ * Extracts field component map for array item schemas.
25
+ * - For Struct schemas: returns a map of field names to components
26
+ * - For primitive schemas: returns a single component
27
+ *
28
+ * @since 1.0.0
29
+ * @category Models
30
+ */
31
+ export type ArrayItemComponentMap<S extends Schema.Schema.Any> = S extends Schema.Struct<infer Fields> ? {
32
+ readonly [K in keyof Fields]: Fields[K] extends Schema.Schema.Any ? React.FC<FieldComponentProps<Fields[K]>> : never;
33
+ } : React.FC<FieldComponentProps<S>>;
34
+ /**
35
+ * Maps field names to their React components.
36
+ *
37
+ * @since 1.0.0
38
+ * @category Models
39
+ */
40
+ export type FieldComponentMap<TFields extends Form.FieldsRecord> = {
41
+ readonly [K in keyof TFields]: TFields[K] extends Form.FieldDef<any, infer S> ? React.FC<FieldComponentProps<S>> : TFields[K] extends Form.ArrayFieldDef<any, infer S> ? ArrayItemComponentMap<S> : never;
42
+ };
43
+ /**
44
+ * Maps field names to their type-safe Field references for setValue operations.
45
+ *
46
+ * @since 1.0.0
47
+ * @category Models
48
+ */
49
+ export type FieldRefs<TFields extends Form.FieldsRecord> = FormAtoms.FieldRefs<TFields>;
50
+ /**
51
+ * Operations available for array fields.
52
+ *
53
+ * @since 1.0.0
54
+ * @category Models
55
+ */
56
+ export interface ArrayFieldOperations<TItem> {
57
+ readonly items: ReadonlyArray<TItem>;
58
+ readonly append: (value?: TItem) => void;
59
+ readonly remove: (index: number) => void;
60
+ readonly swap: (indexA: number, indexB: number) => void;
61
+ readonly move: (from: number, to: number) => void;
62
+ }
63
+ /**
64
+ * State exposed to form.Subscribe render prop.
65
+ *
66
+ * @since 1.0.0
67
+ * @category Models
68
+ */
69
+ export interface SubscribeState<TFields extends Form.FieldsRecord> {
70
+ readonly values: Form.EncodedFromFields<TFields>;
71
+ readonly isDirty: boolean;
72
+ readonly submitResult: Result.Result<unknown, unknown>;
73
+ readonly submit: () => void;
74
+ readonly reset: () => void;
75
+ readonly setValue: <S>(field: Form.Field<S>, update: S | ((prev: S) => S)) => void;
76
+ readonly setValues: (values: Form.EncodedFromFields<TFields>) => void;
77
+ }
78
+ /**
79
+ * The result of building a form, containing all components and utilities needed
80
+ * for form rendering and submission.
81
+ *
82
+ * @since 1.0.0
83
+ * @category Models
84
+ */
85
+ export type BuiltForm<TFields extends Form.FieldsRecord, R> = {
86
+ readonly atom: Atom.Writable<Option.Option<Form.FormState<TFields>>, Option.Option<Form.FormState<TFields>>>;
87
+ readonly schema: Schema.Schema<Form.DecodedFromFields<TFields>, Form.EncodedFromFields<TFields>, R>;
88
+ readonly fields: FieldRefs<TFields>;
89
+ readonly Form: React.FC<{
90
+ readonly defaultValues: Form.EncodedFromFields<TFields>;
91
+ readonly onSubmit: Atom.AtomResultFn<Form.DecodedFromFields<TFields>, unknown, unknown>;
92
+ readonly children: React.ReactNode;
93
+ }>;
94
+ readonly Subscribe: React.FC<{
95
+ readonly children: (state: SubscribeState<TFields>) => React.ReactNode;
96
+ }>;
97
+ readonly useForm: () => {
98
+ readonly submit: () => void;
99
+ readonly reset: () => void;
100
+ readonly isDirty: boolean;
101
+ readonly submitResult: Result.Result<unknown, unknown>;
102
+ readonly values: Form.EncodedFromFields<TFields>;
103
+ readonly setValue: <S>(field: Form.Field<S>, update: S | ((prev: S) => S)) => void;
104
+ readonly setValues: (values: Form.EncodedFromFields<TFields>) => void;
105
+ };
106
+ readonly submit: <A, E>(fn: (values: Form.DecodedFromFields<TFields>, get: Atom.FnContext) => Effect.Effect<A, E, R>) => Atom.AtomResultFn<Form.DecodedFromFields<TFields>, A, E>;
107
+ } & FieldComponents<TFields>;
108
+ type FieldComponents<TFields extends Form.FieldsRecord> = {
109
+ readonly [K in keyof TFields]: TFields[K] extends Form.FieldDef<any, any> ? React.FC : TFields[K] extends Form.ArrayFieldDef<any, infer S> ? ArrayFieldComponent<S> : never;
110
+ };
111
+ type ArrayFieldComponent<S extends Schema.Schema.Any> = React.FC<{
112
+ readonly children: (ops: ArrayFieldOperations<Schema.Schema.Encoded<S>>) => React.ReactNode;
113
+ }> & {
114
+ readonly Item: React.FC<{
115
+ readonly index: number;
116
+ readonly children: React.ReactNode | ((props: {
117
+ readonly remove: () => void;
118
+ }) => React.ReactNode);
119
+ }>;
120
+ } & (S extends Schema.Struct<infer Fields> ? {
121
+ readonly [K in keyof Fields]: React.FC;
122
+ } : unknown);
123
+ /**
124
+ * Builds a React form from a FormBuilder.
125
+ *
126
+ * @example
127
+ * ```tsx
128
+ * import { Form } from "@lucas-barake/effect-form"
129
+ * import { FormReact } from "@lucas-barake/effect-form-react"
130
+ * import * as Atom from "@effect-atom/atom/Atom"
131
+ * import * as Schema from "effect/Schema"
132
+ * import * as Effect from "effect/Effect"
133
+ * import * as Layer from "effect/Layer"
134
+ *
135
+ * const runtime = Atom.runtime(Layer.empty)
136
+ *
137
+ * const loginForm = Form.empty
138
+ * .addField("email", Schema.String)
139
+ * .addField("password", Schema.String)
140
+ *
141
+ * const form = FormReact.build(loginForm, {
142
+ * runtime,
143
+ * fields: { email: TextInput, password: PasswordInput },
144
+ * })
145
+ *
146
+ * function LoginDialog({ onClose }) {
147
+ * const handleSubmit = form.submit((values) =>
148
+ * Effect.gen(function* () {
149
+ * yield* saveUser(values)
150
+ * onClose()
151
+ * })
152
+ * )
153
+ *
154
+ * return (
155
+ * <form.Form defaultValues={{ email: "", password: "" }} onSubmit={handleSubmit}>
156
+ * <form.email />
157
+ * <form.password />
158
+ * <form.Subscribe>
159
+ * {({ isDirty, submit }) => (
160
+ * <button onClick={submit} disabled={!isDirty}>Login</button>
161
+ * )}
162
+ * </form.Subscribe>
163
+ * </form.Form>
164
+ * )
165
+ * }
166
+ * ```
167
+ *
168
+ * @since 1.0.0
169
+ * @category Constructors
170
+ */
171
+ export declare const build: <TFields extends Form.FieldsRecord, R, ER = never>(self: Form.FormBuilder<TFields, R>, options: {
172
+ readonly runtime: Atom.AtomRuntime<R, ER>;
173
+ readonly fields: FieldComponentMap<TFields>;
174
+ readonly mode?: Mode.FormMode;
175
+ }) => BuiltForm<TFields, R>;
176
+ export {};
177
+ //# sourceMappingURL=FormReact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FormReact.d.ts","sourceRoot":"","sources":["../../src/FormReact.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=use-debounced.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-debounced.d.ts","sourceRoot":"","sources":["../../../src/internal/use-debounced.ts"],"names":[],"mappings":""}
@@ -0,0 +1,449 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * @since 1.0.0
4
+ */
5
+ import { RegistryContext, useAtom, useAtomSet, useAtomSubscribe, useAtomValue } from "@effect-atom/atom-react";
6
+ import * as Atom from "@effect-atom/atom/Atom";
7
+ import { Form, FormAtoms, Mode, Validation } from "@lucas-barake/effect-form";
8
+ import { getNestedValue, isPathOrParentDirty, schemaPathToFieldPath } from "@lucas-barake/effect-form/internal/path";
9
+ import * as Cause from "effect/Cause";
10
+ import * as Option from "effect/Option";
11
+ import * as ParseResult from "effect/ParseResult";
12
+ import * as AST from "effect/SchemaAST";
13
+ import * as React from "react";
14
+ import { createContext, useContext } from "react";
15
+ import { useDebounced } from "./internal/use-debounced.js";
16
+ const ArrayItemContext = /*#__PURE__*/createContext(null);
17
+ const AutoSubmitContext = /*#__PURE__*/createContext(null);
18
+ const makeFieldComponent = (fieldKey, fieldDef, crossFieldErrorsAtom, submitCountAtom, dirtyFieldsAtom, parsedMode, getOrCreateValidationAtom, getOrCreateFieldAtoms, Component) => {
19
+ const FieldComponent = React.memo(() => {
20
+ const arrayCtx = useContext(ArrayItemContext);
21
+ const autoSubmitOnBlur = useContext(AutoSubmitContext);
22
+ const fieldPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey;
23
+ const {
24
+ crossFieldErrorAtom,
25
+ touchedAtom,
26
+ valueAtom
27
+ } = React.useMemo(() => getOrCreateFieldAtoms(fieldPath), [fieldPath]);
28
+ const [value, setValue] = useAtom(valueAtom);
29
+ const [isTouched, setTouched] = useAtom(touchedAtom);
30
+ const crossFieldError = useAtomValue(crossFieldErrorAtom);
31
+ const setCrossFieldErrors = useAtomSet(crossFieldErrorsAtom);
32
+ const submitCount = useAtomValue(submitCountAtom);
33
+ const validationAtom = React.useMemo(() => getOrCreateValidationAtom(fieldPath, fieldDef.schema), [fieldPath]);
34
+ const validationResult = useAtomValue(validationAtom);
35
+ const validateImmediate = useAtomSet(validationAtom);
36
+ const shouldDebounceValidation = parsedMode.validation === "onChange" && parsedMode.debounce !== null && !parsedMode.autoSubmit;
37
+ const validate = useDebounced(validateImmediate, shouldDebounceValidation ? parsedMode.debounce : null);
38
+ const prevValueRef = React.useRef(value);
39
+ React.useEffect(() => {
40
+ if (prevValueRef.current === value) {
41
+ return;
42
+ }
43
+ prevValueRef.current = value;
44
+ const shouldValidate = parsedMode.validation === "onChange" || parsedMode.validation === "onBlur" && isTouched;
45
+ if (shouldValidate) {
46
+ validate(value);
47
+ }
48
+ }, [value, isTouched, validate]);
49
+ const perFieldError = React.useMemo(() => {
50
+ if (validationResult._tag === "Failure") {
51
+ const parseError = Cause.failureOption(validationResult.cause);
52
+ if (Option.isSome(parseError) && ParseResult.isParseError(parseError.value)) {
53
+ return Validation.extractFirstError(parseError.value);
54
+ }
55
+ }
56
+ return Option.none();
57
+ }, [validationResult]);
58
+ const validationError = Option.isSome(perFieldError) ? perFieldError : crossFieldError;
59
+ const onChange = React.useCallback(newValue => {
60
+ setValue(newValue);
61
+ setCrossFieldErrors(prev => {
62
+ if (prev.has(fieldPath)) {
63
+ const next = new Map(prev);
64
+ next.delete(fieldPath);
65
+ return next;
66
+ }
67
+ return prev;
68
+ });
69
+ if (parsedMode.validation === "onChange") {
70
+ validate(newValue);
71
+ }
72
+ }, [fieldPath, setValue, setCrossFieldErrors, validate]);
73
+ const onBlur = React.useCallback(() => {
74
+ setTouched(true);
75
+ if (parsedMode.validation === "onBlur") {
76
+ validate(value);
77
+ }
78
+ autoSubmitOnBlur?.();
79
+ }, [setTouched, validate, value, autoSubmitOnBlur]);
80
+ const dirtyFields = useAtomValue(dirtyFieldsAtom);
81
+ const isDirty = React.useMemo(() => isPathOrParentDirty(dirtyFields, fieldPath), [dirtyFields, fieldPath]);
82
+ const isValidating = validationResult.waiting;
83
+ const shouldShowError = isTouched || submitCount > 0;
84
+ return _jsx(Component, {
85
+ value: value,
86
+ onChange: onChange,
87
+ onBlur: onBlur,
88
+ error: shouldShowError ? validationError : Option.none(),
89
+ isTouched: isTouched,
90
+ isValidating: isValidating,
91
+ isDirty: isDirty
92
+ });
93
+ });
94
+ return FieldComponent;
95
+ };
96
+ const makeArrayFieldComponent = (fieldKey, def, stateAtom, crossFieldErrorsAtom, submitCountAtom, dirtyFieldsAtom, parsedMode, getOrCreateValidationAtom, getOrCreateFieldAtoms, operations, componentMap) => {
97
+ const isStructSchema = AST.isTypeLiteral(def.itemSchema.ast);
98
+ const ArrayWrapper = ({
99
+ children
100
+ }) => {
101
+ const arrayCtx = useContext(ArrayItemContext);
102
+ const [formStateOption, setFormState] = useAtom(stateAtom);
103
+ const formState = Option.getOrThrow(formStateOption);
104
+ const fieldPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey;
105
+ const items = React.useMemo(() => getNestedValue(formState.values, fieldPath) ?? [], [formState.values, fieldPath]);
106
+ const append = React.useCallback(value => {
107
+ setFormState(prev => {
108
+ if (Option.isNone(prev)) return prev;
109
+ return Option.some(operations.appendArrayItem(prev.value, fieldPath, def.itemSchema, value));
110
+ });
111
+ }, [fieldPath, setFormState]);
112
+ const remove = React.useCallback(index => {
113
+ setFormState(prev => {
114
+ if (Option.isNone(prev)) return prev;
115
+ return Option.some(operations.removeArrayItem(prev.value, fieldPath, index));
116
+ });
117
+ }, [fieldPath, setFormState]);
118
+ const swap = React.useCallback((indexA, indexB) => {
119
+ setFormState(prev => {
120
+ if (Option.isNone(prev)) return prev;
121
+ return Option.some(operations.swapArrayItems(prev.value, fieldPath, indexA, indexB));
122
+ });
123
+ }, [fieldPath, setFormState]);
124
+ const move = React.useCallback((from, to) => {
125
+ setFormState(prev => {
126
+ if (Option.isNone(prev)) return prev;
127
+ return Option.some(operations.moveArrayItem(prev.value, fieldPath, from, to));
128
+ });
129
+ }, [fieldPath, setFormState]);
130
+ return _jsx(_Fragment, {
131
+ children: children({
132
+ items,
133
+ append,
134
+ remove,
135
+ swap,
136
+ move
137
+ })
138
+ });
139
+ };
140
+ const ItemWrapper = ({
141
+ children,
142
+ index
143
+ }) => {
144
+ const arrayCtx = useContext(ArrayItemContext);
145
+ const setFormState = useAtomSet(stateAtom);
146
+ const parentPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey;
147
+ const itemPath = `${parentPath}[${index}]`;
148
+ const remove = React.useCallback(() => {
149
+ setFormState(prev => {
150
+ if (Option.isNone(prev)) return prev;
151
+ return Option.some(operations.removeArrayItem(prev.value, parentPath, index));
152
+ });
153
+ }, [parentPath, index, setFormState]);
154
+ return _jsx(ArrayItemContext.Provider, {
155
+ value: {
156
+ index,
157
+ parentPath: itemPath
158
+ },
159
+ children: typeof children === "function" ? children({
160
+ remove
161
+ }) : children
162
+ });
163
+ };
164
+ const itemFieldComponents = {};
165
+ if (isStructSchema) {
166
+ const ast = def.itemSchema.ast;
167
+ for (const prop of ast.propertySignatures) {
168
+ const itemKey = prop.name;
169
+ const itemSchema = {
170
+ ast: prop.type
171
+ };
172
+ const itemDef = Form.makeField(itemKey, itemSchema);
173
+ const itemComponent = componentMap[itemKey];
174
+ itemFieldComponents[itemKey] = makeFieldComponent(itemKey, itemDef, crossFieldErrorsAtom, submitCountAtom, dirtyFieldsAtom, parsedMode, getOrCreateValidationAtom, getOrCreateFieldAtoms, itemComponent);
175
+ }
176
+ }
177
+ const properties = {
178
+ Item: ItemWrapper,
179
+ ...itemFieldComponents
180
+ };
181
+ return new Proxy(ArrayWrapper, {
182
+ get(target, prop) {
183
+ if (prop in properties) {
184
+ return properties[prop];
185
+ }
186
+ return Reflect.get(target, prop);
187
+ }
188
+ });
189
+ };
190
+ const makeFieldComponents = (fields, stateAtom, crossFieldErrorsAtom, submitCountAtom, dirtyFieldsAtom, parsedMode, getOrCreateValidationAtom, getOrCreateFieldAtoms, operations, componentMap) => {
191
+ const components = {};
192
+ for (const [key, def] of Object.entries(fields)) {
193
+ if (Form.isArrayFieldDef(def)) {
194
+ const arrayComponentMap = componentMap[key];
195
+ components[key] = makeArrayFieldComponent(key, def, stateAtom, crossFieldErrorsAtom, submitCountAtom, dirtyFieldsAtom, parsedMode, getOrCreateValidationAtom, getOrCreateFieldAtoms, operations, arrayComponentMap);
196
+ } else if (Form.isFieldDef(def)) {
197
+ const fieldComponent = componentMap[key];
198
+ components[key] = makeFieldComponent(key, def, crossFieldErrorsAtom, submitCountAtom, dirtyFieldsAtom, parsedMode, getOrCreateValidationAtom, getOrCreateFieldAtoms, fieldComponent);
199
+ }
200
+ }
201
+ return components;
202
+ };
203
+ /**
204
+ * Builds a React form from a FormBuilder.
205
+ *
206
+ * @example
207
+ * ```tsx
208
+ * import { Form } from "@lucas-barake/effect-form"
209
+ * import { FormReact } from "@lucas-barake/effect-form-react"
210
+ * import * as Atom from "@effect-atom/atom/Atom"
211
+ * import * as Schema from "effect/Schema"
212
+ * import * as Effect from "effect/Effect"
213
+ * import * as Layer from "effect/Layer"
214
+ *
215
+ * const runtime = Atom.runtime(Layer.empty)
216
+ *
217
+ * const loginForm = Form.empty
218
+ * .addField("email", Schema.String)
219
+ * .addField("password", Schema.String)
220
+ *
221
+ * const form = FormReact.build(loginForm, {
222
+ * runtime,
223
+ * fields: { email: TextInput, password: PasswordInput },
224
+ * })
225
+ *
226
+ * function LoginDialog({ onClose }) {
227
+ * const handleSubmit = form.submit((values) =>
228
+ * Effect.gen(function* () {
229
+ * yield* saveUser(values)
230
+ * onClose()
231
+ * })
232
+ * )
233
+ *
234
+ * return (
235
+ * <form.Form defaultValues={{ email: "", password: "" }} onSubmit={handleSubmit}>
236
+ * <form.email />
237
+ * <form.password />
238
+ * <form.Subscribe>
239
+ * {({ isDirty, submit }) => (
240
+ * <button onClick={submit} disabled={!isDirty}>Login</button>
241
+ * )}
242
+ * </form.Subscribe>
243
+ * </form.Form>
244
+ * )
245
+ * }
246
+ * ```
247
+ *
248
+ * @since 1.0.0
249
+ * @category Constructors
250
+ */
251
+ export const build = (self, options) => {
252
+ const {
253
+ fields: components,
254
+ mode,
255
+ runtime
256
+ } = options;
257
+ const parsedMode = Mode.parse(mode);
258
+ const {
259
+ fields
260
+ } = self;
261
+ const formAtoms = FormAtoms.make({
262
+ formBuilder: self,
263
+ runtime
264
+ });
265
+ const {
266
+ combinedSchema,
267
+ crossFieldErrorsAtom,
268
+ decodeAndSubmit,
269
+ dirtyFieldsAtom,
270
+ fieldRefs,
271
+ getOrCreateFieldAtoms,
272
+ getOrCreateValidationAtom,
273
+ isDirtyAtom,
274
+ onSubmitAtom,
275
+ operations,
276
+ resetValidationAtoms,
277
+ stateAtom,
278
+ submitCountAtom
279
+ } = formAtoms;
280
+ const FormComponent = ({
281
+ children,
282
+ defaultValues,
283
+ onSubmit
284
+ }) => {
285
+ const registry = React.useContext(RegistryContext);
286
+ const state = useAtomValue(stateAtom);
287
+ const setFormState = useAtomSet(stateAtom);
288
+ const setOnSubmit = useAtomSet(onSubmitAtom);
289
+ const callDecodeAndSubmit = useAtomSet(decodeAndSubmit);
290
+ React.useEffect(() => {
291
+ setOnSubmit(onSubmit);
292
+ }, [onSubmit, setOnSubmit]);
293
+ React.useEffect(() => {
294
+ setFormState(Option.some(operations.createInitialState(defaultValues)));
295
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only
296
+ }, []);
297
+ const debouncedAutoSubmit = useDebounced(() => {
298
+ const stateOption = registry.get(stateAtom);
299
+ if (Option.isNone(stateOption)) return;
300
+ callDecodeAndSubmit(stateOption.value.values);
301
+ }, parsedMode.autoSubmit && parsedMode.validation === "onChange" ? parsedMode.debounce : null);
302
+ useAtomSubscribe(stateAtom, React.useCallback(() => {
303
+ if (parsedMode.autoSubmit && parsedMode.validation === "onChange") {
304
+ debouncedAutoSubmit();
305
+ }
306
+ }, [debouncedAutoSubmit]), {
307
+ immediate: false
308
+ });
309
+ const onBlurAutoSubmit = React.useCallback(() => {
310
+ if (parsedMode.autoSubmit && parsedMode.validation === "onBlur") {
311
+ const stateOption = registry.get(stateAtom);
312
+ if (Option.isNone(stateOption)) return;
313
+ callDecodeAndSubmit(stateOption.value.values);
314
+ }
315
+ }, [registry, callDecodeAndSubmit]);
316
+ if (Option.isNone(state)) return null;
317
+ return _jsx(AutoSubmitContext.Provider, {
318
+ value: onBlurAutoSubmit,
319
+ children: _jsx("form", {
320
+ onSubmit: e => {
321
+ e.preventDefault();
322
+ e.stopPropagation();
323
+ },
324
+ children: children
325
+ })
326
+ });
327
+ };
328
+ const useFormHook = () => {
329
+ const registry = React.useContext(RegistryContext);
330
+ const formValues = Option.getOrThrow(useAtomValue(stateAtom)).values;
331
+ const setFormState = useAtomSet(stateAtom);
332
+ const setCrossFieldErrors = useAtomSet(crossFieldErrorsAtom);
333
+ const [decodeAndSubmitResult, callDecodeAndSubmit] = useAtom(decodeAndSubmit);
334
+ const isDirty = useAtomValue(isDirtyAtom);
335
+ React.useEffect(() => {
336
+ if (decodeAndSubmitResult._tag === "Failure") {
337
+ const parseError = Cause.failureOption(decodeAndSubmitResult.cause);
338
+ if (Option.isSome(parseError) && ParseResult.isParseError(parseError.value)) {
339
+ const issues = ParseResult.ArrayFormatter.formatErrorSync(parseError.value);
340
+ const fieldErrors = new Map();
341
+ for (const issue of issues) {
342
+ if (issue.path.length > 0) {
343
+ const fieldPath = schemaPathToFieldPath(issue.path);
344
+ if (!fieldErrors.has(fieldPath)) {
345
+ fieldErrors.set(fieldPath, issue.message);
346
+ }
347
+ }
348
+ }
349
+ if (fieldErrors.size > 0) {
350
+ setCrossFieldErrors(fieldErrors);
351
+ }
352
+ }
353
+ }
354
+ }, [decodeAndSubmitResult, setCrossFieldErrors]);
355
+ const submit = React.useCallback(() => {
356
+ const stateOption = registry.get(stateAtom);
357
+ if (Option.isNone(stateOption)) return;
358
+ setCrossFieldErrors(new Map());
359
+ setFormState(prev => {
360
+ if (Option.isNone(prev)) return prev;
361
+ return Option.some(operations.createSubmitState(prev.value));
362
+ });
363
+ callDecodeAndSubmit(stateOption.value.values);
364
+ }, [setFormState, callDecodeAndSubmit, setCrossFieldErrors, registry]);
365
+ const reset = React.useCallback(() => {
366
+ setFormState(prev => {
367
+ if (Option.isNone(prev)) return prev;
368
+ return Option.some(operations.createResetState(prev.value));
369
+ });
370
+ setCrossFieldErrors(new Map());
371
+ resetValidationAtoms(registry);
372
+ callDecodeAndSubmit(Atom.Reset);
373
+ }, [setFormState, setCrossFieldErrors, callDecodeAndSubmit, registry]);
374
+ const setValue = React.useCallback((field, update) => {
375
+ const path = field.key;
376
+ setFormState(prev => {
377
+ if (Option.isNone(prev)) return prev;
378
+ const state = prev.value;
379
+ const currentValue = getNestedValue(state.values, path);
380
+ const newValue = typeof update === "function" ? update(currentValue) : update;
381
+ return Option.some(operations.setFieldValue(state, path, newValue));
382
+ });
383
+ setCrossFieldErrors(prev => {
384
+ let changed = false;
385
+ const next = new Map(prev);
386
+ for (const errorPath of prev.keys()) {
387
+ if (errorPath === path || errorPath.startsWith(path + ".") || errorPath.startsWith(path + "[")) {
388
+ next.delete(errorPath);
389
+ changed = true;
390
+ }
391
+ }
392
+ return changed ? next : prev;
393
+ });
394
+ }, [setFormState, setCrossFieldErrors]);
395
+ const setValues = React.useCallback(values => {
396
+ setFormState(prev => {
397
+ if (Option.isNone(prev)) return prev;
398
+ return Option.some(operations.setFormValues(prev.value, values));
399
+ });
400
+ setCrossFieldErrors(new Map());
401
+ }, [setFormState, setCrossFieldErrors]);
402
+ return {
403
+ submit,
404
+ reset,
405
+ isDirty,
406
+ submitResult: decodeAndSubmitResult,
407
+ values: formValues,
408
+ setValue,
409
+ setValues
410
+ };
411
+ };
412
+ const SubscribeComponent = ({
413
+ children
414
+ }) => {
415
+ const {
416
+ isDirty,
417
+ reset,
418
+ setValue,
419
+ setValues,
420
+ submit,
421
+ submitResult,
422
+ values
423
+ } = useFormHook();
424
+ return _jsx(_Fragment, {
425
+ children: children({
426
+ values,
427
+ isDirty,
428
+ submitResult,
429
+ submit,
430
+ reset,
431
+ setValue,
432
+ setValues
433
+ })
434
+ });
435
+ };
436
+ const submitHelper = fn => runtime.fn()(fn);
437
+ const fieldComponents = makeFieldComponents(fields, stateAtom, crossFieldErrorsAtom, submitCountAtom, dirtyFieldsAtom, parsedMode, getOrCreateValidationAtom, getOrCreateFieldAtoms, operations, components);
438
+ return {
439
+ atom: stateAtom,
440
+ schema: combinedSchema,
441
+ fields: fieldRefs,
442
+ Form: FormComponent,
443
+ Subscribe: SubscribeComponent,
444
+ useForm: useFormHook,
445
+ submit: submitHelper,
446
+ ...fieldComponents
447
+ };
448
+ };
449
+ //# sourceMappingURL=FormReact.js.map