remix-validated-form 4.2.0 → 4.4.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/.turbo/turbo-build.log +15 -9
- package/README.md +1 -0
- package/browser/ValidatedForm.js +16 -26
- package/browser/hooks.d.ts +2 -0
- package/browser/hooks.js +20 -9
- package/browser/internal/MultiValueMap.d.ts +2 -0
- package/browser/internal/MultiValueMap.js +4 -0
- package/browser/internal/getInputProps.js +2 -1
- package/browser/internal/hooks.d.ts +20 -9
- package/browser/internal/hooks.js +32 -23
- package/browser/internal/logic/getRadioChecked.js +10 -0
- package/browser/internal/reset.d.ts +28 -0
- package/browser/internal/reset.js +13 -0
- package/browser/internal/state/cleanup.d.ts +2 -0
- package/browser/internal/state/cleanup.js +6 -0
- package/browser/internal/state/controlledFieldStore.d.ts +24 -0
- package/browser/internal/state/controlledFieldStore.js +57 -0
- package/browser/internal/state/controlledFields.d.ts +6 -62
- package/browser/internal/state/controlledFields.js +36 -63
- package/browser/internal/state/createFormStore.d.ts +40 -0
- package/browser/internal/state/createFormStore.js +83 -0
- package/browser/internal/state/storeFamily.d.ts +9 -0
- package/browser/internal/state/storeFamily.js +18 -0
- package/browser/internal/state/storeHooks.d.ts +5 -0
- package/browser/internal/state/storeHooks.js +10 -0
- package/browser/internal/state.d.ts +0 -27
- package/browser/internal/state.js +0 -5
- package/browser/unreleased/formStateHooks.d.ts +15 -0
- package/browser/unreleased/formStateHooks.js +23 -14
- package/browser/userFacingFormContext.d.ts +8 -0
- package/browser/userFacingFormContext.js +5 -4
- package/dist/remix-validated-form.cjs.js +17 -0
- package/dist/remix-validated-form.es.js +2844 -0
- package/dist/remix-validated-form.umd.js +17 -0
- package/{build → dist/types}/ValidatedForm.d.ts +0 -0
- package/{build → dist/types}/hooks.d.ts +2 -0
- package/{build → dist/types}/index.d.ts +0 -0
- package/{build → dist/types}/internal/MultiValueMap.d.ts +2 -0
- package/{build → dist/types}/internal/constants.d.ts +0 -0
- package/{build → dist/types}/internal/flatten.d.ts +0 -0
- package/{build → dist/types}/internal/formContext.d.ts +0 -0
- package/{build → dist/types}/internal/getInputProps.d.ts +0 -0
- package/dist/types/internal/hooks.d.ts +32 -0
- package/{build → dist/types}/internal/hydratable.d.ts +0 -0
- package/{build → dist/types}/internal/logic/getCheckboxChecked.d.ts +0 -0
- package/{build → dist/types}/internal/logic/getRadioChecked.d.ts +0 -0
- package/dist/types/internal/state/cleanup.d.ts +2 -0
- package/dist/types/internal/state/controlledFieldStore.d.ts +24 -0
- package/dist/types/internal/state/controlledFields.d.ts +6 -0
- package/dist/types/internal/state/createFormStore.d.ts +40 -0
- package/dist/types/internal/state/storeFamily.d.ts +9 -0
- package/dist/types/internal/state/storeHooks.d.ts +5 -0
- package/{build → dist/types}/internal/submissionCallbacks.d.ts +0 -0
- package/{build → dist/types}/internal/util.d.ts +0 -0
- package/{build → dist/types}/server.d.ts +0 -0
- package/{build → dist/types}/unreleased/formStateHooks.d.ts +15 -0
- package/{build → dist/types}/userFacingFormContext.d.ts +8 -0
- package/{build → dist/types}/validation/createValidator.d.ts +0 -0
- package/{build → dist/types}/validation/types.d.ts +0 -0
- package/package.json +11 -9
- package/src/ValidatedForm.tsx +25 -43
- package/src/hooks.ts +29 -17
- package/src/internal/MultiValueMap.ts +6 -0
- package/src/internal/getInputProps.test.ts +251 -0
- package/src/internal/getInputProps.ts +2 -1
- package/src/internal/hooks.ts +69 -45
- package/src/internal/logic/getRadioChecked.ts +11 -0
- package/src/internal/state/cleanup.ts +8 -0
- package/src/internal/state/controlledFieldStore.ts +91 -0
- package/src/internal/state/controlledFields.ts +78 -0
- package/src/internal/state/createFormStore.ts +152 -0
- package/src/internal/state/storeFamily.ts +24 -0
- package/src/internal/state/storeHooks.ts +22 -0
- package/src/unreleased/formStateHooks.ts +50 -27
- package/src/userFacingFormContext.ts +17 -5
- package/src/validation/validation.test.ts +304 -0
- package/tsconfig.json +4 -1
- package/vite.config.ts +7 -0
- package/.turbo/turbo-test.log +0 -11
- package/browser/components.d.ts +0 -7
- package/browser/components.js +0 -10
- package/browser/internal/SingleTypeMultiValueMap.d.ts +0 -9
- package/browser/internal/SingleTypeMultiValueMap.js +0 -41
- package/browser/internal/customState.d.ts +0 -105
- package/browser/internal/customState.js +0 -46
- package/browser/internal/hooks-valtio.d.ts +0 -18
- package/browser/internal/hooks-valtio.js +0 -110
- package/browser/internal/hooks-zustand.d.ts +0 -16
- package/browser/internal/hooks-zustand.js +0 -100
- package/browser/internal/immerMiddleware.d.ts +0 -6
- package/browser/internal/immerMiddleware.js +0 -7
- package/browser/internal/logic/elementUtils.d.ts +0 -3
- package/browser/internal/logic/elementUtils.js +0 -3
- package/browser/internal/logic/getCheckboxChecked copy.d.ts +0 -1
- package/browser/internal/logic/getCheckboxChecked copy.js +0 -9
- package/browser/internal/logic/setFieldValue.d.ts +0 -1
- package/browser/internal/logic/setFieldValue.js +0 -40
- package/browser/internal/logic/setInputValueInForm.d.ts +0 -1
- package/browser/internal/logic/setInputValueInForm.js +0 -77
- package/browser/internal/setFieldValue.d.ts +0 -20
- package/browser/internal/setFieldValue.js +0 -83
- package/browser/internal/setFormValues.d.ts +0 -2
- package/browser/internal/setFormValues.js +0 -26
- package/browser/internal/state/setFieldValue.d.ts +0 -0
- package/browser/internal/state/setFieldValue.js +0 -1
- package/browser/internal/state-valtio.d.ts +0 -62
- package/browser/internal/state-valtio.js +0 -69
- package/browser/internal/state-zustand.d.ts +0 -47
- package/browser/internal/state-zustand.js +0 -85
- package/browser/internal/test.d.ts +0 -0
- package/browser/internal/test.js +0 -15
- package/browser/internal/useMultiValueMap.d.ts +0 -1
- package/browser/internal/useMultiValueMap.js +0 -11
- package/browser/internal/watch.d.ts +0 -18
- package/browser/internal/watch.js +0 -122
- package/browser/lowLevelHooks.d.ts +0 -0
- package/browser/lowLevelHooks.js +0 -1
- package/browser/test-data/testFormData.d.ts +0 -15
- package/browser/test-data/testFormData.js +0 -46
- package/browser/types.d.ts +0 -1
- package/browser/types.js +0 -1
- package/browser/validation/validation.test.d.ts +0 -1
- package/browser/validation/validation.test.js +0 -274
- package/browser/validation/withYup.d.ts +0 -6
- package/browser/validation/withYup.js +0 -40
- package/browser/validation/withZod.d.ts +0 -6
- package/browser/validation/withZod.js +0 -50
- package/build/ValidatedForm.js +0 -257
- package/build/hooks.js +0 -79
- package/build/index.js +0 -18
- package/build/internal/MultiValueMap.js +0 -44
- package/build/internal/SingleTypeMultiValueMap.d.ts +0 -8
- package/build/internal/SingleTypeMultiValueMap.js +0 -45
- package/build/internal/constants.js +0 -7
- package/build/internal/flatten.js +0 -14
- package/build/internal/formContext.js +0 -5
- package/build/internal/getInputProps.js +0 -57
- package/build/internal/hooks-valtio.d.ts +0 -18
- package/build/internal/hooks-valtio.js +0 -128
- package/build/internal/hooks-zustand.d.ts +0 -16
- package/build/internal/hooks-zustand.js +0 -117
- package/build/internal/hooks.d.ts +0 -21
- package/build/internal/hooks.js +0 -128
- package/build/internal/hydratable.js +0 -17
- package/build/internal/immerMiddleware.d.ts +0 -6
- package/build/internal/immerMiddleware.js +0 -14
- package/build/internal/logic/elementUtils.d.ts +0 -3
- package/build/internal/logic/elementUtils.js +0 -9
- package/build/internal/logic/getCheckboxChecked.js +0 -13
- package/build/internal/logic/getRadioChecked.js +0 -9
- package/build/internal/logic/setFieldValue.d.ts +0 -1
- package/build/internal/logic/setFieldValue.js +0 -47
- package/build/internal/logic/setInputValueInForm.d.ts +0 -1
- package/build/internal/logic/setInputValueInForm.js +0 -84
- package/build/internal/setFormValues.d.ts +0 -2
- package/build/internal/setFormValues.js +0 -33
- package/build/internal/state/atomUtils.d.ts +0 -38
- package/build/internal/state/atomUtils.js +0 -13
- package/build/internal/state/controlledFields.d.ts +0 -62
- package/build/internal/state/controlledFields.js +0 -85
- package/build/internal/state-valtio.d.ts +0 -62
- package/build/internal/state-valtio.js +0 -83
- package/build/internal/state-zustand.d.ts +0 -47
- package/build/internal/state-zustand.js +0 -91
- package/build/internal/state.d.ts +0 -370
- package/build/internal/state.js +0 -76
- package/build/internal/submissionCallbacks.js +0 -17
- package/build/internal/test.d.ts +0 -1
- package/build/internal/test.js +0 -12
- package/build/internal/util.js +0 -41
- package/build/internal/watch.d.ts +0 -20
- package/build/internal/watch.js +0 -126
- package/build/server.js +0 -32
- package/build/types.d.ts +0 -1
- package/build/types.js +0 -2
- package/build/unreleased/formStateHooks.js +0 -59
- package/build/userFacingFormContext.js +0 -30
- package/build/validation/createValidator.js +0 -45
- package/build/validation/types.js +0 -2
- package/src/internal/state/atomUtils.ts +0 -13
- package/src/internal/state.ts +0 -132
@@ -0,0 +1,152 @@
|
|
1
|
+
import invariant from "tiny-invariant";
|
2
|
+
import create from "zustand";
|
3
|
+
import { immer } from "zustand/middleware/immer";
|
4
|
+
import { FieldErrors, TouchedFields, Validator } from "../../validation/types";
|
5
|
+
import { controlledFieldStore } from "./controlledFieldStore";
|
6
|
+
import { storeFamily } from "./storeFamily";
|
7
|
+
|
8
|
+
export type SyncedFormProps = {
|
9
|
+
formId?: string;
|
10
|
+
action?: string;
|
11
|
+
subaction?: string;
|
12
|
+
defaultValues: { [fieldName: string]: any };
|
13
|
+
registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
|
14
|
+
validator: Validator<unknown>;
|
15
|
+
};
|
16
|
+
|
17
|
+
export type FormState = {
|
18
|
+
isHydrated: boolean;
|
19
|
+
isSubmitting: boolean;
|
20
|
+
hasBeenSubmitted: boolean;
|
21
|
+
fieldErrors: FieldErrors;
|
22
|
+
touchedFields: TouchedFields;
|
23
|
+
formProps?: SyncedFormProps;
|
24
|
+
formElement: HTMLFormElement | null;
|
25
|
+
|
26
|
+
isValid: () => boolean;
|
27
|
+
startSubmit: () => void;
|
28
|
+
endSubmit: () => void;
|
29
|
+
setTouched: (field: string, touched: boolean) => void;
|
30
|
+
setFieldError: (field: string, error: string) => void;
|
31
|
+
setFieldErrors: (errors: FieldErrors) => void;
|
32
|
+
clearFieldError: (field: string) => void;
|
33
|
+
reset: () => void;
|
34
|
+
syncFormProps: (props: SyncedFormProps) => void;
|
35
|
+
setHydrated: () => void;
|
36
|
+
setFormElement: (formElement: HTMLFormElement | null) => void;
|
37
|
+
validateField: (fieldName: string) => Promise<string | null>;
|
38
|
+
validate: () => Promise<void>;
|
39
|
+
resetFormElement: () => void;
|
40
|
+
};
|
41
|
+
|
42
|
+
export const formStore = storeFamily((formId) =>
|
43
|
+
create<FormState>()(
|
44
|
+
immer((set, get, api) => ({
|
45
|
+
isHydrated: false,
|
46
|
+
isSubmitting: false,
|
47
|
+
hasBeenSubmitted: false,
|
48
|
+
touchedFields: {},
|
49
|
+
fieldErrors: {},
|
50
|
+
formElement: null,
|
51
|
+
|
52
|
+
isValid: () => Object.keys(get().fieldErrors).length === 0,
|
53
|
+
startSubmit: () =>
|
54
|
+
set((state) => {
|
55
|
+
state.isSubmitting = true;
|
56
|
+
state.hasBeenSubmitted = true;
|
57
|
+
}),
|
58
|
+
endSubmit: () =>
|
59
|
+
set((state) => {
|
60
|
+
state.isSubmitting = false;
|
61
|
+
}),
|
62
|
+
setTouched: (fieldName, touched) =>
|
63
|
+
set((state) => {
|
64
|
+
state.touchedFields[fieldName] = touched;
|
65
|
+
}),
|
66
|
+
setFieldError: (fieldName: string, error: string) =>
|
67
|
+
set((state) => {
|
68
|
+
state.fieldErrors[fieldName] = error;
|
69
|
+
}),
|
70
|
+
setFieldErrors: (errors: FieldErrors) =>
|
71
|
+
set((state) => {
|
72
|
+
state.fieldErrors = errors;
|
73
|
+
}),
|
74
|
+
clearFieldError: (fieldName: string) =>
|
75
|
+
set((state) => {
|
76
|
+
delete state.fieldErrors[fieldName];
|
77
|
+
}),
|
78
|
+
|
79
|
+
reset: () =>
|
80
|
+
set((state) => {
|
81
|
+
state.fieldErrors = {};
|
82
|
+
state.touchedFields = {};
|
83
|
+
state.hasBeenSubmitted = false;
|
84
|
+
}),
|
85
|
+
syncFormProps: (props: SyncedFormProps) =>
|
86
|
+
set((state) => {
|
87
|
+
state.formProps = props;
|
88
|
+
}),
|
89
|
+
setHydrated: () =>
|
90
|
+
set((state) => {
|
91
|
+
state.isHydrated = true;
|
92
|
+
}),
|
93
|
+
setFormElement: (formElement: HTMLFormElement | null) => {
|
94
|
+
// This gets called frequently, so we want to avoid calling set() every time
|
95
|
+
// Or else we wind up with an infinite loop
|
96
|
+
if (get().formElement === formElement) return;
|
97
|
+
set((state) => {
|
98
|
+
// weird type issue here
|
99
|
+
// seems to be because formElement is a writable draft
|
100
|
+
state.formElement = formElement as any;
|
101
|
+
});
|
102
|
+
},
|
103
|
+
validateField: async (field: string) => {
|
104
|
+
const formElement = get().formElement;
|
105
|
+
invariant(
|
106
|
+
formElement,
|
107
|
+
"Cannot find reference to form. This is probably a bug in remix-validated-form."
|
108
|
+
);
|
109
|
+
|
110
|
+
const validator = get().formProps?.validator;
|
111
|
+
invariant(
|
112
|
+
validator,
|
113
|
+
"Cannot validator. This is probably a bug in remix-validated-form."
|
114
|
+
);
|
115
|
+
|
116
|
+
await controlledFieldStore(formId).getState().awaitValueUpdate?.(field);
|
117
|
+
|
118
|
+
const { error } = await validator.validateField(
|
119
|
+
new FormData(formElement),
|
120
|
+
field
|
121
|
+
);
|
122
|
+
|
123
|
+
if (error) {
|
124
|
+
get().setFieldError(field, error);
|
125
|
+
return error;
|
126
|
+
} else {
|
127
|
+
get().clearFieldError(field);
|
128
|
+
return null;
|
129
|
+
}
|
130
|
+
},
|
131
|
+
|
132
|
+
validate: async () => {
|
133
|
+
const formElement = get().formElement;
|
134
|
+
invariant(
|
135
|
+
formElement,
|
136
|
+
"Cannot find reference to form. This is probably a bug in remix-validated-form."
|
137
|
+
);
|
138
|
+
|
139
|
+
const validator = get().formProps?.validator;
|
140
|
+
invariant(
|
141
|
+
validator,
|
142
|
+
"Cannot validator. This is probably a bug in remix-validated-form."
|
143
|
+
);
|
144
|
+
|
145
|
+
const { error } = await validator.validate(new FormData(formElement));
|
146
|
+
if (error) get().setFieldErrors(error.fieldErrors);
|
147
|
+
},
|
148
|
+
|
149
|
+
resetFormElement: () => get().formElement?.reset(),
|
150
|
+
}))
|
151
|
+
)
|
152
|
+
);
|
@@ -0,0 +1,24 @@
|
|
1
|
+
/**
|
2
|
+
* This is basically what `atomFamily` from jotai does,
|
3
|
+
* but it doesn't make sense to include the entire jotai library just for that api.
|
4
|
+
*/
|
5
|
+
|
6
|
+
export type InternalFormId = string | symbol;
|
7
|
+
|
8
|
+
export const storeFamily = <T>(create: (formId: InternalFormId) => T) => {
|
9
|
+
const stores: Map<InternalFormId, T> = new Map();
|
10
|
+
|
11
|
+
const family = (formId: InternalFormId) => {
|
12
|
+
if (stores.has(formId)) return stores.get(formId)!;
|
13
|
+
|
14
|
+
const store = create(formId);
|
15
|
+
stores.set(formId, store);
|
16
|
+
return store;
|
17
|
+
};
|
18
|
+
|
19
|
+
family.remove = (formId: InternalFormId) => {
|
20
|
+
stores.delete(formId);
|
21
|
+
};
|
22
|
+
|
23
|
+
return family;
|
24
|
+
};
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import {
|
2
|
+
ControlledFieldState,
|
3
|
+
controlledFieldStore,
|
4
|
+
} from "./controlledFieldStore";
|
5
|
+
import { FormState, formStore } from "./createFormStore";
|
6
|
+
import { InternalFormId } from "./storeFamily";
|
7
|
+
|
8
|
+
export const useFormStore = <T>(
|
9
|
+
formId: InternalFormId,
|
10
|
+
selector: (state: FormState) => T
|
11
|
+
) => {
|
12
|
+
const useStore = formStore(formId);
|
13
|
+
return useStore(selector);
|
14
|
+
};
|
15
|
+
|
16
|
+
export const useControlledFieldStore = <T>(
|
17
|
+
formId: InternalFormId,
|
18
|
+
selector: (state: ControlledFieldState) => T
|
19
|
+
) => {
|
20
|
+
const useStore = controlledFieldStore(formId);
|
21
|
+
return useStore(selector);
|
22
|
+
};
|
@@ -6,16 +6,19 @@ import {
|
|
6
6
|
useSetTouched,
|
7
7
|
useDefaultValuesForForm,
|
8
8
|
useFieldErrorsForForm,
|
9
|
-
|
9
|
+
useInternalIsSubmitting,
|
10
|
+
useInternalHasBeenSubmitted,
|
11
|
+
useTouchedFields,
|
12
|
+
useInternalIsValid,
|
13
|
+
useFieldErrors,
|
14
|
+
useValidateField,
|
15
|
+
useValidate,
|
16
|
+
useSetFieldErrors,
|
17
|
+
useResetFormElement,
|
18
|
+
useSyncedDefaultValues,
|
19
|
+
useFormActionProp,
|
20
|
+
useFormSubactionProp,
|
10
21
|
} from "../internal/hooks";
|
11
|
-
import {
|
12
|
-
fieldErrorsAtom,
|
13
|
-
formPropsAtom,
|
14
|
-
hasBeenSubmittedAtom,
|
15
|
-
isSubmittingAtom,
|
16
|
-
isValidAtom,
|
17
|
-
touchedFieldsAtom,
|
18
|
-
} from "../internal/state";
|
19
22
|
import { FieldErrors, TouchedFields } from "../validation/types";
|
20
23
|
|
21
24
|
export type FormState = {
|
@@ -35,29 +38,27 @@ export type FormState = {
|
|
35
38
|
* @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
|
36
39
|
*/
|
37
40
|
export const useFormState = (formId?: string): FormState => {
|
38
|
-
const formContext = useInternalFormContext(formId, "
|
39
|
-
const
|
40
|
-
const
|
41
|
-
const
|
42
|
-
|
43
|
-
);
|
44
|
-
const
|
45
|
-
const isValid = useFormAtomValue(isValidAtom(formContext.formId));
|
41
|
+
const formContext = useInternalFormContext(formId, "useFormState");
|
42
|
+
const isSubmitting = useInternalIsSubmitting(formContext.formId);
|
43
|
+
const hasBeenSubmitted = useInternalHasBeenSubmitted(formContext.formId);
|
44
|
+
const touchedFields = useTouchedFields(formContext.formId);
|
45
|
+
const isValid = useInternalIsValid(formContext.formId);
|
46
|
+
const action = useFormActionProp(formContext.formId);
|
47
|
+
const subaction = useFormSubactionProp(formContext.formId);
|
46
48
|
|
49
|
+
const syncedDefaultValues = useSyncedDefaultValues(formContext.formId);
|
47
50
|
const defaultValuesToUse = useDefaultValuesForForm(formContext);
|
48
|
-
const hydratedDefaultValues =
|
49
|
-
|
50
|
-
);
|
51
|
+
const hydratedDefaultValues =
|
52
|
+
defaultValuesToUse.hydrateTo(syncedDefaultValues);
|
51
53
|
|
52
|
-
const fieldErrorsFromState =
|
53
|
-
fieldErrorsAtom(formContext.formId)
|
54
|
-
);
|
54
|
+
const fieldErrorsFromState = useFieldErrors(formContext.formId);
|
55
55
|
const fieldErrorsToUse = useFieldErrorsForForm(formContext);
|
56
56
|
const hydratedFieldErrors = fieldErrorsToUse.hydrateTo(fieldErrorsFromState);
|
57
57
|
|
58
58
|
return useMemo(
|
59
59
|
() => ({
|
60
|
-
|
60
|
+
action,
|
61
|
+
subaction,
|
61
62
|
defaultValues: hydratedDefaultValues,
|
62
63
|
fieldErrors: hydratedFieldErrors ?? {},
|
63
64
|
hasBeenSubmitted,
|
@@ -66,12 +67,13 @@ export const useFormState = (formId?: string): FormState => {
|
|
66
67
|
isValid,
|
67
68
|
}),
|
68
69
|
[
|
69
|
-
|
70
|
+
action,
|
70
71
|
hasBeenSubmitted,
|
71
72
|
hydratedDefaultValues,
|
72
73
|
hydratedFieldErrors,
|
73
74
|
isSubmitting,
|
74
75
|
isValid,
|
76
|
+
subaction,
|
75
77
|
touchedFields,
|
76
78
|
]
|
77
79
|
);
|
@@ -90,6 +92,21 @@ export type FormHelpers = {
|
|
90
92
|
* Change the touched state of the specified field.
|
91
93
|
*/
|
92
94
|
setTouched: (fieldName: string, touched: boolean) => void;
|
95
|
+
/**
|
96
|
+
* Validate the whole form and populate any errors.
|
97
|
+
*/
|
98
|
+
validate: () => Promise<void>;
|
99
|
+
/**
|
100
|
+
* Clears all errors on the form.
|
101
|
+
*/
|
102
|
+
clearAllErrors: () => void;
|
103
|
+
/**
|
104
|
+
* Resets the form.
|
105
|
+
*
|
106
|
+
* _Note_: The equivalent behavior can be achieved by calling formElement.reset()
|
107
|
+
* or clicking a button element with `type="reset"`.
|
108
|
+
*/
|
109
|
+
reset: () => void;
|
93
110
|
};
|
94
111
|
|
95
112
|
/**
|
@@ -100,14 +117,20 @@ export type FormHelpers = {
|
|
100
117
|
export const useFormHelpers = (formId?: string): FormHelpers => {
|
101
118
|
const formContext = useInternalFormContext(formId, "useFormHelpers");
|
102
119
|
const setTouched = useSetTouched(formContext);
|
103
|
-
const
|
120
|
+
const validateField = useValidateField(formContext.formId);
|
121
|
+
const validate = useValidate(formContext.formId);
|
104
122
|
const clearError = useClearError(formContext);
|
123
|
+
const setFieldErrors = useSetFieldErrors(formContext.formId);
|
124
|
+
const reset = useResetFormElement(formContext.formId);
|
105
125
|
return useMemo(
|
106
126
|
() => ({
|
107
127
|
setTouched,
|
108
128
|
validateField,
|
109
129
|
clearError,
|
130
|
+
validate,
|
131
|
+
clearAllErrors: () => setFieldErrors({}),
|
132
|
+
reset,
|
110
133
|
}),
|
111
|
-
[clearError, setTouched, validateField]
|
134
|
+
[clearError, reset, setFieldErrors, setTouched, validate, validateField]
|
112
135
|
);
|
113
136
|
};
|
@@ -1,6 +1,8 @@
|
|
1
1
|
import { useCallback } from "react";
|
2
|
-
import {
|
3
|
-
|
2
|
+
import {
|
3
|
+
useInternalFormContext,
|
4
|
+
useRegisterReceiveFocus,
|
5
|
+
} from "./internal/hooks";
|
4
6
|
import { useFormHelpers, useFormState } from "./unreleased/formStateHooks";
|
5
7
|
import { FieldErrors, TouchedFields } from "./validation/types";
|
6
8
|
|
@@ -56,6 +58,14 @@ export type FormContextValue = {
|
|
56
58
|
* Change the touched state of the specified field.
|
57
59
|
*/
|
58
60
|
setFieldTouched: (fieldName: string, touched: boolean) => void;
|
61
|
+
/**
|
62
|
+
* Validate the whole form and populate any errors.
|
63
|
+
*/
|
64
|
+
validate: () => Promise<void>;
|
65
|
+
/**
|
66
|
+
* Clears all errors on the form.
|
67
|
+
*/
|
68
|
+
clearAllErrors: () => void;
|
59
69
|
};
|
60
70
|
|
61
71
|
/**
|
@@ -69,11 +79,11 @@ export const useFormContext = (formId?: string): FormContextValue => {
|
|
69
79
|
clearError: internalClearError,
|
70
80
|
setTouched,
|
71
81
|
validateField,
|
82
|
+
clearAllErrors,
|
83
|
+
validate,
|
72
84
|
} = useFormHelpers(formId);
|
73
85
|
|
74
|
-
const
|
75
|
-
formPropsAtom(context.formId)
|
76
|
-
);
|
86
|
+
const registerReceiveFocus = useRegisterReceiveFocus(context.formId);
|
77
87
|
|
78
88
|
const clearError = useCallback(
|
79
89
|
(...names: string[]) => {
|
@@ -90,5 +100,7 @@ export const useFormContext = (formId?: string): FormContextValue => {
|
|
90
100
|
validateField,
|
91
101
|
clearError,
|
92
102
|
registerReceiveFocus,
|
103
|
+
clearAllErrors,
|
104
|
+
validate,
|
93
105
|
};
|
94
106
|
};
|
@@ -0,0 +1,304 @@
|
|
1
|
+
import { anyString, TestFormData } from "@remix-validated-form/test-utils";
|
2
|
+
import { withYup } from "@remix-validated-form/with-yup/src";
|
3
|
+
import { withZod } from "@remix-validated-form/with-zod";
|
4
|
+
import { Validator } from "remix-validated-form/src";
|
5
|
+
import { objectFromPathEntries } from "remix-validated-form/src/internal/flatten";
|
6
|
+
import { describe, it, expect } from "vitest";
|
7
|
+
import * as yup from "yup";
|
8
|
+
import { z } from "zod";
|
9
|
+
|
10
|
+
// If adding an adapter, write a validator that validates this shape
|
11
|
+
type Person = {
|
12
|
+
firstName: string;
|
13
|
+
lastName: string;
|
14
|
+
age?: number;
|
15
|
+
address: {
|
16
|
+
streetAddress: string;
|
17
|
+
city: string;
|
18
|
+
country: string;
|
19
|
+
};
|
20
|
+
pets?: {
|
21
|
+
animal: string;
|
22
|
+
name: string;
|
23
|
+
}[];
|
24
|
+
};
|
25
|
+
|
26
|
+
type ValidationTestCase = {
|
27
|
+
name: string;
|
28
|
+
validator: Validator<Person>;
|
29
|
+
};
|
30
|
+
|
31
|
+
const validationTestCases: ValidationTestCase[] = [
|
32
|
+
{
|
33
|
+
name: "yup",
|
34
|
+
validator: withYup(
|
35
|
+
yup.object({
|
36
|
+
firstName: yup.string().required(),
|
37
|
+
lastName: yup.string().required(),
|
38
|
+
age: yup.number(),
|
39
|
+
address: yup
|
40
|
+
.object({
|
41
|
+
streetAddress: yup.string().required(),
|
42
|
+
city: yup.string().required(),
|
43
|
+
country: yup.string().required(),
|
44
|
+
})
|
45
|
+
.required(),
|
46
|
+
pets: yup.array().of(
|
47
|
+
yup.object({
|
48
|
+
animal: yup.string().required(),
|
49
|
+
name: yup.string().required(),
|
50
|
+
})
|
51
|
+
),
|
52
|
+
})
|
53
|
+
),
|
54
|
+
},
|
55
|
+
{
|
56
|
+
name: "zod",
|
57
|
+
validator: withZod(
|
58
|
+
z.object({
|
59
|
+
firstName: z.string().nonempty(),
|
60
|
+
lastName: z.string().nonempty(),
|
61
|
+
age: z.optional(z.number()),
|
62
|
+
address: z.preprocess(
|
63
|
+
(value) => (value == null ? {} : value),
|
64
|
+
z.object({
|
65
|
+
streetAddress: z.string().nonempty(),
|
66
|
+
city: z.string().nonempty(),
|
67
|
+
country: z.string().nonempty(),
|
68
|
+
})
|
69
|
+
),
|
70
|
+
pets: z
|
71
|
+
.object({
|
72
|
+
animal: z.string().nonempty(),
|
73
|
+
name: z.string().nonempty(),
|
74
|
+
})
|
75
|
+
.array()
|
76
|
+
.optional(),
|
77
|
+
})
|
78
|
+
),
|
79
|
+
},
|
80
|
+
];
|
81
|
+
|
82
|
+
describe("Validation", () => {
|
83
|
+
describe.each(validationTestCases)("Adapter for $name", ({ validator }) => {
|
84
|
+
describe("validate", () => {
|
85
|
+
it("should return the data when valid", async () => {
|
86
|
+
const person: Person = {
|
87
|
+
firstName: "John",
|
88
|
+
lastName: "Doe",
|
89
|
+
age: 30,
|
90
|
+
address: {
|
91
|
+
streetAddress: "123 Main St",
|
92
|
+
city: "Anytown",
|
93
|
+
country: "USA",
|
94
|
+
},
|
95
|
+
pets: [{ animal: "dog", name: "Fido" }],
|
96
|
+
};
|
97
|
+
expect(await validator.validate(person)).toEqual({
|
98
|
+
data: person,
|
99
|
+
error: undefined,
|
100
|
+
submittedData: person,
|
101
|
+
});
|
102
|
+
});
|
103
|
+
|
104
|
+
it("should return field errors when invalid", async () => {
|
105
|
+
const obj = { age: "hi!", pets: [{ animal: "dog" }] };
|
106
|
+
expect(await validator.validate(obj)).toEqual({
|
107
|
+
data: undefined,
|
108
|
+
error: {
|
109
|
+
fieldErrors: {
|
110
|
+
firstName: anyString,
|
111
|
+
lastName: anyString,
|
112
|
+
age: anyString,
|
113
|
+
"address.city": anyString,
|
114
|
+
"address.country": anyString,
|
115
|
+
"address.streetAddress": anyString,
|
116
|
+
"pets[0].name": anyString,
|
117
|
+
},
|
118
|
+
subaction: undefined,
|
119
|
+
},
|
120
|
+
submittedData: obj,
|
121
|
+
});
|
122
|
+
});
|
123
|
+
|
124
|
+
it("should unflatten data when validating", async () => {
|
125
|
+
const data = {
|
126
|
+
firstName: "John",
|
127
|
+
lastName: "Doe",
|
128
|
+
age: 30,
|
129
|
+
"address.streetAddress": "123 Main St",
|
130
|
+
"address.city": "Anytown",
|
131
|
+
"address.country": "USA",
|
132
|
+
"pets[0].animal": "dog",
|
133
|
+
"pets[0].name": "Fido",
|
134
|
+
};
|
135
|
+
expect(await validator.validate(data)).toEqual({
|
136
|
+
data: {
|
137
|
+
firstName: "John",
|
138
|
+
lastName: "Doe",
|
139
|
+
age: 30,
|
140
|
+
address: {
|
141
|
+
streetAddress: "123 Main St",
|
142
|
+
city: "Anytown",
|
143
|
+
country: "USA",
|
144
|
+
},
|
145
|
+
pets: [{ animal: "dog", name: "Fido" }],
|
146
|
+
},
|
147
|
+
error: undefined,
|
148
|
+
submittedData: objectFromPathEntries(Object.entries(data)),
|
149
|
+
});
|
150
|
+
});
|
151
|
+
|
152
|
+
it("should accept FormData directly and return errors", async () => {
|
153
|
+
const formData = new TestFormData();
|
154
|
+
formData.set("firstName", "John");
|
155
|
+
formData.set("lastName", "Doe");
|
156
|
+
formData.set("address.streetAddress", "123 Main St");
|
157
|
+
formData.set("address.country", "USA");
|
158
|
+
formData.set("pets[0].animal", "dog");
|
159
|
+
|
160
|
+
expect(await validator.validate(formData)).toEqual({
|
161
|
+
data: undefined,
|
162
|
+
error: {
|
163
|
+
fieldErrors: {
|
164
|
+
"address.city": anyString,
|
165
|
+
"pets[0].name": anyString,
|
166
|
+
},
|
167
|
+
subaction: undefined,
|
168
|
+
},
|
169
|
+
submittedData: objectFromPathEntries([...formData.entries()]),
|
170
|
+
});
|
171
|
+
});
|
172
|
+
|
173
|
+
it("should accept FormData directly and return valid data", async () => {
|
174
|
+
const formData = new TestFormData();
|
175
|
+
formData.set("firstName", "John");
|
176
|
+
formData.set("lastName", "Doe");
|
177
|
+
formData.set("address.streetAddress", "123 Main St");
|
178
|
+
formData.set("address.country", "USA");
|
179
|
+
formData.set("address.city", "Anytown");
|
180
|
+
formData.set("pets[0].animal", "dog");
|
181
|
+
formData.set("pets[0].name", "Fido");
|
182
|
+
|
183
|
+
expect(await validator.validate(formData)).toEqual({
|
184
|
+
data: {
|
185
|
+
firstName: "John",
|
186
|
+
lastName: "Doe",
|
187
|
+
address: {
|
188
|
+
streetAddress: "123 Main St",
|
189
|
+
country: "USA",
|
190
|
+
city: "Anytown",
|
191
|
+
},
|
192
|
+
pets: [{ animal: "dog", name: "Fido" }],
|
193
|
+
},
|
194
|
+
error: undefined,
|
195
|
+
subaction: undefined,
|
196
|
+
submittedData: objectFromPathEntries([...formData.entries()]),
|
197
|
+
});
|
198
|
+
});
|
199
|
+
|
200
|
+
it("should return the subaction in the ValidatorError if there is one", async () => {
|
201
|
+
const person = {
|
202
|
+
lastName: "Doe",
|
203
|
+
age: 20,
|
204
|
+
address: {
|
205
|
+
streetAddress: "123 Main St",
|
206
|
+
city: "Anytown",
|
207
|
+
country: "USA",
|
208
|
+
},
|
209
|
+
pets: [{ animal: "dog", name: "Fido" }],
|
210
|
+
subaction: "updatePerson",
|
211
|
+
};
|
212
|
+
expect(await validator.validate(person)).toEqual({
|
213
|
+
error: {
|
214
|
+
fieldErrors: {
|
215
|
+
firstName: anyString,
|
216
|
+
},
|
217
|
+
subaction: "updatePerson",
|
218
|
+
},
|
219
|
+
data: undefined,
|
220
|
+
submittedData: person,
|
221
|
+
});
|
222
|
+
});
|
223
|
+
});
|
224
|
+
|
225
|
+
describe("validateField", () => {
|
226
|
+
it("should not return an error if field is valid", async () => {
|
227
|
+
const person = {
|
228
|
+
firstName: "John",
|
229
|
+
lastName: {}, // invalid, but we should only be validating firstName
|
230
|
+
};
|
231
|
+
expect(await validator.validateField(person, "firstName")).toEqual({
|
232
|
+
error: undefined,
|
233
|
+
});
|
234
|
+
});
|
235
|
+
it("should not return an error if a nested field is valid", async () => {
|
236
|
+
const person = {
|
237
|
+
firstName: "John",
|
238
|
+
lastName: {}, // invalid, but we should only be validating firstName
|
239
|
+
address: {
|
240
|
+
streetAddress: "123 Main St",
|
241
|
+
city: "Anytown",
|
242
|
+
country: "USA",
|
243
|
+
},
|
244
|
+
pets: [{ animal: "dog", name: "Fido" }],
|
245
|
+
};
|
246
|
+
expect(
|
247
|
+
await validator.validateField(person, "address.streetAddress")
|
248
|
+
).toEqual({
|
249
|
+
error: undefined,
|
250
|
+
});
|
251
|
+
expect(await validator.validateField(person, "address.city")).toEqual({
|
252
|
+
error: undefined,
|
253
|
+
});
|
254
|
+
expect(
|
255
|
+
await validator.validateField(person, "address.country")
|
256
|
+
).toEqual({
|
257
|
+
error: undefined,
|
258
|
+
});
|
259
|
+
expect(await validator.validateField(person, "pets[0].animal")).toEqual(
|
260
|
+
{
|
261
|
+
error: undefined,
|
262
|
+
}
|
263
|
+
);
|
264
|
+
expect(await validator.validateField(person, "pets[0].name")).toEqual({
|
265
|
+
error: undefined,
|
266
|
+
});
|
267
|
+
});
|
268
|
+
|
269
|
+
it("should return an error if field is invalid", async () => {
|
270
|
+
const person = {
|
271
|
+
firstName: "John",
|
272
|
+
lastName: {},
|
273
|
+
address: {
|
274
|
+
streetAddress: "123 Main St",
|
275
|
+
city: 1234,
|
276
|
+
},
|
277
|
+
};
|
278
|
+
expect(await validator.validateField(person, "lastName")).toEqual({
|
279
|
+
error: anyString,
|
280
|
+
});
|
281
|
+
});
|
282
|
+
|
283
|
+
it("should return an error if a nested field is invalid", async () => {
|
284
|
+
const person = {
|
285
|
+
firstName: "John",
|
286
|
+
lastName: {},
|
287
|
+
address: {
|
288
|
+
streetAddress: "123 Main St",
|
289
|
+
city: 1234,
|
290
|
+
},
|
291
|
+
pets: [{ animal: "dog" }],
|
292
|
+
};
|
293
|
+
expect(
|
294
|
+
await validator.validateField(person, "address.country")
|
295
|
+
).toEqual({
|
296
|
+
error: anyString,
|
297
|
+
});
|
298
|
+
expect(await validator.validateField(person, "pets[0].name")).toEqual({
|
299
|
+
error: anyString,
|
300
|
+
});
|
301
|
+
});
|
302
|
+
});
|
303
|
+
});
|
304
|
+
});
|
package/tsconfig.json
CHANGED