remix-validated-form 4.0.1 → 4.1.0-beta.3

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 (66) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/README.md +4 -4
  3. package/browser/ValidatedForm.d.ts +2 -2
  4. package/browser/ValidatedForm.js +142 -149
  5. package/browser/components.d.ts +5 -8
  6. package/browser/components.js +5 -5
  7. package/browser/hooks.d.ts +19 -14
  8. package/browser/hooks.js +41 -39
  9. package/browser/index.d.ts +1 -1
  10. package/browser/index.js +1 -0
  11. package/browser/internal/constants.d.ts +3 -0
  12. package/browser/internal/constants.js +3 -0
  13. package/browser/internal/formContext.d.ts +7 -49
  14. package/browser/internal/formContext.js +1 -1
  15. package/browser/internal/getInputProps.js +4 -3
  16. package/browser/internal/hooks.d.ts +22 -0
  17. package/browser/internal/hooks.js +110 -0
  18. package/browser/internal/state.d.ts +269 -0
  19. package/browser/internal/state.js +82 -0
  20. package/browser/internal/util.d.ts +1 -0
  21. package/browser/internal/util.js +2 -0
  22. package/browser/lowLevelHooks.d.ts +0 -0
  23. package/browser/lowLevelHooks.js +1 -0
  24. package/browser/server.d.ts +5 -0
  25. package/browser/server.js +5 -0
  26. package/browser/userFacingFormContext.d.ts +56 -0
  27. package/browser/userFacingFormContext.js +40 -0
  28. package/browser/validation/createValidator.js +4 -0
  29. package/browser/validation/types.d.ts +3 -0
  30. package/build/ValidatedForm.d.ts +2 -2
  31. package/build/ValidatedForm.js +138 -145
  32. package/build/hooks.d.ts +19 -14
  33. package/build/hooks.js +43 -45
  34. package/build/index.d.ts +1 -1
  35. package/build/index.js +1 -0
  36. package/build/internal/constants.d.ts +3 -0
  37. package/build/internal/constants.js +7 -0
  38. package/build/internal/formContext.d.ts +7 -49
  39. package/build/internal/formContext.js +2 -2
  40. package/build/internal/getInputProps.js +7 -3
  41. package/build/internal/hooks.d.ts +22 -0
  42. package/build/internal/hooks.js +130 -0
  43. package/build/internal/state.d.ts +269 -0
  44. package/build/internal/state.js +92 -0
  45. package/build/internal/util.d.ts +1 -0
  46. package/build/internal/util.js +3 -1
  47. package/build/server.d.ts +5 -0
  48. package/build/server.js +7 -1
  49. package/build/userFacingFormContext.d.ts +56 -0
  50. package/build/userFacingFormContext.js +44 -0
  51. package/build/validation/createValidator.js +4 -0
  52. package/build/validation/types.d.ts +3 -0
  53. package/package.json +3 -1
  54. package/src/ValidatedForm.tsx +205 -200
  55. package/src/hooks.ts +71 -54
  56. package/src/index.ts +1 -1
  57. package/src/internal/constants.ts +4 -0
  58. package/src/internal/formContext.ts +8 -49
  59. package/src/internal/getInputProps.ts +6 -4
  60. package/src/internal/hooks.ts +191 -0
  61. package/src/internal/state.ts +210 -0
  62. package/src/internal/util.ts +4 -0
  63. package/src/server.ts +16 -0
  64. package/src/userFacingFormContext.ts +129 -0
  65. package/src/validation/createValidator.ts +4 -0
  66. package/src/validation/types.ts +3 -1
package/src/hooks.ts CHANGED
@@ -1,20 +1,46 @@
1
- import get from "lodash/get";
2
- import toPath from "lodash/toPath";
3
- import { useContext, useEffect, useMemo } from "react";
4
- import { FormContext } from "./internal/formContext";
1
+ import { useEffect, useMemo } from "react";
5
2
  import {
6
3
  createGetInputProps,
7
4
  GetInputProps,
8
5
  ValidationBehaviorOptions,
9
6
  } from "./internal/getInputProps";
