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.
Files changed (181) hide show
  1. package/.turbo/turbo-build.log +15 -9
  2. package/README.md +1 -0
  3. package/browser/ValidatedForm.js +16 -26
  4. package/browser/hooks.d.ts +2 -0
  5. package/browser/hooks.js +20 -9
  6. package/browser/internal/MultiValueMap.d.ts +2 -0
  7. package/browser/internal/MultiValueMap.js +4 -0
  8. package/browser/internal/getInputProps.js +2 -1
  9. package/browser/internal/hooks.d.ts +20 -9
  10. package/browser/internal/hooks.js +32 -23
  11. package/browser/internal/logic/getRadioChecked.js +10 -0
  12. package/browser/internal/reset.d.ts +28 -0
  13. package/browser/internal/reset.js +13 -0
  14. package/browser/internal/state/cleanup.d.ts +2 -0
  15. package/browser/internal/state/cleanup.js +6 -0
  16. package/browser/internal/state/controlledFieldStore.d.ts +24 -0
  17. package/browser/internal/state/controlledFieldStore.js +57 -0
  18. package/browser/internal/state/controlledFields.d.ts +6 -62
  19. package/browser/internal/state/controlledFields.js +36 -63
  20. package/browser/internal/state/createFormStore.d.ts +40 -0
  21. package/browser/internal/state/createFormStore.js +83 -0
  22. package/browser/internal/state/storeFamily.d.ts +9 -0
  23. package/browser/internal/state/storeFamily.js +18 -0
  24. package/browser/internal/state/storeHooks.d.ts +5 -0
  25. package/browser/internal/state/storeHooks.js +10 -0
  26. package/browser/internal/state.d.ts +0 -27
  27. package/browser/internal/state.js +0 -5
  28. package/browser/unreleased/formStateHooks.d.ts +15 -0
  29. package/browser/unreleased/formStateHooks.js +23 -14
  30. package/browser/userFacingFormContext.d.ts +8 -0
  31. package/browser/userFacingFormContext.js +5 -4
  32. package/dist/remix-validated-form.cjs.js +17 -0
  33. package/dist/remix-validated-form.es.js +2844 -0
  34. package/dist/remix-validated-form.umd.js +17 -0
  35. package/{build → dist/types}/ValidatedForm.d.ts +0 -0
  36. package/{build → dist/types}/hooks.d.ts +2 -0
  37. package/{build → dist/types}/index.d.ts +0 -0
  38. package/{build → dist/types}/internal/MultiValueMap.d.ts +2 -0
  39. package/{build → dist/types}/internal/constants.d.ts +0 -0
  40. package/{build → dist/types}/internal/flatten.d.ts +0 -0
  41. package/{build → dist/types}/internal/formContext.d.ts +0 -0
  42. package/{build → dist/types}/internal/getInputProps.d.ts +0 -0
  43. package/dist/types/internal/hooks.d.ts +32 -0
  44. package/{build → dist/types}/internal/hydratable.d.ts +0 -0
  45. package/{build → dist/types}/internal/logic/getCheckboxChecked.d.ts +0 -0
  46. package/{build → dist/types}/internal/logic/getRadioChecked.d.ts +0 -0
  47. package/dist/types/internal/state/cleanup.d.ts +2 -0
  48. package/dist/types/internal/state/controlledFieldStore.d.ts +24 -0
  49. package/dist/types/internal/state/controlledFields.d.ts +6 -0
  50. package/dist/types/internal/state/createFormStore.d.ts +40 -0
  51. package/dist/types/internal/state/storeFamily.d.ts +9 -0
  52. package/dist/types/internal/state/storeHooks.d.ts +5 -0
  53. package/{build → dist/types}/internal/submissionCallbacks.d.ts +0 -0
  54. package/{build → dist/types}/internal/util.d.ts +0 -0
  55. package/{build → dist/types}/server.d.ts +0 -0
  56. package/{build → dist/types}/unreleased/formStateHooks.d.ts +15 -0
  57. package/{build → dist/types}/userFacingFormContext.d.ts +8 -0
  58. package/{build → dist/types}/validation/createValidator.d.ts +0 -0
  59. package/{build → dist/types}/validation/types.d.ts +0 -0
  60. package/package.json +11 -9
  61. package/src/ValidatedForm.tsx +25 -43
  62. package/src/hooks.ts +29 -17
  63. package/src/internal/MultiValueMap.ts +6 -0
  64. package/src/internal/getInputProps.test.ts +251 -0
  65. package/src/internal/getInputProps.ts +2 -1
  66. package/src/internal/hooks.ts +69 -45
  67. package/src/internal/logic/getRadioChecked.ts +11 -0
  68. package/src/internal/state/cleanup.ts +8 -0
  69. package/src/internal/state/controlledFieldStore.ts +91 -0
  70. package/src/internal/state/controlledFields.ts +78 -0
  71. package/src/internal/state/createFormStore.ts +152 -0
  72. package/src/internal/state/storeFamily.ts +24 -0
  73. package/src/internal/state/storeHooks.ts +22 -0
  74. package/src/unreleased/formStateHooks.ts +50 -27
  75. package/src/userFacingFormContext.ts +17 -5
  76. package/src/validation/validation.test.ts +304 -0
  77. package/tsconfig.json +4 -1
  78. package/vite.config.ts +7 -0
  79. package/.turbo/turbo-test.log +0 -11
  80. package/browser/components.d.ts +0 -7
  81. package/browser/components.js +0 -10
  82. package/browser/internal/SingleTypeMultiValueMap.d.ts +0 -9
  83. package/browser/internal/SingleTypeMultiValueMap.js +0 -41
  84. package/browser/internal/customState.d.ts +0 -105
  85. package/browser/internal/customState.js +0 -46
  86. package/browser/internal/hooks-valtio.d.ts +0 -18
  87. package/browser/internal/hooks-valtio.js +0 -110
  88. package/browser/internal/hooks-zustand.d.ts +0 -16
  89. package/browser/internal/hooks-zustand.js +0 -100
  90. package/browser/internal/immerMiddleware.d.ts +0 -6
  91. package/browser/internal/immerMiddleware.js +0 -7
  92. package/browser/internal/logic/elementUtils.d.ts +0 -3
  93. package/browser/internal/logic/elementUtils.js +0 -3
  94. package/browser/internal/logic/getCheckboxChecked copy.d.ts +0 -1
  95. package/browser/internal/logic/getCheckboxChecked copy.js +0 -9
  96. package/browser/internal/logic/setFieldValue.d.ts +0 -1
  97. package/browser/internal/logic/setFieldValue.js +0 -40
  98. package/browser/internal/logic/setInputValueInForm.d.ts +0 -1
  99. package/browser/internal/logic/setInputValueInForm.js +0 -77
  100. package/browser/internal/setFieldValue.d.ts +0 -20
  101. package/browser/internal/setFieldValue.js +0 -83
  102. package/browser/internal/setFormValues.d.ts +0 -2
  103. package/browser/internal/setFormValues.js +0 -26
  104. package/browser/internal/state/setFieldValue.d.ts +0 -0
  105. package/browser/internal/state/setFieldValue.js +0 -1
  106. package/browser/internal/state-valtio.d.ts +0 -62
  107. package/browser/internal/state-valtio.js +0 -69
  108. package/browser/internal/state-zustand.d.ts +0 -47
  109. package/browser/internal/state-zustand.js +0 -85
  110. package/browser/internal/test.d.ts +0 -0
  111. package/browser/internal/test.js +0 -15
  112. package/browser/internal/useMultiValueMap.d.ts +0 -1
  113. package/browser/internal/useMultiValueMap.js +0 -11
  114. package/browser/internal/watch.d.ts +0 -18
  115. package/browser/internal/watch.js +0 -122
  116. package/browser/lowLevelHooks.d.ts +0 -0
  117. package/browser/lowLevelHooks.js +0 -1
  118. package/browser/test-data/testFormData.d.ts +0 -15
  119. package/browser/test-data/testFormData.js +0 -46
  120. package/browser/types.d.ts +0 -1
  121. package/browser/types.js +0 -1
  122. package/browser/validation/validation.test.d.ts +0 -1
  123. package/browser/validation/validation.test.js +0 -274
  124. package/browser/validation/withYup.d.ts +0 -6
  125. package/browser/validation/withYup.js +0 -40
  126. package/browser/validation/withZod.d.ts +0 -6
  127. package/browser/validation/withZod.js +0 -50
  128. package/build/ValidatedForm.js +0 -257
  129. package/build/hooks.js +0 -79
  130. package/build/index.js +0 -18
  131. package/build/internal/MultiValueMap.js +0 -44
  132. package/build/internal/SingleTypeMultiValueMap.d.ts +0 -8
  133. package/build/internal/SingleTypeMultiValueMap.js +0 -45
  134. package/build/internal/constants.js +0 -7
  135. package/build/internal/flatten.js +0 -14
  136. package/build/internal/formContext.js +0 -5
  137. package/build/internal/getInputProps.js +0 -57
  138. package/build/internal/hooks-valtio.d.ts +0 -18
  139. package/build/internal/hooks-valtio.js +0 -128
  140. package/build/internal/hooks-zustand.d.ts +0 -16
  141. package/build/internal/hooks-zustand.js +0 -117
  142. package/build/internal/hooks.d.ts +0 -21
  143. package/build/internal/hooks.js +0 -128
  144. package/build/internal/hydratable.js +0 -17
  145. package/build/internal/immerMiddleware.d.ts +0 -6
  146. package/build/internal/immerMiddleware.js +0 -14
  147. package/build/internal/logic/elementUtils.d.ts +0 -3
  148. package/build/internal/logic/elementUtils.js +0 -9
  149. package/build/internal/logic/getCheckboxChecked.js +0 -13
  150. package/build/internal/logic/getRadioChecked.js +0 -9
  151. package/build/internal/logic/setFieldValue.d.ts +0 -1
  152. package/build/internal/logic/setFieldValue.js +0 -47
  153. package/build/internal/logic/setInputValueInForm.d.ts +0 -1
  154. package/build/internal/logic/setInputValueInForm.js +0 -84
  155. package/build/internal/setFormValues.d.ts +0 -2
  156. package/build/internal/setFormValues.js +0 -33
  157. package/build/internal/state/atomUtils.d.ts +0 -38
  158. package/build/internal/state/atomUtils.js +0 -13
  159. package/build/internal/state/controlledFields.d.ts +0 -62
  160. package/build/internal/state/controlledFields.js +0 -85
  161. package/build/internal/state-valtio.d.ts +0 -62
  162. package/build/internal/state-valtio.js +0 -83
  163. package/build/internal/state-zustand.d.ts +0 -47
  164. package/build/internal/state-zustand.js +0 -91
  165. package/build/internal/state.d.ts +0 -370
  166. package/build/internal/state.js +0 -76
  167. package/build/internal/submissionCallbacks.js +0 -17
  168. package/build/internal/test.d.ts +0 -1
  169. package/build/internal/test.js +0 -12
  170. package/build/internal/util.js +0 -41
  171. package/build/internal/watch.d.ts +0 -20
  172. package/build/internal/watch.js +0 -126
  173. package/build/server.js +0 -32
  174. package/build/types.d.ts +0 -1
  175. package/build/types.js +0 -2
  176. package/build/unreleased/formStateHooks.js +0 -59
  177. package/build/userFacingFormContext.js +0 -30
  178. package/build/validation/createValidator.js +0 -45
  179. package/build/validation/types.js +0 -2
  180. package/src/internal/state/atomUtils.ts +0 -13
  181. 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
