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.
Files changed (52) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/browser/ValidatedForm.js +31 -36
  3. package/browser/hooks.js +13 -16
  4. package/browser/internal/customState.d.ts +105 -0
  5. package/browser/internal/customState.js +46 -0
  6. package/browser/internal/getInputProps.js +4 -14
  7. package/browser/internal/hooks.d.ts +14 -15
  8. package/browser/internal/hooks.js +37 -39
  9. package/browser/internal/logic/elementUtils.d.ts +3 -0
  10. package/browser/internal/logic/elementUtils.js +3 -0
  11. package/browser/internal/logic/setInputValueInForm.js +9 -52
  12. package/browser/internal/setFieldValue.d.ts +0 -0
  13. package/browser/internal/setFieldValue.js +0 -0
  14. package/browser/internal/setFormValues.d.ts +0 -0
  15. package/browser/internal/setFormValues.js +0 -0
  16. package/browser/internal/state.d.ts +339 -238
  17. package/browser/internal/state.js +59 -72
  18. package/browser/internal/watch.d.ts +18 -0
  19. package/browser/internal/watch.js +122 -0
  20. package/browser/unreleased/formStateHooks.d.ts +39 -0
  21. package/browser/unreleased/formStateHooks.js +54 -0
  22. package/browser/userFacingFormContext.js +9 -23
  23. package/build/ValidatedForm.js +30 -35
  24. package/build/hooks.js +11 -14
  25. package/build/internal/getInputProps.js +4 -14
  26. package/build/internal/hooks.d.ts +14 -15
  27. package/build/internal/hooks.js +39 -41
  28. package/build/internal/logic/elementUtils.d.ts +3 -0
  29. package/build/internal/logic/elementUtils.js +9 -0
  30. package/build/internal/logic/setInputValueInForm.js +12 -55
  31. package/build/internal/setFormValues.d.ts +0 -0
  32. package/build/internal/setFormValues.js +0 -0
  33. package/build/internal/state/controlledFields.js +11 -2
  34. package/build/internal/state.d.ts +339 -238
  35. package/build/internal/state.js +61 -77
  36. package/build/internal/watch.d.ts +20 -0
  37. package/build/internal/watch.js +126 -0
  38. package/build/unreleased/formStateHooks.d.ts +39 -0
  39. package/build/unreleased/formStateHooks.js +59 -0
  40. package/build/userFacingFormContext.js +9 -23
  41. package/package.json +1 -2
  42. package/src/ValidatedForm.tsx +48 -52
  43. package/src/hooks.ts +15 -26
  44. package/src/internal/getInputProps.ts +4 -14
  45. package/src/internal/hooks.ts +60 -72
  46. package/src/internal/hydratable.ts +28 -0
  47. package/src/internal/logic/getCheckboxChecked.ts +10 -0
  48. package/src/internal/logic/getRadioChecked.ts +7 -0
  49. package/src/internal/state/atomUtils.ts +13 -0
  50. package/src/internal/state.ts +99 -177
  51. package/src/unreleased/formStateHooks.ts +113 -0
  52. package/src/userFacingFormContext.ts +14 -53
@@ -1,210 +1,132 @@
1
1
  import { atom } from "jotai";
2
- import { atomWithImmer } from "jotai/immer";
3
2
  import { atomFamily, selectAtom } from "jotai/utils";
4
- import lodashGet from "lodash/get";
3
+ import omit from "lodash/omit";
5
4
  import { FieldErrors, TouchedFields } from "../validation/types";
5
+ import {
6
+ fieldAtomFamily,
7
+ formAtomFamily,
8
+ InternalFormId,
9
+ } from "./state/atomUtils";
6
10
 
7
11
  export const ATOM_SCOPE = Symbol("remix-validated-form-scope");
8
12
 
