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

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 +1 -1
  4. package/browser/ValidatedForm.js +99 -140
  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 +36 -40
  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 +23 -0
  17. package/browser/internal/hooks.js +114 -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 +1 -1
  31. package/build/ValidatedForm.js +95 -136
  32. package/build/hooks.d.ts +19 -14
  33. package/build/hooks.js +38 -46
  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 +23 -0
  42. package/build/internal/hooks.js +135 -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 +150 -181
  55. package/src/hooks.ts +69 -55
  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 +200 -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/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,200 @@
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 useUnknownFormContextSelectAtom = <T>(
52
+ formId: string | symbol | undefined,
53
+ selectorAtomCreator: FormSelectorAtomCreator<T>,
54
+ hookName: string
55
+ ) => {
56
+ const formContext = useInternalFormContext(formId, hookName);
57
+ return useContextSelectAtom(formContext.formId, selectorAtomCreator);
58
+ };
59
+
60
+ export const useHydratableSelector = <T, U>(
61
+ { formId }: InternalFormContextValue,
62
+ atomCreator: FormSelectorAtomCreator<T>,
63
+ dataToUse: U | typeof USE_HYDRATED_STATE,
64
+ selector: (data: U) => T = identity
65
+ ) => {
66
+ const dataFromState = useContextSelectAtom(formId, atomCreator);
67
+ return dataToUse === USE_HYDRATED_STATE ? dataFromState : selector(dataToUse);
68
+ };
69
+
70
+ export function useErrorResponseForForm({
71
+ fetcher,
72
+ subaction,
73
+ formId,
74
+ }: InternalFormContextValue): ValidationErrorResponseData | null {
75
+ const actionData = useActionData<any>();
76
+ if (fetcher) {
77
+ if ((fetcher.data as any)?.fieldErrors) return fetcher.data as any;
78
+ return null;
79
+ }
80
+
81
+ if (!actionData?.fieldErrors) return null;
82
+
83
+ // If there's an explicit id, we should ignore data that has the wrong id
84
+ if (typeof formId === "string" && actionData.formId)
85
+ return actionData.formId === formId ? actionData : null;
86
+
87
+ if (
88
+ (!subaction && !actionData.subaction) ||
89
+ actionData.subaction === subaction
90
+ )
91
+ return actionData;
92
+
93
+ return null;
94
+ }
95
+
96
+ export const useFieldErrorsForForm = (context: InternalFormContextValue) => {
97
+ const response = useErrorResponseForForm(context);
98
+ const hydrated = useContextSelectAtom(context.formId, isHydratedAtom);
99
+ return hydrated ? USE_HYDRATED_STATE : response?.fieldErrors;
100
+ };
101
+
102
+ export const useDefaultValuesFromLoader = ({
103
+ formId,
104
+ }: InternalFormContextValue) => {
105
+ const matches = useMatches();
106
+ if (typeof formId === "string") {
107
+ const dataKey = formDefaultValuesKey(formId);
108
+ // If multiple loaders declare the same default values,
109
+ // we should use the data from the deepest route.
110
+ const match = matches
111
+ .reverse()
112
+ .find((match) => match.data && dataKey in match.data);
113
+ return match?.data[dataKey];
114
+ }
115
+
116
+ return null;
117
+ };
118
+
119
+ export const useDefaultValuesForForm = (context: InternalFormContextValue) => {
120
+ const { formId, defaultValuesProp } = context;
121
+ const hydrated = useContextSelectAtom(formId, isHydratedAtom);
122
+ const errorResponse = useErrorResponseForForm(context);
123
+ const defaultValuesFromLoader = useDefaultValuesFromLoader(context);
124
+
125
+ // Typical flow is:
126
+ // - Default values only available from props or server
127
+ // - Props have a higher priority than server
128
+ // - State gets hydrated with default values
129
+ // - After submit, we may need to use values from the error
130
+
131
+ if (hydrated) return USE_HYDRATED_STATE;
132
+ if (errorResponse?.repopulateFields) return errorResponse.repopulateFields;
133
+ if (defaultValuesProp) return defaultValuesProp;
134
+ return defaultValuesFromLoader;
135
+ };
136
+
137
+ export const useHasActiveFormSubmit = ({
138
+ fetcher,
139
+ }: InternalFormContextValue): boolean => {
140
+ const transition = useTransition();
141
+ const hasActiveSubmission = fetcher
142
+ ? fetcher.state === "submitting"
143
+ : !!transition.submission;
144
+ return hasActiveSubmission;
145
+ };
146
+
147
+ export const useFieldTouched = (
148
+ name: string,
149
+ { formId }: InternalFormContextValue
150
+ ) => {
151
+ const atomCreator = useMemo(() => fieldTouchedAtom(name), [name]);
152
+ return useContextSelectAtom(formId, atomCreator);
153
+ };
154
+
155
+ export const useFieldError = (
156
+ name: string,
157
+ context: InternalFormContextValue
158
+ ) => {
159
+ return useHydratableSelector(
160
+ context,
161
+ useMemo(() => fieldErrorAtom(name), [name]),
162
+ useFieldErrorsForForm(context),
163
+ (fieldErrors) => fieldErrors?.[name]
164
+ );
165
+ };
166
+
167
+ export const useFieldDefaultValue = (
168
+ name: string,
169
+ context: InternalFormContextValue
170
+ ) => {
171
+ return useHydratableSelector(
172
+ context,
173
+ useMemo(() => fieldDefaultValueAtom(name), [name]),
174
+ useDefaultValuesForForm(context),
175
+ (val) => lodashGet(val, name)
176
+ );
177
+ };
178
+
179
+ export const useFormUpdateAtom: typeof useUpdateAtom = (atom) =>
180
+ useUpdateAtom(atom, ATOM_SCOPE);
181
+
182
+ export const useClearError = (context: InternalFormContextValue) => {
183
+ const clearError = useFormUpdateAtom(clearErrorAtom);
184
+ return useCallback(
185
+ (name: string) => {
186
+ clearError({ name, formAtom: formRegistry(context.formId) });
187
+ },
188
+ [clearError, context.formId]
189
+ );
190
+ };
191
+
192
+ export const useSetTouched = (context: InternalFormContextValue) => {
193
+ const setTouched = useFormUpdateAtom(setTouchedAtom);
194
+ return useCallback(
195
+ (name: string, touched: boolean) => {
196
+ setTouched({ name, formAtom: formRegistry(context.formId), touched });
197
+ },
198
+ [setTouched, context.formId]
199
+ );
200
+ };
@@ -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;
package/src/server.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import { json } from "@remix-run/server-runtime";
2
+ import {
3
+ formDefaultValuesKey,
4
+ FORM_DEFAULTS_FIELD,
5
+ } from "./internal/constants";
2
6
  import {
3
7
  ValidatorError,
4
8
  ValidationErrorResponseData,
@@ -27,7 +31,19 @@ export function validationError(
27
31
  fieldErrors: error.fieldErrors,
28
32
  subaction: error.subaction,
29
33
  repopulateFields,
34
+ formId: error.formId,
30
35
  },
31
36
  { status: 422 }
32
37
  );
