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
package/src/hooks.ts CHANGED
@@ -8,17 +8,14 @@ import {
8
8
  useInternalFormContext,
9
9
  useFieldTouched,
10
10
  useFieldError,
11
+ useFormAtomValue,
11
12
  useFieldDefaultValue,
12
- useContextSelectAtom,
13
- useClearError,
14
- useSetTouched,
15
13
  } from "./internal/hooks";
16
14
  import {
15
+ formPropsAtom,
17
16
  hasBeenSubmittedAtom,
18
17
  isSubmittingAtom,
19
18
  isValidAtom,
20
- registerReceiveFocusAtom,
21
- validateFieldAtom,
22
19
  } from "./internal/state";
23
20
 
24
21
  /**
@@ -30,7 +27,7 @@ import {
30
27
  */
31
28
  export const useIsSubmitting = (formId?: string) => {
32
29
  const formContext = useInternalFormContext(formId, "useIsSubmitting");
33
- return useContextSelectAtom(formContext.formId, isSubmittingAtom);
30
+ return useFormAtomValue(isSubmittingAtom(formContext.formId));
34
31
  };
35
32
 
36
33
  /**
@@ -40,7 +37,7 @@ export const useIsSubmitting = (formId?: string) => {
40
37
  */
41
38
  export const useIsValid = (formId?: string) => {
42
39
  const formContext = useInternalFormContext(formId, "useIsValid");
43
- return useContextSelectAtom(formContext.formId, isValidAtom);
40
+ return useFormAtomValue(isValidAtom(formContext.formId));
44
41
  };
45
42
 
