remix-validated-form 4.1.4-beta.0 → 4.1.6
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.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.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 +14 -53
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);
|