9
- export type FormState = {
10
- // Actual state
11
- hydrated: boolean;
12
- fieldErrors?: FieldErrors;
13
- isSubmitting: boolean;
14
- hasBeenSubmitted: boolean;
15
- touchedFields: TouchedFields;
16
-
17
- // Populated by the form component
13
+ export type SyncedFormProps = {
18
14
  formId?: string;
19
15
  action?: string;
20
16
  subaction?: string;
21
- defaultValues?: { [fieldName: string]: any };
17
+ defaultValues: { [fieldName: string]: any };
22
18
  validateField: (fieldName: string) => Promise<string | null>;
23
19
  registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
24
20
  };
25
21
 
26
- export type FormAtom = ReturnType<typeof formRegistry>;
27
-
28
- export type FieldState = {
29
- touched: boolean;
30
- defaultValue?: any;
31
- error?: string;
22
+ export const isHydratedAtom = formAtomFamily(false);
23
+ export const isSubmittingAtom = formAtomFamily(false);
24
+ export const hasBeenSubmittedAtom = formAtomFamily(false);
25
+ export const fieldErrorsAtom = formAtomFamily<FieldErrors>({});
26
+ export const touchedFieldsAtom = formAtomFamily<TouchedFields>({});
27
+ export const formPropsAtom = formAtomFamily<SyncedFormProps>({
28
+ validateField: () => Promise.resolve(null),
29
+ registerReceiveFocus: () => () => {},
30
+ defaultValues: {},
31
+ });
32
+ export const formElementAtom = formAtomFamily<HTMLFormElement | null>(null);
33
+
34
+ //// Everything below is derived from the above
35
+
36
+ export const cleanupFormState = (formId: InternalFormId) => {
37
+ [
38
+ isHydratedAtom,
39
+ isSubmittingAtom,
40
+ hasBeenSubmittedAtom,
41
+ fieldErrorsAtom,
42
+ touchedFieldsAtom,
43
+ formPropsAtom,
44
+ ].forEach((formAtom) => formAtom.remove(formId));
32
45
  };
33
46
 
34
- export const formRegistry = atomFamily((formId: string | symbol) =>
35
- atomWithImmer<FormState>({
36
- hydrated: false,
37
- isSubmitting: false,
38
- hasBeenSubmitted: false,
39
- touchedFields: {},
40
-
41
- // The symbol version is just to keep things straight with the `atomFamily`
42
- formId: typeof formId === "string" ? formId : undefined,
43
-
44
- // Will change upon hydration -- these will never actually be used
45
- validateField: () => Promise.resolve(null),
46
- registerReceiveFocus: () => () => {},
47
- })
47
+ export const isValidAtom = atomFamily((formId: InternalFormId) =>
48
+ atom((get) => Object.keys(get(fieldErrorsAtom(formId))).length === 0)
48
49
  );
49
50
 
50
- export const fieldErrorAtom = (name: string) => (formAtom: FormAtom) =>
51
- selectAtom(formAtom, (formState) => formState.fieldErrors?.[name]);
52
-
53
- export const fieldTouchedAtom = (name: string) => (formAtom: FormAtom) =>
54
- selectAtom(formAtom, (formState) => formState.touchedFields[name]);
55
-
56
- export const fieldDefaultValueAtom = (name: string) => (formAtom: FormAtom) =>
57
- selectAtom(
58
- formAtom,
59
- (formState) =>
60
- formState.defaultValues && lodashGet(formState.defaultValues, name)
61
- );
62
-
63
- // Selector atoms
64
-
65
- export const formSelectorAtom =
66
- <T>(selector: (state: FormState) => T) =>
67
- (formAtom: FormAtom) =>
68
- selectAtom(formAtom, selector);
69
-
70
- export const fieldErrorsAtom = formSelectorAtom((state) => state.fieldErrors);
71
- export const touchedFieldsAtom = formSelectorAtom(
72
- (state) => state.touchedFields
73
- );
74
- export const actionAtom = formSelectorAtom((state) => state.action);
75
- export const hasBeenSubmittedAtom = formSelectorAtom(
76
- (state) => state.hasBeenSubmitted
77
- );
78
- export const validateFieldAtom = formSelectorAtom(
79
- (state) => state.validateField
80
- );
81
- export const registerReceiveFocusAtom = formSelectorAtom(
82
- (state) => state.registerReceiveFocus
83
- );
84
- export const isSubmittingAtom = formSelectorAtom((state) => state.isSubmitting);
85
- export const defaultValuesAtom = formSelectorAtom(
86
- (state) => state.defaultValues
87
- );
88
- export const isValidAtom = formSelectorAtom(
89
- (state) => Object.keys(state.fieldErrors ?? {}).length === 0
51
+ export const resetAtom = atomFamily((formId: InternalFormId) =>
52
+ atom(null, (_get, set) => {
53
+ set(fieldErrorsAtom(formId), {});
54
+ set(touchedFieldsAtom(formId), {});
55
+ set(hasBeenSubmittedAtom(formId), false);
56
+ })
90
57
  );
91
- export const isHydratedAtom = formSelectorAtom((state) => state.hydrated);
92
-
93
- // Update atoms
94
-
95
- export type FieldAtomArgs = {
96
- name: string;
97
- formAtom: FormAtom;
98
- };
99
58
 
100
- export const clearErrorAtom = atom(
101
- null,
102
- (get, set, { name, formAtom }: FieldAtomArgs) =>
103
- set(formAtom, (state) => {
104
- delete state.fieldErrors?.[name];
105
- return state;
106
- })
59
+ export const startSubmitAtom = atomFamily((formId: InternalFormId) =>
60
+ atom(null, (_get, set) => {
61
+ set(isSubmittingAtom(formId), true);
62
+ set(hasBeenSubmittedAtom(formId), true);
63
+ })
107
64
  );
108
65
 
109
- export const addErrorAtom = atom(
110
- null,
111
- (get, set, { name, formAtom, error }: FieldAtomArgs & { error: string }) =>
112
- set(formAtom, (state) => {
113
- if (!state.fieldErrors) state.fieldErrors = {};
114
- state.fieldErrors[name] = error;
115
- return state;
116
- })
66
+ export const endSubmitAtom = atomFamily((formId: InternalFormId) =>
67
+ atom(null, (_get, set) => {
68
+ set(isSubmittingAtom(formId), false);
69
+ })
117
70
  );
118
71
 
119
- export const setFieldErrorsAtom = atom(
120
- null,
121
- (
122
- get,
123
- set,
124
- { formAtom, fieldErrors }: { fieldErrors: FieldErrors; formAtom: FormAtom }
125
- ) =>
126
- set(formAtom, (state) => {
127
- state.fieldErrors = fieldErrors;
128
- return state;
129
- })
72
+ export const setTouchedAtom = atomFamily((formId: InternalFormId) =>
73
+ atom(
74
+ null,
75
+ (get, set, { field, touched }: { field: string; touched: boolean }) => {
76
+ const prev = get(touchedFieldsAtom(formId));
77
+ if (prev[field] !== touched) {
78
+ set(touchedFieldsAtom(formId), {
79
+ ...prev,
80
+ [field]: touched,
81
+ });
82
+ }
83
+ }
84
+ )
130
85
  );
131
86
 
132
- export const setTouchedAtom = atom(
133
- null,
134
- (
135
- get,
136
- set,
137
- { name, formAtom, touched }: FieldAtomArgs & { touched: boolean }
138
- ) =>
139
- set(formAtom, (state) => {
140
- state.touchedFields[name] = touched;
141
- return state;
142
- })
87
+ export const setFieldErrorAtom = atomFamily((formId: InternalFormId) =>
88
+ atom(
89
+ null,
90
+ (
91
+ get,
92
+ set,
93
+ { field, error }: { field: string; error: string | undefined }
94
+ ) => {
95
+ const prev = get(fieldErrorsAtom(formId));
96
+ if (error === undefined && field in prev) {
97
+ set(fieldErrorsAtom(formId), omit(prev, field));
98
+ }
99
+
100
+ if (error !== undefined && prev[field] !== error) {
101
+ set(fieldErrorsAtom(formId), {
102
+ ...get(fieldErrorsAtom(formId)),
103
+ [field]: error,
104
+ });
105
+ }
106
+ }
107
+ )
143
108
  );
144
109
 
145
- export const resetAtom = atom(
146
- null,
147
- (get, set, { formAtom }: { formAtom: FormAtom }) => {
148
- set(formAtom, (state) => {
149
- state.fieldErrors = {};
150
- state.touchedFields = {};
151
- state.hasBeenSubmitted = false;
152
- return state;
153
- });
154
- }
155
- );
110
+ //// Field specific
156
111
 
157
- export const startSubmitAtom = atom(
158
- null,
159
- (get, set, { formAtom }: { formAtom: FormAtom }) => {
160
- set(formAtom, (state) => {
161
- state.hasBeenSubmitted = true;
162
- state.isSubmitting = true;
163
- return state;
164
- });
165
- }
112
+ export const fieldTouchedAtom = fieldAtomFamily(({ formId, field }) =>
113
+ atom(
114
+ (get) => get(touchedFieldsAtom(formId))[field],
115
+ (_get, set, touched: boolean) => {
116
+ set(setTouchedAtom(formId), { field, touched });
117
+ }
118
+ )
166
119
  );
167
120
 
168
- export const endSubmitAtom = atom(
169
- null,
170
- (get, set, { formAtom }: { formAtom: FormAtom }) => {
171
- set(formAtom, (state) => {
172
- state.isSubmitting = false;
173
- return state;
174
- });
175
- }
121
+ export const fieldErrorAtom = fieldAtomFamily(({ formId, field }) =>
122
+ atom(
123
+ (get) => get(fieldErrorsAtom(formId))[field],
124
+ (_get, set, error: string | undefined) => {
125
+ set(setFieldErrorAtom(formId), { field, error });
126
+ }
127
+ )
176
128
  );
177
129
 
178
- type SyncFormContextArgs = {
179
- defaultValues?: { [fieldName: string]: any };
180
- action?: string;
181
- subaction?: string;
182
- validateField: FormState["validateField"];
183
- registerReceiveFocus: FormState["registerReceiveFocus"];
184
- formAtom: FormAtom;
185
- };
186
- export const syncFormContextAtom = atom(
187
- null,
188
- (
189
- get,
190
- set,
191
- {
192
- defaultValues,
193
- action,
194
- subaction,
195
- formAtom,
196
- validateField,
197
- registerReceiveFocus,
198
- }: SyncFormContextArgs
199
- ) => {
200
- set(formAtom, (state) => {
201
- state.defaultValues = defaultValues;
202
- state.action = action;
203
- state.subaction = subaction;
204
- state.registerReceiveFocus = registerReceiveFocus;
205
- state.validateField = validateField;
206
- state.hydrated = true;
207
- return state;
208
- });
209
- }
130
+ export const fieldDefaultValueAtom = fieldAtomFamily(({ formId, field }) =>
131
+ selectAtom(formPropsAtom(formId), (state) => state.defaultValues[field])
210
132
  );
@@ -0,0 +1,113 @@
1
+ import { useMemo } from "react";
2
+ import {} from "../internal/getInputProps";
3
+ import {
4
+ useInternalFormContext,
5
+ useClearError,
6
+ useSetTouched,
7
+ useDefaultValuesForForm,
8
+ useFieldErrorsForForm,
9
+ useFormAtomValue,
10
+ } from "../internal/hooks";
11
+ import {
12
+ fieldErrorsAtom,
13
+ formPropsAtom,
14
+ hasBeenSubmittedAtom,
15
+ isSubmittingAtom,
16
+ isValidAtom,
17
+ touchedFieldsAtom,
18
+ } from "../internal/state";
19
+ import { FieldErrors, TouchedFields } from "../validation/types";
20
+
21
+ export type FormState = {
22
+ fieldErrors: FieldErrors;
23
+ isSubmitting: boolean;
24
+ hasBeenSubmitted: boolean;
25
+ touchedFields: TouchedFields;
26
+ defaultValues: { [fieldName: string]: any };
27
+ action?: string;
28
+ subaction?: string;
29
+ isValid: boolean;
30
+ };
31
+
32
+ /**
33
+ * Returns information about the form.
34
+ *
35
+ * @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
36
+ */
37
+ 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));
46
+
47
+ const defaultValuesToUse = useDefaultValuesForForm(formContext);
48
+ const hydratedDefaultValues = defaultValuesToUse.hydrateTo(
49
+ formProps.defaultValues
50
+ );
51
+
52
+ const fieldErrorsFromState = useFormAtomValue(
53
+ fieldErrorsAtom(formContext.formId)
54
+ );
55
+ const fieldErrorsToUse = useFieldErrorsForForm(formContext);
56
+ const hydratedFieldErrors = fieldErrorsToUse.hydrateTo(fieldErrorsFromState);
57
+
58
+ return useMemo(
59
+ () => ({
60
+ ...formProps,
61
+ defaultValues: hydratedDefaultValues,
62
+ fieldErrors: hydratedFieldErrors ?? {},
63
+ hasBeenSubmitted,
64
+ isSubmitting,
65
+ touchedFields,
66
+ isValid,
67
+ }),
68
+ [
69
+ formProps,
70
+ hasBeenSubmitted,
71
+ hydratedDefaultValues,
72
+ hydratedFieldErrors,
73
+ isSubmitting,
74
+ isValid,
75
+ touchedFields,
76
+ ]
77
+ );
78
+ };
79
+
80
+ export type FormHelpers = {
81
+ /**
82
+ * Clear the error of the specified field.
83
+ */
84
+ clearError: (fieldName: string) => void;
85
+ /**
86
+ * Validate the specified field.
87
+ */
88
+ validateField: (fieldName: string) => Promise<string | null>;
89
+ /**
90
+ * Change the touched state of the specified field.
91
+ */
92
+ setTouched: (fieldName: string, touched: boolean) => void;
93
+ };
94
+
95
+ /**
96
+ * Returns helpers that can be used to update the form state.
97
+ *
98
+ * @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
99
+ */
100
+ export const useFormHelpers = (formId?: string): FormHelpers => {
101
+ const formContext = useInternalFormContext(formId, "useFormHelpers");
102
+ const setTouched = useSetTouched(formContext);
103
+ const { validateField } = useFormAtomValue(formPropsAtom(formContext.formId));
104
+ const clearError = useClearError(formContext);
105
+ return useMemo(
106
+ () => ({
107
+ setTouched,
108
+ validateField,
109
+ clearError,
110
+ }),
111
+ [clearError, setTouched, validateField]
112
+ );
113
+ };
@@ -1,23 +1,7 @@
1
1
  import { useCallback } from "react";
2
- import { useIsSubmitting, useIsValid } from "./hooks";
3
- import {
4
- useClearError,
5
- useContextSelectAtom,
6
- useDefaultValuesForForm,
7
- useFieldErrorsForForm,
8
- useHydratableSelector,
9
- useInternalFormContext,
10
- useSetTouched,
11
- } from "./internal/hooks";
12
- import {
13
- actionAtom,
14
- defaultValuesAtom,
15
- fieldErrorsAtom,
16
- hasBeenSubmittedAtom,
17
- registerReceiveFocusAtom,
18
- touchedFieldsAtom,
19
- validateFieldAtom,
20
- } from "./internal/state";
2
+ import { useFormAtomValue, useInternalFormContext } from "./internal/hooks";
3
+ import { formPropsAtom } from "./internal/state";
4
+ import { useFormHelpers, useFormState } from "./unreleased/formStateHooks";
21
5
  import { FieldErrors, TouchedFields } from "./validation/types";
22
6
 
23
7
  export type FormContextValue = {
@@ -76,34 +60,17 @@ export type FormContextValue = {
76
60
  export const useFormContext = (formId?: string): FormContextValue => {
77
61
  // Try to access context so we get our error specific to this hook if it's not there
78
62
  const context = useInternalFormContext(formId, "useFormContext");
63
+ const state = useFormState(formId);
64
+ const {
65
+ clearError: internalClearError,
66
+ setTouched,
67
+ validateField,
68
+ } = useFormHelpers(formId);
79
69
 
80
- const action = useContextSelectAtom(context.formId, actionAtom);
81
- const isSubmitting = useIsSubmitting(formId);
82
- const hasBeenSubmitted = useContextSelectAtom(
83
- context.formId,
84
- hasBeenSubmittedAtom
85
- );
86
- const isValid = useIsValid(formId);
87
- const defaultValues = useHydratableSelector(
88
- context,
89
- defaultValuesAtom,
90
- useDefaultValuesForForm(context)
91
- );
92
- const fieldErrors = useHydratableSelector(
93
- context,
94
- fieldErrorsAtom,
95
- useFieldErrorsForForm(context)
96
- );
97
-
98
- const setFieldTouched = useSetTouched(context);
99
- const touchedFields = useContextSelectAtom(context.formId, touchedFieldsAtom);
100
- const validateField = useContextSelectAtom(context.formId, validateFieldAtom);
101
- const registerReceiveFocus = useContextSelectAtom(
102
- context.formId,
103
- registerReceiveFocusAtom
70
+ const { registerReceiveFocus } = useFormAtomValue(
71
+ formPropsAtom(context.formId)
104
72
  );
105
73
 
106
- const internalClearError = useClearError(context);
107
74
  const clearError = useCallback(
108
75
  (...names: string[]) => {
109
76
  names.forEach((name) => {
@@ -114,16 +81,10 @@ export const useFormContext = (formId?: string): FormContextValue => {
114
81
  );
115
82
 
116
83
  return {
117
- isSubmitting,
118
- hasBeenSubmitted,
119
- isValid,
120
- defaultValues,
121
- clearError,
122
- fieldErrors: fieldErrors ?? {},
123
- action,
124
- setFieldTouched,
125
- touchedFields,
84
+ ...state,
85
+ setFieldTouched: setTouched,
126
86
  validateField,
87
+ clearError,
127
88
  registerReceiveFocus,
128
89
  };
129
90
  };