@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/dist/cjs/FormReact.js +7 -147
- package/dist/cjs/FormReact.js.map +1 -1
- package/dist/cjs/internal/use-debounced.js +0 -11
- package/dist/cjs/internal/use-debounced.js.map +1 -1
- package/dist/dts/FormReact.d.ts +11 -184
- package/dist/dts/FormReact.d.ts.map +1 -1
- package/dist/dts/internal/use-debounced.d.ts +1 -1
- package/dist/dts/internal/use-debounced.d.ts.map +1 -1
- package/dist/esm/FormReact.js +7 -145
- package/dist/esm/FormReact.js.map +1 -1
- package/dist/esm/internal/use-debounced.js +0 -10
- package/dist/esm/internal/use-debounced.js.map +1 -1
- package/package.json +2 -2
- package/src/FormReact.tsx +287 -508
- package/src/internal/use-debounced.ts +0 -10
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
|
|
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
|
|
19
|
-
import
|
|
20
|
-
import * as
|
|
21
|
-
import
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
readonly
|
|
32
|
-
readonly
|
|
33
|
-
readonly
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
|
91
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
|
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
|
-
?
|
|
240
|
+
? isDirty || submitCount > 0
|
|
314
241
|
: parsedMode.validation === "onBlur"
|
|
315
|
-
?
|
|
316
|
-
: submitCount > 0
|
|
317
|
-
|
|
318
|
-
const fieldState: FieldState<S
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
+
};
|