33
38
  }
39
+
40
+ export type FormDefaults = {
41
+ [formDefaultsKey: `${typeof FORM_DEFAULTS_FIELD}_${string}`]: any;
42
+ };
43
+
44
+ export const setFormDefaults = <DataType = any>(
45
+ formId: string,
46
+ defaultValues: Partial<DataType>
47
+ ): FormDefaults => ({
48
+ [formDefaultValuesKey(formId)]: defaultValues,
49
+ });
@@ -0,0 +1,129 @@
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";
21
+ import { FieldErrors, TouchedFields } from "./validation/types";
22
+
23
+ export type FormContextValue = {
24
+ /**
25
+ * All the errors in all the fields in the form.
26
+ */
27
+ fieldErrors: FieldErrors;
28
+ /**
29
+ * Clear the errors of the specified fields.
30
+ */
31
+ clearError: (...names: string[]) => void;
32
+ /**
33
+ * Validate the specified field.
34
+ */
35
+ validateField: (fieldName: string) => Promise<string | null>;
36
+ /**
37
+ * The `action` prop of the form.
38
+ */
39
+ action?: string;
40
+ /**
41
+ * Whether or not the form is submitting.
42
+ */
43
+ isSubmitting: boolean;
44
+ /**
45
+ * Whether or not a submission has been attempted.
46
+ * This is true once the form has been submitted, even if there were validation errors.
47
+ * Resets to false when the form is reset.
48
+ */
49
+ hasBeenSubmitted: boolean;
50
+ /**
51
+ * Whether or not the form is valid.
52
+ */
53
+ isValid: boolean;
54
+ /**
55
+ * The default values of the form.
56
+ */
57
+ defaultValues?: { [fieldName: string]: any };
58
+ /**
59
+ * Register a custom focus handler to be used when
60
+ * the field needs to receive focus due to a validation error.
61
+ */
62
+ registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
63
+ /**
64
+ * Any fields that have been touched by the user.
65
+ */
66
+ touchedFields: TouchedFields;
67
+ /**
68
+ * Change the touched state of the specified field.
69
+ */
70
+ setFieldTouched: (fieldName: string, touched: boolean) => void;
71
+ };
72
+
73
+ /**
74
+ * Provides access to some of the internal state of the form.
75
+ */
76
+ export const useFormContext = (formId?: string): FormContextValue => {
77
+ // Try to access context so we get our error specific to this hook if it's not there
78
+ const context = useInternalFormContext(formId, "useFormContext");
79
+
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
104
+ );
105
+
106
+ const internalClearError = useClearError(context);
107
+ const clearError = useCallback(
108
+ (...names: string[]) => {
109
+ names.forEach((name) => {
110
+ internalClearError(name);
111
+ });
112
+ },
113
+ [internalClearError]
114
+ );
115
+
116
+ return {
117
+ isSubmitting,
118
+ hasBeenSubmitted,
119
+ isValid,
120
+ defaultValues,
121
+ clearError,
122
+ fieldErrors: fieldErrors ?? {},
123
+ action,
124
+ setFieldTouched,
125
+ touchedFields,
126
+ validateField,
127
+ registerReceiveFocus,
128
+ };
129
+ };