- useFormAtomValue,
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, "useIsValid");
39
- const formProps = useFormAtomValue(formPropsAtom(formContext.formId));
40
- const isSubmitting = useFormAtomValue(isSubmittingAtom(formContext.formId));
41
- const hasBeenSubmitted = useFormAtomValue(
42
- hasBeenSubmittedAtom(formContext.formId)
43
- );
44
- const touchedFields = useFormAtomValue(touchedFieldsAtom(formContext.formId));
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 = defaultValuesToUse.hydrateTo(
49
- formProps.defaultValues
50
- );
51
+ const hydratedDefaultValues =
52
+ defaultValuesToUse.hydrateTo(syncedDefaultValues);
51
53
 
52
- const fieldErrorsFromState = useFormAtomValue(
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
- ...formProps,
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
- formProps,
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 { validateField } = useFormAtomValue(formPropsAtom(formContext.formId));
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 { useFormAtomValue, useInternalFormContext } from "./internal/hooks";
3
- import { formPropsAtom } from "./internal/state";
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 { registerReceiveFocus } = useFormAtomValue(
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
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "extends": "tsconfig/tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "esnext"
5
+ },
3
6
  "include": ["src/**/*.ts", "src/**/*.tsx"],
4
- "exclude": ["node_modules"]
7
+ "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx"]
5
8
  }
package/vite.config.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { makeConfig } from "vite-config";
2
+
3
+ export default makeConfig({
4
+ lib: "remix-validated-form",
5
+ external: ["react", "@remix-run/react", "@remix-run/server-runtime"],
6
+ dir: __dirname,
7
+ });