46
43
  export type FieldProps = {
@@ -97,26 +94,18 @@ export const useField = (
97
94
  formId?: string;
98
95
  }
99
96
  ): FieldProps => {
100
- const { handleReceiveFocus, formId: providedFormId } = options ?? {};
97
+ const { formId: providedFormId, handleReceiveFocus } = options ?? {};
101
98
  const formContext = useInternalFormContext(providedFormId, "useField");
102
99
 
103
100
  const defaultValue = useFieldDefaultValue(name, formContext);
104
- const touched = useFieldTouched(name, formContext);
105
- const error = useFieldError(name, formContext);
101
+ const [touched, setTouched] = useFieldTouched(name, formContext);
102
+ const [error, setError] = useFieldError(name, formContext);
106
103
 
107
- const clearError = useClearError(formContext);
108
- const setTouched = useSetTouched(formContext);
109
- const hasBeenSubmitted = useContextSelectAtom(
110
- formContext.formId,
111
- hasBeenSubmittedAtom
104
+ const hasBeenSubmitted = useFormAtomValue(
105
+ hasBeenSubmittedAtom(formContext.formId)
112
106
  );
113
- const validateField = useContextSelectAtom(
114
- formContext.formId,
115
- validateFieldAtom
116
- );
117
- const registerReceiveFocus = useContextSelectAtom(
118
- formContext.formId,
119
- registerReceiveFocusAtom
107
+ const { validateField, registerReceiveFocus } = useFormAtomValue(
108
+ formPropsAtom(formContext.formId)
120
109
  );
121
110
 
122
111
  useEffect(() => {
@@ -127,13 +116,13 @@ export const useField = (
127
116
  const field = useMemo<FieldProps>(() => {
128
117
  const helpers = {
129
118
  error,
130
- clearError: () => clearError(name),
119
+ clearError: () => setError(undefined),
131
120
  validate: () => {
132
121
  validateField(name);
133
122
  },
134
123
  defaultValue,
135
124
  touched,
136
- setTouched: (touched: boolean) => setTouched(name, touched),
125
+ setTouched,
137
126
  };
138
127
  const getInputProps = createGetInputProps({
139
128
  ...helpers,
@@ -149,12 +138,12 @@ export const useField = (
149
138
  error,
150
139
  defaultValue,
151
140
  touched,
141
+ setTouched,
152
142
  name,
153
143
  hasBeenSubmitted,
154
144
  options?.validationBehavior,
155
- clearError,
145
+ setError,
156
146
  validateField,
157
- setTouched,
158
147
  ]);
159
148
 
160
149
  return field;
@@ -1,4 +1,6 @@
1
1
  import omitBy from "lodash/omitBy";
2
+ import { getCheckboxChecked } from "./logic/getCheckboxChecked";
3
+ import { getRadioChecked } from "./logic/getRadioChecked";
2
4
 
3
5
  export type ValidationBehavior = "onBlur" | "onChange" | "onSubmit";
4
6
 
@@ -41,13 +43,6 @@ const defaultValidationBehavior: ValidationBehaviorOptions = {
41
43
  whenSubmitted: "onChange",
42
44
  };
43
45
 
44
- const getCheckboxDefaultChecked = (value: string, defaultValue: any) => {
45
- if (Array.isArray(defaultValue)) return defaultValue.includes(value);
46
- if (typeof defaultValue === "boolean") return defaultValue;
47
- if (typeof defaultValue === "string") return defaultValue === value;
48
- return undefined;
49
- };
50
-
51
46
  export const createGetInputProps = ({
52
47
  clearError,
53
48
  validate,
@@ -86,14 +81,9 @@ export const createGetInputProps = ({
86
81
  };
87
82
 
88
83
  if (props.type === "checkbox") {
89
- const value = props.value ?? "on";
90
- inputProps.defaultChecked = getCheckboxDefaultChecked(
91
- value,
92
- defaultValue
93
- );
84
+ inputProps.defaultChecked = getCheckboxChecked(props.value, defaultValue);
94
85
  } else if (props.type === "radio") {
95
- const value = props.value ?? "on";
96
- inputProps.defaultChecked = defaultValue === value;
86
+ inputProps.defaultChecked = getRadioChecked(props.value, defaultValue);
97
87
  } else {
98
88
  inputProps.defaultValue = defaultValue;
99
89
  }
@@ -1,26 +1,32 @@
1
1
  import { useActionData, useMatches, useTransition } from "@remix-run/react";
2
- import { Atom } from "jotai";
2
+ import { Atom, useAtom, WritableAtom } from "jotai";
3
3
  import { useAtomValue, useUpdateAtom } from "jotai/utils";
4
4
  import lodashGet from "lodash/get";
5
- import identity from "lodash/identity";
6
- import { useCallback, useContext, useMemo } from "react";
7
- import { ValidationErrorResponseData } from "..";
5
+ import { useCallback, useContext } from "react";
6
+ import invariant from "tiny-invariant";
7
+ import { FieldErrors, ValidationErrorResponseData } from "..";
8
8
  import { formDefaultValuesKey } from "./constants";
9
9
  import { InternalFormContext, InternalFormContextValue } from "./formContext";
10
+ import { Hydratable, hydratable } from "./hydratable";
10
11
  import {
11
12
  ATOM_SCOPE,
12
- clearErrorAtom,
13
- fieldDefaultValueAtom,
14
13
  fieldErrorAtom,
15
14
  fieldTouchedAtom,
16
- FormAtom,
17
- formRegistry,
15
+ formPropsAtom,
18
16
  isHydratedAtom,
17
+ setFieldErrorAtom,
19
18
  setTouchedAtom,
20
19
  } from "./state";
21
20
 
22
- type FormSelectorAtomCreator<T> = (formState: FormAtom) => Atom<T>;
23
- const USE_HYDRATED_STATE = Symbol("USE_HYDRATED_STATE");
21
+ export const useFormUpdateAtom: typeof useUpdateAtom = (atom) =>
22
+ useUpdateAtom(atom, ATOM_SCOPE);
23
+
24
+ export const useFormAtom = <Value, Update, Result extends void | Promise<void>>(
25
+ anAtom: WritableAtom<Value, Update, Result>
26
+ ) => useAtom(anAtom, ATOM_SCOPE);
27
+
28
+ export const useFormAtomValue = <Value>(anAtom: Atom<Value>) =>
29
+ useAtomValue(anAtom, ATOM_SCOPE);
24
30
 
25
31
  export const useInternalFormContext = (
26
32
  formId?: string | symbol,
@@ -32,32 +38,10 @@ export const useInternalFormContext = (
32
38
  if (formContext) return formContext;
33
39
 
34
40
  throw new Error(
35
- `Unable to determine form for ${hookName}. Please use it inside a form or pass a 'formId'.`
41
+ `Unable to determine form for ${hookName}. Please use it inside a ValidatedForm or pass a 'formId'.`
36
42
  );
37
43
  };
38
44
 
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
45
  export function useErrorResponseForForm({
62
46
  fetcher,
63
47
  subaction,
@@ -84,10 +68,12 @@ export function useErrorResponseForForm({
84
68
  return null;
85
69
  }
86
70
 
87
- export const useFieldErrorsForForm = (context: InternalFormContextValue) => {
71
+ export const useFieldErrorsForForm = (
72
+ context: InternalFormContextValue
73
+ ): Hydratable<FieldErrors | undefined> => {
88
74
  const response = useErrorResponseForForm(context);
89
- const hydrated = useContextSelectAtom(context.formId, isHydratedAtom);
90
- return hydrated ? USE_HYDRATED_STATE : response?.fieldErrors;
75
+ const hydrated = useFormAtomValue(isHydratedAtom(context.formId));
76
+ return hydratable.from(response?.fieldErrors, hydrated);
91
77
  };
92
78
 
93
79
  export const useDefaultValuesFromLoader = ({
@@ -107,9 +93,11 @@ export const useDefaultValuesFromLoader = ({
107
93
  return null;
108
94
  };
109
95
 
110
- export const useDefaultValuesForForm = (context: InternalFormContextValue) => {
96
+ export const useDefaultValuesForForm = (
97
+ context: InternalFormContextValue
98
+ ): Hydratable<{ [fieldName: string]: any }> => {
111
99
  const { formId, defaultValuesProp } = context;
112
- const hydrated = useContextSelectAtom(formId, isHydratedAtom);
100
+ const hydrated = useFormAtomValue(isHydratedAtom(formId));
113
101
  const errorResponse = useErrorResponseForForm(context);
114
102
  const defaultValuesFromLoader = useDefaultValuesFromLoader(context);
115
103
 
@@ -119,10 +107,17 @@ export const useDefaultValuesForForm = (context: InternalFormContextValue) => {
119
107
  // - State gets hydrated with default values
120
108
  // - After submit, we may need to use values from the error
121
109
 
122
- if (hydrated) return USE_HYDRATED_STATE;
123
- if (errorResponse?.repopulateFields) return errorResponse.repopulateFields;
124
- if (defaultValuesProp) return defaultValuesProp;
125
- return defaultValuesFromLoader;
110
+ if (hydrated) return hydratable.hydratedData();
111
+ if (errorResponse?.repopulateFields) {
112
+ invariant(
113
+ typeof errorResponse.repopulateFields === "object",
114
+ "repopulateFields returned something other than an object"
115
+ );
116
+ return hydratable.serverData(errorResponse.repopulateFields);
117
+ }
118
+ if (defaultValuesProp) return hydratable.serverData(defaultValuesProp);
119
+
120
+ return hydratable.serverData(defaultValuesFromLoader);
126
121
  };
127
122
 
128
123
  export const useHasActiveFormSubmit = ({
@@ -136,56 +131,49 @@ export const useHasActiveFormSubmit = ({
136
131
  };
137
132
 
138
133
  export const useFieldTouched = (
139
- name: string,
134
+ field: string,
140
135
  { formId }: InternalFormContextValue
141
- ) => {
142
- const atomCreator = useMemo(() => fieldTouchedAtom(name), [name]);
143
- return useContextSelectAtom(formId, atomCreator);
144
- };
136
+ ) => useFormAtom(fieldTouchedAtom({ formId, field }));
145
137
 
146
138
  export const useFieldError = (
147
139
  name: string,
148
140
  context: InternalFormContextValue
149
141
  ) => {
150
- return useHydratableSelector(
151
- context,
152
- useMemo(() => fieldErrorAtom(name), [name]),
153
- useFieldErrorsForForm(context),
154
- (fieldErrors) => fieldErrors?.[name]
142
+ const fieldErrors = useFieldErrorsForForm(context);
143
+ const [state, set] = useFormAtom(
144
+ fieldErrorAtom({ formId: context.formId, field: name })
155
145
  );
146
+ return [
147
+ fieldErrors.map((fieldErrors) => fieldErrors?.[name]).hydrateTo(state),
148
+ set,
149
+ ] as const;
156
150
  };
157
151
 
158
152
  export const useFieldDefaultValue = (
159
153
  name: string,
160
154
  context: InternalFormContextValue
161
155
  ) => {
162
- return useHydratableSelector(
163
- context,
164
- useMemo(() => fieldDefaultValueAtom(name), [name]),
165
- useDefaultValuesForForm(context),
166
- (val) => lodashGet(val, name)
156
+ const defaultValues = useDefaultValuesForForm(context);
157
+ const { defaultValues: state } = useFormAtomValue(
158
+ formPropsAtom(context.formId)
167
159
  );
160
+ return defaultValues
161
+ .map((val) => lodashGet(val, name))
162
+ .hydrateTo(state[name]);
168
163
  };
169
164
 
170
- export const useFormUpdateAtom: typeof useUpdateAtom = (atom) =>
171
- useUpdateAtom(atom, ATOM_SCOPE);
172
-
173
- export const useClearError = (context: InternalFormContextValue) => {
174
- const clearError = useFormUpdateAtom(clearErrorAtom);
165
+ export const useClearError = ({ formId }: InternalFormContextValue) => {
166
+ const updateError = useFormUpdateAtom(setFieldErrorAtom(formId));
175
167
  return useCallback(
176
- (name: string) => {
177
- clearError({ name, formAtom: formRegistry(context.formId) });
178
- },
179
- [clearError, context.formId]
168
+ (name: string) => updateError({ field: name, error: undefined }),
169
+ [updateError]
180
170
  );
181
171
  };
182
172
 
183
- export const useSetTouched = (context: InternalFormContextValue) => {
184
- const setTouched = useFormUpdateAtom(setTouchedAtom);
173
+ export const useSetTouched = ({ formId }: InternalFormContextValue) => {
174
+ const setTouched = useFormUpdateAtom(setTouchedAtom(formId));
185
175
  return useCallback(
186
- (name: string, touched: boolean) => {
187
- setTouched({ name, formAtom: formRegistry(context.formId), touched });
188
- },
189
- [setTouched, context.formId]
176
+ (name: string, touched: boolean) => setTouched({ field: name, touched }),
177
+ [setTouched]
190
178
  );
191
179
  };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The purpose of this type is to simplify the logic
3
+ * around data that needs to come from the server initially,
4
+ * but from the internal state after hydration.
5
+ */
6
+ export type Hydratable<T> = {
7
+ hydrateTo: (data: T) => T;
8
+ map: <U>(fn: (data: T) => U) => Hydratable<U>;
9
+ };
10
+
11
+ const serverData = <T>(data: T): Hydratable<T> => ({
12
+ hydrateTo: () => data,
13
+ map: (fn) => serverData(fn(data)),
14
+ });
15
+
16
+ const hydratedData = <T>(): Hydratable<T> => ({
17
+ hydrateTo: (hydratedData: T) => hydratedData,
18
+ map: <U>() => hydratedData<U>(),
19
+ });
20
+
21
+ const from = <T>(data: T, hydrated: boolean): Hydratable<T> =>
22
+ hydrated ? hydratedData<T>() : serverData<T>(data);
23
+
24
+ export const hydratable = {
25
+ serverData,
26
+ hydratedData,
27
+ from,
28
+ };
@@ -0,0 +1,10 @@
1
+ export const getCheckboxChecked = (
2
+ checkboxValue: string | undefined = "on",
3
+ newValue: unknown
4
+ ): boolean | undefined => {
5
+ if (Array.isArray(newValue))
6
+ return newValue.some((val) => val === true || val === checkboxValue);
7
+ if (typeof newValue === "boolean") return newValue;
8
+ if (typeof newValue === "string") return newValue === checkboxValue;
9
+ return undefined;
10
+ };
@@ -0,0 +1,7 @@
1
+ export const getRadioChecked = (
2
+ radioValue: string | undefined = "on",
3
+ newValue: unknown
4
+ ) => {
5
+ if (typeof newValue === "string") return newValue === radioValue;
6
+ return undefined;
7
+ };
@@ -0,0 +1,13 @@
1
+ import { Atom, atom } from "jotai";
2
+ import { atomFamily } from "jotai/utils";
3
+ import isEqual from "lodash/isEqual";
4
+
5
+ export type InternalFormId = string | symbol;
6
+
7
+ export const formAtomFamily = <T>(data: T) =>
8
+ atomFamily((_: InternalFormId) => atom(data));
9
+
10
+ export type FieldAtomKey = { formId: InternalFormId; field: string };
11
+ export const fieldAtomFamily = <T extends Atom<unknown>>(
12
+ func: (key: FieldAtomKey) => T
13
+ ) => atomFamily(func, isEqual);