7
+ import {
8
+ useInternalFormContext,
9
+ useFieldTouched,
10
+ useFieldError,
11
+ useFieldDefaultValue,
12
+ useContextSelectAtom,
13
+ useClearError,
14
+ useSetTouched,
15
+ } from "./internal/hooks";
16
+ import {
17
+ hasBeenSubmittedAtom,
18
+ isSubmittingAtom,
19
+ isValidAtom,
20
+ registerReceiveFocusAtom,
21
+ validateFieldAtom,
22
+ } from "./internal/state";
23
+
24
+ /**
25
+ * Returns whether or not the parent form is currently being submitted.
26
+ * This is different from remix's `useTransition().submission` in that it
27
+ * is aware of what form it's in and when _that_ form is being submitted.
28
+ *
29
+ * @param formId
30
+ */
31
+ export const useIsSubmitting = (formId?: string) => {
32
+ const formContext = useInternalFormContext(formId, "useIsSubmitting");
33
+ return useContextSelectAtom(formContext.formId, isSubmittingAtom);
34
+ };
10
35
 
11
- const useInternalFormContext = (hookName: string) => {
12
- const context = useContext(FormContext);
13
- if (!context)
14
- throw new Error(
15
- `${hookName} must be used within a ValidatedForm component`
16
- );
17
- return context;
36
+ /**
37
+ * Returns whether or not the current form is valid.
38
+ *
39
+ * @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
40
+ */
41
+ export const useIsValid = (formId?: string) => {
42
+ const formContext = useInternalFormContext(formId, "useIsValid");
43
+ return useContextSelectAtom(formContext.formId, isValidAtom);
18
44
  };
19
45
 
20
46
  export type FieldProps = {
@@ -64,21 +90,34 @@ export const useField = (
64
90
  * Allows you to specify when a field gets validated (when using getInputProps)
65
91
  */
66
92
  validationBehavior?: Partial<ValidationBehaviorOptions>;
93
+ /**
94
+ * The formId of the form you want to use.
95
+ * This is not necesary if the input is used inside a form.
96
+ */
97
+ formId?: string;
67
98
  }
68
99
  ): FieldProps => {
69
- const {
70
- fieldErrors,
71
- clearError,
72
- validateField,
73
- defaultValues,
74
- registerReceiveFocus,
75
- touchedFields,
76
- setFieldTouched,
77
- hasBeenSubmitted,
78
- } = useInternalFormContext("useField");
100
+ const { handleReceiveFocus, formId: providedFormId } = options ?? {};
101
+ const formContext = useInternalFormContext(providedFormId, "useField");
102
+
103
+ const defaultValue = useFieldDefaultValue(name, formContext);
104
+ const touched = useFieldTouched(name, formContext);
105
+ const error = useFieldError(name, formContext);
79
106
 
80
- const isTouched = !!touchedFields[name];
81
- const { handleReceiveFocus } = options ?? {};
107
+ const clearError = useClearError(formContext);
108
+ const setTouched = useSetTouched(formContext);
109
+ const hasBeenSubmitted = useContextSelectAtom(
110
+ formContext.formId,
111
+ hasBeenSubmittedAtom
112
+ );
113
+ const validateField = useContextSelectAtom(
114
+ formContext.formId,
115
+ validateFieldAtom
116
+ );
117
+ const registerReceiveFocus = useContextSelectAtom(
118
+ formContext.formId,
119
+ registerReceiveFocusAtom
120
+ );
82
121
 
83
122
  useEffect(() => {
84
123
  if (handleReceiveFocus)
@@ -87,18 +126,14 @@ export const useField = (
87
126
 
88
127
  const field = useMemo<FieldProps>(() => {
89
128
  const helpers = {
90
- error: fieldErrors[name],
91
- clearError: () => {
92
- clearError(name);
93
- },
129
+ error,
130
+ clearError: () => clearError(name),
94
131
  validate: () => {
95
132
  validateField(name);
96
133
  },
97
- defaultValue: defaultValues
98
- ? get(defaultValues, toPath(name), undefined)
99
- : undefined,
100
- touched: isTouched,
101
- setTouched: (touched: boolean) => setFieldTouched(name, touched),
134
+ defaultValue,
135
+ touched,
136
+ setTouched: (touched: boolean) => setTouched(name, touched),
102
137
  };
103
138
  const getInputProps = createGetInputProps({
104
139
  ...helpers,
@@ -111,34 +146,16 @@ export const useField = (
111
146
  getInputProps,
112
147
  };
113
148
  }, [
114
- fieldErrors,
149
+ error,
150
+ defaultValue,
151
+ touched,
115
152
  name,
116
- defaultValues,
117
- isTouched,
118
153
  hasBeenSubmitted,
119
154
  options?.validationBehavior,
120
155
  clearError,
121
156
  validateField,
122
- setFieldTouched,
157
+ setTouched,
123
158
  ]);
124
159
 
125
160
  return field;
126
161
  };
127
-
128
- /**
129
- * Provides access to the entire form context.
130
- */
131
- export const useFormContext = () => useInternalFormContext("useFormContext");
132
-
133
- /**
134
- * Returns whether or not the parent form is currently being submitted.
135
- * This is different from remix's `useTransition().submission` in that it
136
- * is aware of what form it's in and when _that_ form is being submitted.
137
- */
138
- export const useIsSubmitting = () =>
139
- useInternalFormContext("useIsSubmitting").isSubmitting;
140
-
141
- /**
142
- * Returns whether or not the current form is valid.
143
- */
144
- export const useIsValid = () => useInternalFormContext("useIsValid").isValid;
package/src/index.ts CHANGED
@@ -3,4 +3,4 @@ export * from "./server";
3
3
  export * from "./ValidatedForm";
4
4
  export * from "./validation/types";
5
5
  export * from "./validation/createValidator";
6
- export type { FormContextValue } from "./internal/formContext";
6
+ export * from "./userFacingFormContext";
@@ -0,0 +1,4 @@
1
+ export const FORM_ID_FIELD = "__rvfInternalFormId" as const;
2
+ export const FORM_DEFAULTS_FIELD = "__rvfInternalFormDefaults" as const;
3
+ export const formDefaultValuesKey = (formId: string) =>
4
+ `${FORM_DEFAULTS_FIELD}_${formId}`;
@@ -1,54 +1,13 @@
1
+ import { useFetcher } from "@remix-run/react";
1
2
  import { createContext } from "react";
2
- import { FieldErrors, TouchedFields } from "../validation/types";
3
3
 
4
- export type FormContextValue = {
5
- /**
6
- * All the errors in all the fields in the form.
7
- */
8
- fieldErrors: FieldErrors;
9
- /**
10
- * Clear the errors of the specified fields.
11
- */
12
- clearError: (...names: string[]) => void;
13
- /**
14
- * Validate the specified field.
15
- */
16
- validateField: (fieldName: string) => Promise<string | null>;
17
- /**
18
- * The `action` prop of the form.
19
- */
4
+ export type InternalFormContextValue = {
5
+ formId: string | symbol;
20
6
  action?: string;
21
- /**
22
- * Whether or not the form is submitting.
23
- */
24
- isSubmitting: boolean;
25
- /**
26
- * Whether or not a submission has been attempted.
27
- * This is true once the form has been submitted, even if there were validation errors.
28
- * Resets to false when the form is reset.
29
- */
30
- hasBeenSubmitted: boolean;
31
- /**
32
- * Whether or not the form is valid.
33
- */
34
- isValid: boolean;
35
- /**
36
- * The default values of the form.
37
- */
38
- defaultValues?: { [fieldName: string]: any };
39
- /**
40
- * Register a custom focus handler to be used when
41
- * the field needs to receive focus due to a validation error.
42
- */
43
- registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
44
- /**
45
- * Any fields that have been touched by the user.
46
- */
47
- touchedFields: TouchedFields;
48
- /**
49
- * Change the touched state of the specified field.
50
- */
51
- setFieldTouched: (fieldName: string, touched: boolean) => void;
7
+ subaction?: string;
8
+ defaultValuesProp?: { [fieldName: string]: any };
9
+ fetcher?: ReturnType<typeof useFetcher>;
52
10
  };
53
11
 
54
- export const FormContext = createContext<FormContextValue | null>(null);
12
+ export const InternalFormContext =
13
+ createContext<InternalFormContextValue | null>(null);
@@ -1,3 +1,5 @@
1
+ import omitBy from "lodash/omitBy";
2
+
1
3
  export type ValidationBehavior = "onBlur" | "onChange" | "onSubmit";
2
4
 
3
5
  export type ValidationBehaviorOptions = {
@@ -68,7 +70,7 @@ export const createGetInputProps = ({
68
70
  ? validationBehaviors.whenTouched
69
71
  : validationBehaviors.initial;
70
72
 
71
- const inputProps: T = {
73
+ const inputProps: MinimalInputProps = {
72
74
  ...props,
73
75
  onChange: (...args: unknown[]) => {
74
76
  if (behavior === "onChange") validate();
@@ -83,19 +85,19 @@ export const createGetInputProps = ({
83
85
  name,
84
86
  };
85
87
 
86
- if (inputProps.type === "checkbox") {
88
+ if (props.type === "checkbox") {
87
89
  const value = props.value ?? "on";
88
90
  inputProps.defaultChecked = getCheckboxDefaultChecked(
89
91
  value,
90
92
  defaultValue
91
93
  );
92
- } else if (inputProps.type === "radio") {
94
+ } else if (props.type === "radio") {
93
95
  const value = props.value ?? "on";
94
96
  inputProps.defaultChecked = defaultValue === value;
95
97
  } else {
96
98
  inputProps.defaultValue = defaultValue;
97
99
  }
98
100
 
99
- return inputProps;
101
+ return omitBy(inputProps, (value) => value === undefined) as T;
100
102
  };
101
103
  };
@@ -0,0 +1,191 @@
1
+ import { useActionData, useMatches, useTransition } from "@remix-run/react";
2
+ import { Atom } from "jotai";
3
+ import { useAtomValue, useUpdateAtom } from "jotai/utils";
4
+ import lodashGet from "lodash/get";
5
+ import identity from "lodash/identity";
6
+ import { useCallback, useContext, useMemo } from "react";
7
+ import { ValidationErrorResponseData } from "..";
8
+ import { formDefaultValuesKey } from "./constants";
9
+ import { InternalFormContext, InternalFormContextValue } from "./formContext";
10
+ import {
11
+ ATOM_SCOPE,
12
+ clearErrorAtom,
13
+ fieldDefaultValueAtom,
14
+ fieldErrorAtom,
15
+ fieldTouchedAtom,
16
+ FormAtom,
17
+ formRegistry,
18
+ isHydratedAtom,
19
+ setTouchedAtom,
20
+ } from "./state";
21
+
22
+ type FormSelectorAtomCreator<T> = (formState: FormAtom) => Atom<T>;
23
+ const USE_HYDRATED_STATE = Symbol("USE_HYDRATED_STATE");
24
+
25
+ export const useInternalFormContext = (
26
+ formId?: string | symbol,
27
+ hookName?: string
28
+ ) => {
29
+ const formContext = useContext(InternalFormContext);
30
+
31
+ if (formId) return { formId };
32
+ if (formContext) return formContext;
33
+
34
+ throw new Error(
35
+ `Unable to determine form for ${hookName}. Please use it inside a form or pass a 'formId'.`
36
+ );
37
+ };
38
+
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
+ export function useErrorResponseForForm({
62
+ fetcher,
63
+ subaction,
64
+ formId,
65
+ }: InternalFormContextValue): ValidationErrorResponseData | null {
66
+ const actionData = useActionData<any>();
67
+ if (fetcher) {
68
+ if ((fetcher.data as any)?.fieldErrors) return fetcher.data as any;
69
+ return null;
70
+ }
71
+
72
+ if (!actionData?.fieldErrors) return null;
73
+
74
+ // If there's an explicit id, we should ignore data that has the wrong id
75
+ if (typeof formId === "string" && actionData.formId)
76
+ return actionData.formId === formId ? actionData : null;
77
+
78
+ if (
79
+ (!subaction && !actionData.subaction) ||
80
+ actionData.subaction === subaction
81
+ )
82
+ return actionData;
83
+
84
+ return null;
85
+ }
86
+
87
+ export const useFieldErrorsForForm = (context: InternalFormContextValue) => {
88
+ const response = useErrorResponseForForm(context);
89
+ const hydrated = useContextSelectAtom(context.formId, isHydratedAtom);
90
+ return hydrated ? USE_HYDRATED_STATE : response?.fieldErrors;
91
+ };
92
+
93
+ export const useDefaultValuesFromLoader = ({
94
+ formId,
95
+ }: InternalFormContextValue) => {
96
+ const matches = useMatches();
97
+ if (typeof formId === "string") {
98
+ const dataKey = formDefaultValuesKey(formId);
99
+ // If multiple loaders declare the same default values,
100
+ // we should use the data from the deepest route.
101
+ const match = matches
102
+ .reverse()
103
+ .find((match) => match.data && dataKey in match.data);
104
+ return match?.data[dataKey];
105
+ }
106
+
107
+ return null;
108
+ };
109
+
110
+ export const useDefaultValuesForForm = (context: InternalFormContextValue) => {
111
+ const { formId, defaultValuesProp } = context;
112
+ const hydrated = useContextSelectAtom(formId, isHydratedAtom);
113
+ const errorResponse = useErrorResponseForForm(context);
114
+ const defaultValuesFromLoader = useDefaultValuesFromLoader(context);
115
+
116
+ // Typical flow is:
117
+ // - Default values only available from props or server
118
+ // - Props have a higher priority than server
119
+ // - State gets hydrated with default values
120
+ // - After submit, we may need to use values from the error
121
+
122
+ if (hydrated) return USE_HYDRATED_STATE;
123
+ if (errorResponse?.repopulateFields) return errorResponse.repopulateFields;
124
+ if (defaultValuesProp) return defaultValuesProp;
125
+ return defaultValuesFromLoader;
126
+ };
127
+
128
+ export const useHasActiveFormSubmit = ({
129
+ fetcher,
130
+ }: InternalFormContextValue): boolean => {
131
+ const transition = useTransition();
132
+ const hasActiveSubmission = fetcher
133
+ ? fetcher.state === "submitting"
134
+ : !!transition.submission;
135
+ return hasActiveSubmission;
136
+ };
137
+
138
+ export const useFieldTouched = (
139
+ name: string,
140
+ { formId }: InternalFormContextValue
141
+ ) => {
142
+ const atomCreator = useMemo(() => fieldTouchedAtom(name), [name]);
143
+ return useContextSelectAtom(formId, atomCreator);
144
+ };
145
+
146
+ export const useFieldError = (
147
+ name: string,
148
+ context: InternalFormContextValue
149
+ ) => {
150
+ return useHydratableSelector(
151
+ context,
152
+ useMemo(() => fieldErrorAtom(name), [name]),
153
+ useFieldErrorsForForm(context),
154
+ (fieldErrors) => fieldErrors?.[name]
155
+ );
156
+ };
157
+
158
+ export const useFieldDefaultValue = (
159
+ name: string,
160
+ context: InternalFormContextValue
161
+ ) => {
162
+ return useHydratableSelector(
163
+ context,
164
+ useMemo(() => fieldDefaultValueAtom(name), [name]),
165
+ useDefaultValuesForForm(context),
166
+ (val) => lodashGet(val, name)
167
+ );
168
+ };
169
+
170
+ export const useFormUpdateAtom: typeof useUpdateAtom = (atom) =>
171
+ useUpdateAtom(atom, ATOM_SCOPE);
172
+
173
+ export const useClearError = (context: InternalFormContextValue) => {
174
+ const clearError = useFormUpdateAtom(clearErrorAtom);
175
+ return useCallback(
176
+ (name: string) => {
177
+ clearError({ name, formAtom: formRegistry(context.formId) });
178
+ },
179
+ [clearError, context.formId]
180
+ );
181
+ };
182
+
183
+ export const useSetTouched = (context: InternalFormContextValue) => {
184
+ const setTouched = useFormUpdateAtom(setTouchedAtom);
185
+ return useCallback(
186
+ (name: string, touched: boolean) => {
187
+ setTouched({ name, formAtom: formRegistry(context.formId), touched });
188
+ },
189
+ [setTouched, context.formId]
190
+ );
191
+ };
@@ -0,0 +1,210 @@
1
+ import { atom } from "jotai";
2
+ import { atomWithImmer } from "jotai/immer";
3
+ import { atomFamily, selectAtom } from "jotai/utils";
4
+ import lodashGet from "lodash/get";
5
+ import { FieldErrors, TouchedFields } from "../validation/types";
6
+
7
+ export const ATOM_SCOPE = Symbol("remix-validated-form-scope");
8
+
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
18
+ formId?: string;
19
+ action?: string;
20
+ subaction?: string;
21
+ defaultValues?: { [fieldName: string]: any };
22
+ validateField: (fieldName: string) => Promise<string | null>;
23
+ registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
24
+ };
25
+
26
+ export type FormAtom = ReturnType<typeof formRegistry>;
27
+
28
+ export type FieldState = {
29
+ touched: boolean;
30
+ defaultValue?: any;
31
+ error?: string;
32
+ };
33
+
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
+ })
48
+ );
49
+
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
90
+ );
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
+
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
+ })
107
+ );
108
+
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
+ })
117
+ );
118
+
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
+ })
130
+ );
131
+
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
+ })
143
+ );
144
+
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
+ );
156
+
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
+ }
166
+ );
167
+
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
+ }
176
+ );
177
+
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
+ }
210
+ );
@@ -1,4 +1,5 @@
1
1
  import type React from "react";
2
+ import { useEffect, useLayoutEffect } from "react";
2
3
 
3
4
  export const omit = (obj: any, ...keys: string[]) => {
4
5
  const result = { ...obj };
@@ -21,3 +22,6 @@ export const mergeRefs = <T = any>(
21
22
  });
22
23
  };
23
24
  };
25
+
26
+ export const useIsomorphicLayoutEffect =
27
+ typeof window !== "undefined" ? useLayoutEffect : useEffect;