remix-validated-form 4.1.4 → 4.1.7
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/.turbo/turbo-build.log +2 -2
- package/browser/ValidatedForm.js +31 -36
- package/browser/hooks.js +13 -16
- package/browser/internal/customState.d.ts +105 -0
- package/browser/internal/customState.js +46 -0
- package/browser/internal/getInputProps.js +4 -14
- package/browser/internal/hooks.d.ts +14 -15
- package/browser/internal/hooks.js +37 -39
- package/browser/internal/logic/elementUtils.d.ts +3 -0
- package/browser/internal/logic/elementUtils.js +3 -0
- package/browser/internal/logic/setInputValueInForm.js +9 -52
- package/browser/internal/setFieldValue.d.ts +0 -0
- package/browser/internal/setFieldValue.js +0 -0
- package/browser/internal/setFormValues.d.ts +0 -0
- package/browser/internal/setFormValues.js +0 -0
- package/browser/internal/state.d.ts +339 -238
- package/browser/internal/state.js +59 -72
- package/browser/internal/watch.d.ts +18 -0
- package/browser/internal/watch.js +122 -0
- package/browser/unreleased/formStateHooks.d.ts +39 -0
- package/browser/unreleased/formStateHooks.js +54 -0
- package/browser/userFacingFormContext.d.ts +4 -0
- package/browser/userFacingFormContext.js +9 -23
- package/build/ValidatedForm.js +30 -35
- package/build/hooks.js +11 -14
- package/build/internal/getInputProps.js +4 -14
- package/build/internal/hooks.d.ts +14 -15
- package/build/internal/hooks.js +39 -41
- package/build/internal/logic/elementUtils.d.ts +3 -0
- package/build/internal/logic/elementUtils.js +9 -0
- package/build/internal/logic/setInputValueInForm.js +12 -55
- package/build/internal/setFormValues.d.ts +0 -0
- package/build/internal/setFormValues.js +0 -0
- package/build/internal/state/controlledFields.js +11 -2
- package/build/internal/state.d.ts +339 -238
- package/build/internal/state.js +61 -77
- package/build/internal/watch.d.ts +20 -0
- package/build/internal/watch.js +126 -0
- package/build/unreleased/formStateHooks.d.ts +39 -0
- package/build/unreleased/formStateHooks.js +59 -0
- package/build/userFacingFormContext.d.ts +4 -0
- package/build/userFacingFormContext.js +9 -23
- package/package.json +1 -2
- package/src/ValidatedForm.tsx +48 -52
- package/src/hooks.ts +15 -26
- package/src/internal/getInputProps.ts +4 -14
- package/src/internal/hooks.ts +60 -72
- package/src/internal/hydratable.ts +28 -0
- package/src/internal/logic/getCheckboxChecked.ts +10 -0
- package/src/internal/logic/getRadioChecked.ts +7 -0
- package/src/internal/state/atomUtils.ts +13 -0
- package/src/internal/state.ts +99 -177
- package/src/unreleased/formStateHooks.ts +113 -0
- package/src/userFacingFormContext.ts +18 -53
package/src/ValidatedForm.tsx
CHANGED
@@ -25,15 +25,16 @@ import {
|
|
25
25
|
} from "./internal/hooks";
|
26
26
|
import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
|
27
27
|
import {
|
28
|
-
|
29
|
-
clearErrorAtom,
|
28
|
+
cleanupFormState,
|
30
29
|
endSubmitAtom,
|
31
|
-
|
32
|
-
|
30
|
+
fieldErrorsAtom,
|
31
|
+
formElementAtom,
|
32
|
+
formPropsAtom,
|
33
|
+
isHydratedAtom,
|
33
34
|
resetAtom,
|
34
|
-
|
35
|
+
setFieldErrorAtom,
|
35
36
|
startSubmitAtom,
|
36
|
-
|
37
|
+
SyncedFormProps,
|
37
38
|
} from "./internal/state";
|
38
39
|
import { useSubmitComplete } from "./internal/submissionCallbacks";
|
39
40
|
import {
|
@@ -192,12 +193,6 @@ function formEventProxy<T extends object>(event: T): T {
|
|
192
193
|
}) as T;
|
193
194
|
}
|
194
195
|
|
195
|
-
const useFormAtom = (formId: string | symbol) => {
|
196
|
-
const formAtom = formRegistry(formId);
|
197
|
-
useEffect(() => () => formRegistry.remove(formId), [formId]);
|
198
|
-
return formAtom;
|
199
|
-
};
|
200
|
-
|
201
196
|
/**
|
202
197
|
* The primary form component of `remix-validated-form`.
|
203
198
|
*/
|
@@ -219,7 +214,6 @@ export function ValidatedForm<DataType>({
|
|
219
214
|
...rest
|
220
215
|
}: FormProps<DataType>) {
|
221
216
|
const formId = useFormId(id);
|
222
|
-
const formAtom = useFormAtom(formId);
|
223
217
|
const providedDefaultValues = useDeepEqualsMemo(unMemoizedDefaults);
|
224
218
|
const contextValue = useMemo<InternalFormContextValue>(
|
225
219
|
() => ({
|
@@ -238,73 +232,75 @@ export function ValidatedForm<DataType>({
|
|
238
232
|
const Form = fetcher?.Form ?? RemixForm;
|
239
233
|
|
240
234
|
const submit = useSubmit();
|
241
|
-
const
|
242
|
-
const
|
243
|
-
const
|
244
|
-
const
|
245
|
-
const
|
246
|
-
const
|
247
|
-
const
|
235
|
+
const setFieldErrors = useFormUpdateAtom(fieldErrorsAtom(formId));
|
236
|
+
const setFieldError = useFormUpdateAtom(setFieldErrorAtom(formId));
|
237
|
+
const reset = useFormUpdateAtom(resetAtom(formId));
|
238
|
+
const startSubmit = useFormUpdateAtom(startSubmitAtom(formId));
|
239
|
+
const endSubmit = useFormUpdateAtom(endSubmitAtom(formId));
|
240
|
+
const syncFormProps = useFormUpdateAtom(formPropsAtom(formId));
|
241
|
+
const setHydrated = useFormUpdateAtom(isHydratedAtom(formId));
|
242
|
+
const setFormElementInState = useFormUpdateAtom(formElementAtom(formId));
|
248
243
|
|
249
|
-
|
250
|
-
|
244
|
+
useEffect(() => {
|
245
|
+
setHydrated(true);
|
246
|
+
return () => cleanupFormState(formId);
|
247
|
+
}, [formId, setHydrated]);
|
248
|
+
|
249
|
+
const validateField: SyncedFormProps["validateField"] = useCallback(
|
250
|
+
async (field) => {
|
251
251
|
invariant(formRef.current, "Cannot find reference to form");
|
252
252
|
const { error } = await validator.validateField(
|
253
253
|
getDataFromForm(formRef.current),
|
254
|
-
|
254
|
+
field
|
255
255
|
);
|
256
256
|
|
257
257
|
if (error) {
|
258
|
-
|
258
|
+
setFieldError({ field, error });
|
259
259
|
return error;
|
260
260
|
} else {
|
261
|
-
|
261
|
+
setFieldError({ field, error: undefined });
|
262
262
|
return null;
|
263
263
|
}
|
264
264
|
},
|
265
|
-
[
|
265
|
+
[setFieldError, validator]
|
266
266
|
);
|
267
267
|
|
268
268
|
const customFocusHandlers = useMultiValueMap<string, () => void>();
|
269
|
-
const registerReceiveFocus:
|
270
|
-
(
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
269
|
+
const registerReceiveFocus: SyncedFormProps["registerReceiveFocus"] =
|
270
|
+
useCallback(
|
271
|
+
(fieldName, handler) => {
|
272
|
+
customFocusHandlers().add(fieldName, handler);
|
273
|
+
return () => {
|
274
|
+
customFocusHandlers().remove(fieldName, handler);
|
275
|
+
};
|
276
|
+
},
|
277
|
+
[customFocusHandlers]
|
278
|
+
);
|
278
279
|
|
279
280
|
useLayoutEffect(() => {
|
280
|
-
|
281
|
-
formAtom,
|
281
|
+
syncFormProps({
|
282
282
|
action,
|
283
|
-
defaultValues: providedDefaultValues ?? backendDefaultValues,
|
283
|
+
defaultValues: providedDefaultValues ?? backendDefaultValues ?? {},
|
284
284
|
subaction,
|
285
285
|
validateField,
|
286
286
|
registerReceiveFocus,
|
287
287
|
});
|
288
288
|
}, [
|
289
289
|
action,
|
290
|
-
formAtom,
|
291
290
|
providedDefaultValues,
|
292
291
|
registerReceiveFocus,
|
293
292
|
subaction,
|
294
|
-
|
293
|
+
syncFormProps,
|
295
294
|
validateField,
|
296
295
|
backendDefaultValues,
|
297
296
|
]);
|
298
297
|
|
299
298
|
useEffect(() => {
|
300
|
-
setFieldErrors({
|
301
|
-
|
302
|
-
formAtom,
|
303
|
-
});
|
304
|
-
}, [backendError?.fieldErrors, formAtom, setFieldErrors]);
|
299
|
+
setFieldErrors(backendError?.fieldErrors ?? {});
|
300
|
+
}, [backendError?.fieldErrors, setFieldErrors, setFieldError]);
|
305
301
|
|
306
302
|
useSubmitComplete(hasActiveSubmission, () => {
|
307
|
-
endSubmit(
|
303
|
+
endSubmit();
|
308
304
|
});
|
309
305
|
|
310
306
|
let clickedButtonRef = React.useRef<any>();
|
@@ -334,11 +330,11 @@ export function ValidatedForm<DataType>({
|
|
334
330
|
}, []);
|
335
331
|
|
336
332
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
337
|
-
startSubmit(
|
333
|
+
startSubmit();
|
338
334
|
const result = await validator.validate(getDataFromForm(e.currentTarget));
|
339
335
|
if (result.error) {
|
340
|
-
endSubmit(
|
341
|
-
setFieldErrors(
|
336
|
+
endSubmit();
|
337
|
+
setFieldErrors(result.error.fieldErrors);
|
342
338
|
if (!disableFocusOnError) {
|
343
339
|
focusFirstInvalidInput(
|
344
340
|
result.error.fieldErrors,
|
@@ -350,7 +346,7 @@ export function ValidatedForm<DataType>({
|
|
350
346
|
const eventProxy = formEventProxy(e);
|
351
347
|
await onSubmit?.(result.data, eventProxy);
|
352
348
|
if (eventProxy.defaultPrevented) {
|
353
|
-
endSubmit(
|
349
|
+
endSubmit();
|
354
350
|
return;
|
355
351
|
}
|
356
352
|
|
@@ -372,7 +368,7 @@ export function ValidatedForm<DataType>({
|
|
372
368
|
|
373
369
|
return (
|
374
370
|
<Form
|
375
|
-
ref={mergeRefs([formRef, formRefProp])}
|
371
|
+
ref={mergeRefs([formRef, formRefProp, setFormElementInState])}
|
376
372
|
{...rest}
|
377
373
|
id={id}
|
378
374
|
action={action}
|
@@ -385,7 +381,7 @@ export function ValidatedForm<DataType>({
|
|
385
381
|
onReset={(event) => {
|
386
382
|
onReset?.(event);
|
387
383
|
if (event.defaultPrevented) return;
|
388
|
-
reset(
|
384
|
+
reset();
|
389
385
|
}}
|
390
386
|
>
|
391
387
|
<InternalFormContext.Provider value={contextValue}>
|
package/src/hooks.ts
CHANGED
@@ -8,17 +8,14 @@ import {
|
|
8
8
|
useInternalFormContext,
|
9
9
|
useFieldTouched,
|
10
10
|
useFieldError,
|
11
|
+
useFormAtomValue,
|
11
12
|
useFieldDefaultValue,
|
12
|
-
useContextSelectAtom,
|
13
|
-
useClearError,
|
14
|
-
useSetTouched,
|
15
13
|
} from "./internal/hooks";
|
16
14
|
import {
|
15
|
+
formPropsAtom,
|
17
16
|
hasBeenSubmittedAtom,
|
18
17
|
isSubmittingAtom,
|
19
18
|
isValidAtom,
|
20
|
-
registerReceiveFocusAtom,
|
21
|
-
validateFieldAtom,
|
22
19
|
} from "./internal/state";
|
23
20
|
|
24
21
|
/**
|
@@ -30,7 +27,7 @@ import {
|
|
30
27
|
*/
|
31
28
|
export const useIsSubmitting = (formId?: string) => {
|
32
29
|
const formContext = useInternalFormContext(formId, "useIsSubmitting");
|
33
|
-
return
|
30
|
+
return useFormAtomValue(isSubmittingAtom(formContext.formId));
|
34
31
|
};
|
35
32
|
|
36
33
|
/**
|
@@ -40,7 +37,7 @@ export const useIsSubmitting = (formId?: string) => {
|
|
40
37
|
*/
|
41
38
|
export const useIsValid = (formId?: string) => {
|
42
39
|
const formContext = useInternalFormContext(formId, "useIsValid");
|
43
|
-
return
|
40
|
+
return useFormAtomValue(isValidAtom(formContext.formId));
|
44
41
|
};
|
45
42
|
|
46
43
|
export type FieldProps = {
|
@@ -97,26 +94,18 @@ export const useField = (
|
|
97
94
|
formId?: string;
|
98
95
|
}
|
99
96
|
): FieldProps => {
|
100
|
-
const {
|
97
|
+
const { formId: providedFormId, handleReceiveFocus } = options ?? {};
|
101
98
|
const formContext = useInternalFormContext(providedFormId, "useField");
|
102
99
|
|
103
100
|
const defaultValue = useFieldDefaultValue(name, formContext);
|
104
|
-
const touched = useFieldTouched(name, formContext);
|
105
|
-
const error = useFieldError(name, formContext);
|
101
|
+
const [touched, setTouched] = useFieldTouched(name, formContext);
|
102
|
+
const [error, setError] = useFieldError(name, formContext);
|
106
103
|
|
107
|
-
const
|
108
|
-
|
109
|
-
const hasBeenSubmitted = useContextSelectAtom(
|
110
|
-
formContext.formId,
|
111
|
-
hasBeenSubmittedAtom
|
104
|
+
const hasBeenSubmitted = useFormAtomValue(
|
105
|
+
hasBeenSubmittedAtom(formContext.formId)
|
112
106
|
);
|
113
|
-
const validateField =
|
114
|
-
formContext.formId
|
115
|
-
validateFieldAtom
|
116
|
-
);
|
117
|
-
const registerReceiveFocus = useContextSelectAtom(
|
118
|
-
formContext.formId,
|
119
|
-
registerReceiveFocusAtom
|
107
|
+
const { validateField, registerReceiveFocus } = useFormAtomValue(
|
108
|
+
formPropsAtom(formContext.formId)
|
120
109
|
);
|
121
110
|
|
122
111
|
useEffect(() => {
|
@@ -127,13 +116,13 @@ export const useField = (
|
|
127
116
|
const field = useMemo<FieldProps>(() => {
|
128
117
|
const helpers = {
|
129
118
|
error,
|
130
|
-
clearError: () =>
|
119
|
+
clearError: () => setError(undefined),
|
131
120
|
validate: () => {
|
132
121
|
validateField(name);
|
133
122
|
},
|
134
123
|
defaultValue,
|
135
124
|
touched,
|
136
|
-
setTouched
|
125
|
+
setTouched,
|
137
126
|
};
|
138
127
|
const getInputProps = createGetInputProps({
|
139
128
|
...helpers,
|
@@ -149,12 +138,12 @@ export const useField = (
|
|
149
138
|
error,
|
150
139
|
defaultValue,
|
151
140
|
touched,
|
141
|
+
setTouched,
|
152
142
|
name,
|
153
143
|
hasBeenSubmitted,
|
154
144
|
options?.validationBehavior,
|
155
|
-
|
145
|
+
setError,
|
156
146
|
validateField,
|
157
|
-
setTouched,
|
158
147
|
]);
|
159
148
|
|
160
149
|
return field;
|
@@ -1,4 +1,6 @@
|
|
1
1
|
import omitBy from "lodash/omitBy";
|
2
|
+
import { getCheckboxChecked } from "./logic/getCheckboxChecked";
|
3
|
+
import { getRadioChecked } from "./logic/getRadioChecked";
|
2
4
|
|
3
5
|
export type ValidationBehavior = "onBlur" | "onChange" | "onSubmit";
|
4
6
|
|
@@ -41,13 +43,6 @@ const defaultValidationBehavior: ValidationBehaviorOptions = {
|
|
41
43
|
whenSubmitted: "onChange",
|
42
44
|
};
|
43
45
|
|
44
|
-
const getCheckboxDefaultChecked = (value: string, defaultValue: any) => {
|
45
|
-
if (Array.isArray(defaultValue)) return defaultValue.includes(value);
|
46
|
-
if (typeof defaultValue === "boolean") return defaultValue;
|
47
|
-
if (typeof defaultValue === "string") return defaultValue === value;
|
48
|
-
return undefined;
|
49
|
-
};
|
50
|
-
|
51
46
|
export const createGetInputProps = ({
|
52
47
|
clearError,
|
53
48
|
validate,
|
@@ -86,14 +81,9 @@ export const createGetInputProps = ({
|
|
86
81
|
};
|
87
82
|
|
88
83
|
if (props.type === "checkbox") {
|
89
|
-
|
90
|
-
inputProps.defaultChecked = getCheckboxDefaultChecked(
|
91
|
-
value,
|
92
|
-
defaultValue
|
93
|
-
);
|
84
|
+
inputProps.defaultChecked = getCheckboxChecked(props.value, defaultValue);
|
94
85
|
} else if (props.type === "radio") {
|
95
|
-
|
96
|
-
inputProps.defaultChecked = defaultValue === value;
|
86
|
+
inputProps.defaultChecked = getRadioChecked(props.value, defaultValue);
|
97
87
|
} else {
|
98
88
|
inputProps.defaultValue = defaultValue;
|
99
89
|
}
|
package/src/internal/hooks.ts
CHANGED
@@ -1,26 +1,32 @@
|
|
1
1
|
import { useActionData, useMatches, useTransition } from "@remix-run/react";
|
2
|
-
import { Atom } from "jotai";
|
2
|
+
import { Atom, useAtom, WritableAtom } from "jotai";
|
3
3
|
import { useAtomValue, useUpdateAtom } from "jotai/utils";
|
4
4
|
import lodashGet from "lodash/get";
|
5
|
-
import
|
6
|
-
import
|
7
|
-
import { ValidationErrorResponseData } from "..";
|
5
|
+
import { useCallback, useContext } from "react";
|
6
|
+
import invariant from "tiny-invariant";
|
7
|
+
import { FieldErrors, ValidationErrorResponseData } from "..";
|
8
8
|
import { formDefaultValuesKey } from "./constants";
|
9
9
|
import { InternalFormContext, InternalFormContextValue } from "./formContext";
|
10
|
+
import { Hydratable, hydratable } from "./hydratable";
|
10
11
|
import {
|
11
12
|
ATOM_SCOPE,
|
12
|
-
clearErrorAtom,
|
13
|
-
fieldDefaultValueAtom,
|
14
13
|
fieldErrorAtom,
|
15
14
|
fieldTouchedAtom,
|
16
|
-
|
17
|
-
formRegistry,
|
15
|
+
formPropsAtom,
|
18
16
|
isHydratedAtom,
|
17
|
+
setFieldErrorAtom,
|
19
18
|
setTouchedAtom,
|
20
19
|
} from "./state";
|
21
20
|
|
22
|
-
|
23
|
-
|
21
|
+
export const useFormUpdateAtom: typeof useUpdateAtom = (atom) =>
|
22
|
+
useUpdateAtom(atom, ATOM_SCOPE);
|
23
|
+
|
24
|
+
export const useFormAtom = <Value, Update, Result extends void | Promise<void>>(
|
25
|
+
anAtom: WritableAtom<Value, Update, Result>
|
26
|
+
) => useAtom(anAtom, ATOM_SCOPE);
|
27
|
+
|
28
|
+
export const useFormAtomValue = <Value>(anAtom: Atom<Value>) =>
|
29
|
+
useAtomValue(anAtom, ATOM_SCOPE);
|
24
30
|
|
25
31
|
export const useInternalFormContext = (
|
26
32
|
formId?: string | symbol,
|
@@ -32,32 +38,10 @@ export const useInternalFormContext = (
|
|
32
38
|
if (formContext) return formContext;
|
33
39
|
|
34
40
|
throw new Error(
|
35
|
-
`Unable to determine form for ${hookName}. Please use it inside a
|
41
|
+
`Unable to determine form for ${hookName}. Please use it inside a ValidatedForm or pass a 'formId'.`
|
36
42
|
);
|
37
43
|
};
|
38
44
|
|
39
|
-
export const useContextSelectAtom = <T>(
|
40
|
-
formId: string | symbol,
|
41
|
-
selectorAtomCreator: FormSelectorAtomCreator<T>
|
42
|
-
) => {
|
43
|
-
const formAtom = formRegistry(formId);
|
44
|
-
const selectorAtom = useMemo(
|
45
|
-
() => selectorAtomCreator(formAtom),
|
46
|
-
[formAtom, selectorAtomCreator]
|
47
|
-
);
|
48
|
-
return useAtomValue(selectorAtom, ATOM_SCOPE);
|
49
|
-
};
|
50
|
-
|
51
|
-
export const useHydratableSelector = <T, U>(
|
52
|
-
{ formId }: InternalFormContextValue,
|
53
|
-
atomCreator: FormSelectorAtomCreator<T>,
|
54
|
-
dataToUse: U | typeof USE_HYDRATED_STATE,
|
55
|
-
selector: (data: U) => T = identity
|
56
|
-
) => {
|
57
|
-
const dataFromState = useContextSelectAtom(formId, atomCreator);
|
58
|
-
return dataToUse === USE_HYDRATED_STATE ? dataFromState : selector(dataToUse);
|
59
|
-
};
|
60
|
-
|
61
45
|
export function useErrorResponseForForm({
|
62
46
|
fetcher,
|
63
47
|
subaction,
|
@@ -84,10 +68,12 @@ export function useErrorResponseForForm({
|
|
84
68
|
return null;
|
85
69
|
}
|
86
70
|
|
87
|
-
export const useFieldErrorsForForm = (
|
71
|
+
export const useFieldErrorsForForm = (
|
72
|
+
context: InternalFormContextValue
|
73
|
+
): Hydratable<FieldErrors | undefined> => {
|
88
74
|
const response = useErrorResponseForForm(context);
|
89
|
-
const hydrated =
|
90
|
-
return
|
75
|
+
const hydrated = useFormAtomValue(isHydratedAtom(context.formId));
|
76
|
+
return hydratable.from(response?.fieldErrors, hydrated);
|
91
77
|
};
|
92
78
|
|
93
79
|
export const useDefaultValuesFromLoader = ({
|
@@ -107,9 +93,11 @@ export const useDefaultValuesFromLoader = ({
|
|
107
93
|
return null;
|
108
94
|
};
|
109
95
|
|
110
|
-
export const useDefaultValuesForForm = (
|
96
|
+
export const useDefaultValuesForForm = (
|
97
|
+
context: InternalFormContextValue
|
98
|
+
): Hydratable<{ [fieldName: string]: any }> => {
|
111
99
|
const { formId, defaultValuesProp } = context;
|
112
|
-
const hydrated =
|
100
|
+
const hydrated = useFormAtomValue(isHydratedAtom(formId));
|
113
101
|
const errorResponse = useErrorResponseForForm(context);
|
114
102
|
const defaultValuesFromLoader = useDefaultValuesFromLoader(context);
|
115
103
|
|
@@ -119,10 +107,17 @@ export const useDefaultValuesForForm = (context: InternalFormContextValue) => {
|
|
119
107
|
// - State gets hydrated with default values
|
120
108
|
// - After submit, we may need to use values from the error
|
121
109
|
|
122
|
-
if (hydrated) return
|
123
|
-
if (errorResponse?.repopulateFields)
|
124
|
-
|
125
|
-
|
110
|
+
if (hydrated) return hydratable.hydratedData();
|
111
|
+
if (errorResponse?.repopulateFields) {
|
112
|
+
invariant(
|
113
|
+
typeof errorResponse.repopulateFields === "object",
|
114
|
+
"repopulateFields returned something other than an object"
|
115
|
+
);
|
116
|
+
return hydratable.serverData(errorResponse.repopulateFields);
|
117
|
+
}
|
118
|
+
if (defaultValuesProp) return hydratable.serverData(defaultValuesProp);
|
119
|
+
|
120
|
+
return hydratable.serverData(defaultValuesFromLoader);
|
126
121
|
};
|
127
122
|
|
128
123
|
export const useHasActiveFormSubmit = ({
|
@@ -136,56 +131,49 @@ export const useHasActiveFormSubmit = ({
|
|
136
131
|
};
|
137
132
|
|
138
133
|
export const useFieldTouched = (
|
139
|
-
|
134
|
+
field: string,
|
140
135
|
{ formId }: InternalFormContextValue
|
141
|
-
) => {
|
142
|
-
const atomCreator = useMemo(() => fieldTouchedAtom(name), [name]);
|
143
|
-
return useContextSelectAtom(formId, atomCreator);
|
144
|
-
};
|
136
|
+
) => useFormAtom(fieldTouchedAtom({ formId, field }));
|
145
137
|
|
146
138
|
export const useFieldError = (
|
147
139
|
name: string,
|
148
140
|
context: InternalFormContextValue
|
149
141
|
) => {
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
useFieldErrorsForForm(context),
|
154
|
-
(fieldErrors) => fieldErrors?.[name]
|
142
|
+
const fieldErrors = useFieldErrorsForForm(context);
|
143
|
+
const [state, set] = useFormAtom(
|
144
|
+
fieldErrorAtom({ formId: context.formId, field: name })
|
155
145
|
);
|
146
|
+
return [
|
147
|
+
fieldErrors.map((fieldErrors) => fieldErrors?.[name]).hydrateTo(state),
|
148
|
+
set,
|
149
|
+
] as const;
|
156
150
|
};
|
157
151
|
|
158
152
|
export const useFieldDefaultValue = (
|
159
153
|
name: string,
|
160
154
|
context: InternalFormContextValue
|
161
155
|
) => {
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
useDefaultValuesForForm(context),
|
166
|
-
(val) => lodashGet(val, name)
|
156
|
+
const defaultValues = useDefaultValuesForForm(context);
|
157
|
+
const { defaultValues: state } = useFormAtomValue(
|
158
|
+
formPropsAtom(context.formId)
|
167
159
|
);
|
160
|
+
return defaultValues
|
161
|
+
.map((val) => lodashGet(val, name))
|
162
|
+
.hydrateTo(state[name]);
|
168
163
|
};
|
169
164
|
|
170
|
-
export const
|
171
|
-
|
172
|
-
|
173
|
-
export const useClearError = (context: InternalFormContextValue) => {
|
174
|
-
const clearError = useFormUpdateAtom(clearErrorAtom);
|
165
|
+
export const useClearError = ({ formId }: InternalFormContextValue) => {
|
166
|
+
const updateError = useFormUpdateAtom(setFieldErrorAtom(formId));
|
175
167
|
return useCallback(
|
176
|
-
(name: string) => {
|
177
|
-
|
178
|
-
},
|
179
|
-
[clearError, context.formId]
|
168
|
+
(name: string) => updateError({ field: name, error: undefined }),
|
169
|
+
[updateError]
|
180
170
|
);
|
181
171
|
};
|
182
172
|
|
183
|
-
export const useSetTouched = (
|
184
|
-
const setTouched = useFormUpdateAtom(setTouchedAtom);
|
173
|
+
export const useSetTouched = ({ formId }: InternalFormContextValue) => {
|
174
|
+
const setTouched = useFormUpdateAtom(setTouchedAtom(formId));
|
185
175
|
return useCallback(
|
186
|
-
(name: string, touched: boolean) => {
|
187
|
-
|
188
|
-
},
|
189
|
-
[setTouched, context.formId]
|
176
|
+
(name: string, touched: boolean) => setTouched({ field: name, touched }),
|
177
|
+
[setTouched]
|
190
178
|
);
|
191
179
|
};
|
@@ -0,0 +1,28 @@
|
|
1
|
+
/**
|
2
|
+
* The purpose of this type is to simplify the logic
|
3
|
+
* around data that needs to come from the server initially,
|
4
|
+
* but from the internal state after hydration.
|
5
|
+
*/
|
6
|
+
export type Hydratable<T> = {
|
7
|
+
hydrateTo: (data: T) => T;
|
8
|
+
map: <U>(fn: (data: T) => U) => Hydratable<U>;
|
9
|
+
};
|
10
|
+
|
11
|
+
const serverData = <T>(data: T): Hydratable<T> => ({
|
12
|
+
hydrateTo: () => data,
|
13
|
+
map: (fn) => serverData(fn(data)),
|
14
|
+
});
|
15
|
+
|
16
|
+
const hydratedData = <T>(): Hydratable<T> => ({
|
17
|
+
hydrateTo: (hydratedData: T) => hydratedData,
|
18
|
+
map: <U>() => hydratedData<U>(),
|
19
|
+
});
|
20
|
+
|
21
|
+
const from = <T>(data: T, hydrated: boolean): Hydratable<T> =>
|
22
|
+
hydrated ? hydratedData<T>() : serverData<T>(data);
|
23
|
+
|
24
|
+
export const hydratable = {
|
25
|
+
serverData,
|
26
|
+
hydratedData,
|
27
|
+
from,
|
28
|
+
};
|
@@ -0,0 +1,10 @@
|
|
1
|
+
export const getCheckboxChecked = (
|
2
|
+
checkboxValue: string | undefined = "on",
|
3
|
+
newValue: unknown
|
4
|
+
): boolean | undefined => {
|
5
|
+
if (Array.isArray(newValue))
|
6
|
+
return newValue.some((val) => val === true || val === checkboxValue);
|
7
|
+
if (typeof newValue === "boolean") return newValue;
|
8
|
+
if (typeof newValue === "string") return newValue === checkboxValue;
|
9
|
+
return undefined;
|
10
|
+
};
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import { Atom, atom } from "jotai";
|
2
|
+
import { atomFamily } from "jotai/utils";
|
3
|
+
import isEqual from "lodash/isEqual";
|
4
|
+
|
5
|
+
export type InternalFormId = string | symbol;
|
6
|
+
|
7
|
+
export const formAtomFamily = <T>(data: T) =>
|
8
|
+
atomFamily((_: InternalFormId) => atom(data));
|
9
|
+
|
10
|
+
export type FieldAtomKey = { formId: InternalFormId; field: string };
|
11
|
+
export const fieldAtomFamily = <T extends Atom<unknown>>(
|
12
|
+
func: (key: FieldAtomKey) => T
|
13
|
+
) => atomFamily(func, isEqual);
|