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
@@ -1,9 +1,15 @@
1
- $ npm run build:browser && npm run build:main
2
-
3
- > remix-validated-form@4.1.9 build:browser
4
- > tsc --module ESNext --outDir ./browser
5
-
6
-
7
- > remix-validated-form@4.1.9 build:main
8
- > tsc --module CommonJS --outDir ./build
9
-
1
+ $ vite build
2
+ vite v2.9.5 building for production...
3
+ transforming...
4
+ ✓ 321 modules transformed.
5
+ rendering chunks...
6
+ dist/remix-validated-form.cjs.js  43.86 KiB / gzip: 16.69 KiB
7
+ dist/remix-validated-form.es.js  98.55 KiB / gzip: 23.34 KiB
8
+ dist/remix-validated-form.umd.js  44.11 KiB / gzip: 16.80 KiB
9
+ 
10
+ [vite:dts] Start generate declaration files...
11
+ [vite:dts] Declaration files built in 1687ms.
12
+ 
13
+ No name was provided for external module 'react' in output.globals – guessing 'React'
14
+ No name was provided for external module '@remix-run/react' in output.globals – guessing 'react'
15
+ No name was provided for external module '@remix-run/server-runtime' in output.globals – guessing 'serverRuntime'
package/README.md CHANGED
@@ -24,6 +24,7 @@ To run `sample-app`:
24
24
  git clone https://github.com/airjp73/remix-validated-form
25
25
  cd ./remix-validated-form
26
26
  yarn install
27
+ yarn build
27
28
  yarn sample-app
28
29
  ```
29
30
 
@@ -2,13 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Form as RemixForm, useSubmit } from "@remix-run/react";
3
3
  import uniq from "lodash/uniq";
4
4
  import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react";
5
- import invariant from "tiny-invariant";
6
5
  import { useIsSubmitting, useIsValid } from "./hooks";
7
6
  import { FORM_ID_FIELD } from "./internal/constants";
8
7
  import { InternalFormContext, } from "./internal/formContext";
9
- import { useDefaultValuesFromLoader, useErrorResponseForForm, useFormUpdateAtom, useHasActiveFormSubmit, } from "./internal/hooks";
8
+ import { useDefaultValuesFromLoader, useErrorResponseForForm, useHasActiveFormSubmit, useSetFieldErrors, } from "./internal/hooks";
10
9
  import { useMultiValueMap } from "./internal/MultiValueMap";
11
- import { cleanupFormState, endSubmitAtom, fieldErrorsAtom, formElementAtom, formPropsAtom, isHydratedAtom, resetAtom, setFieldErrorAtom, startSubmitAtom, } from "./internal/state";
10
+ import { cleanupFormState } from "./internal/state/cleanup";
11
+ import { useControlledFieldStore, useFormStore, } from "./internal/state/storeHooks";
12
12
  import { useSubmitComplete } from "./internal/submissionCallbacks";
13
13
  import { mergeRefs, useDeepEqualsMemo, useIsomorphicLayoutEffect as useLayoutEffect, } from "./internal/util";
14
14
  const getDataFromForm = (el) => new FormData(el);
@@ -111,30 +111,19 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
111
111
  const formRef = useRef(null);
112
112
  const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : RemixForm;
113
113
  const submit = useSubmit();
114
- const setFieldErrors = useFormUpdateAtom(fieldErrorsAtom(formId));
115
- const setFieldError = useFormUpdateAtom(setFieldErrorAtom(formId));
116
- const reset = useFormUpdateAtom(resetAtom(formId));
117
- const startSubmit = useFormUpdateAtom(startSubmitAtom(formId));
118
- const endSubmit = useFormUpdateAtom(endSubmitAtom(formId));
119
- const syncFormProps = useFormUpdateAtom(formPropsAtom(formId));
120
- const setHydrated = useFormUpdateAtom(isHydratedAtom(formId));
121
- const setFormElementInState = useFormUpdateAtom(formElementAtom(formId));
114
+ const setFieldErrors = useSetFieldErrors(formId);
115
+ const setFieldError = useFormStore(formId, (state) => state.setFieldError);
116
+ const reset = useFormStore(formId, (state) => state.reset);
117
+ const resetControlledFields = useControlledFieldStore(formId, (state) => state.reset);
118
+ const startSubmit = useFormStore(formId, (state) => state.startSubmit);
119
+ const endSubmit = useFormStore(formId, (state) => state.endSubmit);
120
+ const syncFormProps = useFormStore(formId, (state) => state.syncFormProps);
121
+ const setHydrated = useFormStore(formId, (state) => state.setHydrated);
122
+ const setFormElementInState = useFormStore(formId, (state) => state.setFormElement);
122
123
  useEffect(() => {
123
- setHydrated(true);
124
+ setHydrated();
124
125
  return () => cleanupFormState(formId);
125
126
  }, [formId, setHydrated]);
126
- const validateField = useCallback(async (field) => {
127
- invariant(formRef.current, "Cannot find reference to form");
128
- const { error } = await validator.validateField(getDataFromForm(formRef.current), field);
129
- if (error) {
130
- setFieldError({ field, error });
131
- return error;
132
- }
133
- else {
134
- setFieldError({ field, error: undefined });
135
- return null;
136
- }
137
- }, [setFieldError, validator]);
138
127
  const customFocusHandlers = useMultiValueMap();
139
128
  const registerReceiveFocus = useCallback((fieldName, handler) => {
140
129
  customFocusHandlers().add(fieldName, handler);
@@ -148,8 +137,8 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
148
137
  action,
149
138
  defaultValues: (_a = providedDefaultValues !== null && providedDefaultValues !== void 0 ? providedDefaultValues : backendDefaultValues) !== null && _a !== void 0 ? _a : {},
150
139
  subaction,
151
- validateField,
152
140
  registerReceiveFocus,
141
+ validator,
153
142
  });
154
143
  }, [
155
144
  action,
@@ -157,8 +146,8 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
157
146
  registerReceiveFocus,
158
147
  subaction,
159
148
  syncFormProps,
160
- validateField,
161
149
  backendDefaultValues,
150
+ validator,
162
151
  ]);
163
152
  useEffect(() => {
164
153
  var _a;
@@ -227,5 +216,6 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
227
216
  if (event.defaultPrevented)
228
217
  return;
229
218
  reset();
219
+ resetControlledFields();
230
220
  }, children: _jsxs(InternalFormContext.Provider, { value: contextValue, children: [_jsx(FormResetter, { formRef: formRef, resetAfterSubmit: resetAfterSubmit }, void 0), subaction && (_jsx("input", { type: "hidden", value: subaction, name: "subaction" }, void 0)), id && _jsx("input", { type: "hidden", value: id, name: FORM_ID_FIELD }, void 0), children] }, void 0) }, void 0));
231
221
  }
@@ -63,3 +63,5 @@ export declare const useField: (name: string, options?: {
63
63
  */
64
64
  formId?: string | undefined;
65
65
  } | undefined) => FieldProps;
66
+ export declare const useControlField: <T>(name: string, formId?: string | undefined) => readonly [T, (value: T) => void];
67
+ export declare const useUpdateControlledField: (formId?: string | undefined) => (fieldName: string, value: unknown) => void;
package/browser/hooks.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useMemo } from "react";
2
2
  import { createGetInputProps, } from "./internal/getInputProps";
3
- import { useInternalFormContext, useFieldTouched, useFieldError, useFormAtomValue, useFieldDefaultValue, } from "./internal/hooks";
4
- import { formPropsAtom, hasBeenSubmittedAtom, isSubmittingAtom, isValidAtom, } from "./internal/state";
3
+ import { useInternalFormContext, useFieldTouched, useFieldError, useFieldDefaultValue, useClearError, useInternalIsSubmitting, useInternalIsValid, useInternalHasBeenSubmitted, useValidateField, useRegisterReceiveFocus, } from "./internal/hooks";
4
+ import { useControllableValue, useUpdateControllableValue, } from "./internal/state/controlledFields";
5
5
  /**
6
6
  * Returns whether or not the parent form is currently being submitted.
7
7
  * This is different from remix's `useTransition().submission` in that it
@@ -11,7 +11,7 @@ import { formPropsAtom, hasBeenSubmittedAtom, isSubmittingAtom, isValidAtom, } f
11
11
  */
12
12
  export const useIsSubmitting = (formId) => {
13
13
  const formContext = useInternalFormContext(formId, "useIsSubmitting");
14
- return useFormAtomValue(isSubmittingAtom(formContext.formId));
14
+ return useInternalIsSubmitting(formContext.formId);
15
15
  };
16
16
  /**
17
17
  * Returns whether or not the current form is valid.
@@ -20,7 +20,7 @@ export const useIsSubmitting = (formId) => {
20
20
  */
21
21
  export const useIsValid = (formId) => {
22
22
  const formContext = useInternalFormContext(formId, "useIsValid");
23
- return useFormAtomValue(isValidAtom(formContext.formId));
23
+ return useInternalIsValid(formContext.formId);
24
24
  };
25
25
  /**
26
26
  * Provides the data and helpers necessary to set up a field.
@@ -30,9 +30,11 @@ export const useField = (name, options) => {
30
30
  const formContext = useInternalFormContext(providedFormId, "useField");
31
31
  const defaultValue = useFieldDefaultValue(name, formContext);
32
32
  const [touched, setTouched] = useFieldTouched(name, formContext);
33
- const [error, setError] = useFieldError(name, formContext);
34
- const hasBeenSubmitted = useFormAtomValue(hasBeenSubmittedAtom(formContext.formId));
35
- const { validateField, registerReceiveFocus } = useFormAtomValue(formPropsAtom(formContext.formId));
33
+ const error = useFieldError(name, formContext);
34
+ const clearError = useClearError(formContext);
35
+ const hasBeenSubmitted = useInternalHasBeenSubmitted(formContext.formId);
36
+ const validateField = useValidateField(formContext.formId);
37
+ const registerReceiveFocus = useRegisterReceiveFocus(formContext.formId);
36
38
  useEffect(() => {
37
39
  if (handleReceiveFocus)
38
40
  return registerReceiveFocus(name, handleReceiveFocus);
@@ -40,7 +42,7 @@ export const useField = (name, options) => {
40
42
  const field = useMemo(() => {
41
43
  const helpers = {
42
44
  error,
43
- clearError: () => setError(undefined),
45
+ clearError: () => clearError(name),
44
46
  validate: () => {
45
47
  validateField(name);
46
48
  },
@@ -60,14 +62,23 @@ export const useField = (name, options) => {
60
62
  };
61
63
  }, [
62
64
  error,
65
+ clearError,
63
66
  defaultValue,
64
67
  touched,
65
68
  setTouched,
66
69
  name,
67
70
  hasBeenSubmitted,
68
71
  options === null || options === void 0 ? void 0 : options.validationBehavior,
69
- setError,
70
72
  validateField,
71
73
  ]);
72
74
  return field;
73
75
  };
76
+ export const useControlField = (name, formId) => {
77
+ const context = useInternalFormContext(formId, "useControlField");
78
+ const [value, setValue] = useControllableValue(context, name);
79
+ return [value, setValue];
80
+ };
81
+ export const useUpdateControlledField = (formId) => {
82
+ const context = useInternalFormContext(formId, "useControlField");
83
+ return useUpdateControllableValue(context.formId);
84
+ };
@@ -1,9 +1,11 @@
1
1
  export declare class MultiValueMap<Key, Value> {
2
2
  private dict;
3
3
  add: (key: Key, value: Value) => void;
4
+ delete: (key: Key) => void;
4
5
  remove: (key: Key, value: Value) => void;
5
6
  getAll: (key: Key) => Value[];
6
7
  entries: () => IterableIterator<[Key, Value[]]>;
8
+ values: () => IterableIterator<Value[]>;
7
9
  has: (key: Key) => boolean;
8
10
  }
9
11
  export declare const useMultiValueMap: <Key, Value>() => () => MultiValueMap<Key, Value>;
@@ -10,6 +10,9 @@ export class MultiValueMap {
10
10
  this.dict.set(key, [value]);
11
11
  }
12
12
  };
13
+ this.delete = (key) => {
14
+ this.dict.delete(key);
15
+ };
13
16
  this.remove = (key, value) => {
14
17
  if (!this.dict.has(key))
15
18
  return;
@@ -25,6 +28,7 @@ export class MultiValueMap {
25
28
  return (_a = this.dict.get(key)) !== null && _a !== void 0 ? _a : [];
26
29
  };
27
30
  this.entries = () => this.dict.entries();
31
+ this.values = () => this.dict.values();
28
32
  this.has = (key) => this.dict.has(key);
29
33
  }
30
34
  }
@@ -42,7 +42,8 @@ export const createGetInputProps = ({ clearError, validate, defaultValue, touche
42
42
  else if (props.type === "radio") {
43
43
  inputProps.defaultChecked = getRadioChecked(props.value, defaultValue);
44
44
  }
45
- else {
45
+ else if (props.value === undefined) {
46
+ // We should only set the defaultValue if the input is uncontrolled.
46
47
  inputProps.defaultValue = defaultValue;
47
48
  }
48
49
  return omitBy(inputProps, (value) => value === undefined);
@@ -1,11 +1,7 @@
1
- import { Atom, WritableAtom } from "jotai";
2
- import { useUpdateAtom } from "jotai/utils";
3
1
  import { FieldErrors, ValidationErrorResponseData } from "..";
4
2
  import { InternalFormContextValue } from "./formContext";
5
3
  import { Hydratable } from "./hydratable";
6
- export declare const useFormUpdateAtom: typeof useUpdateAtom;
7
- export declare const useFormAtom: <Value, Update, Result extends void | Promise<void>>(anAtom: WritableAtom<Value, Update, Result>) => [Value extends Promise<infer V> ? V : Value, import("jotai/core/atom").SetAtom<Update, Result>];
8
- export declare const useFormAtomValue: <Value>(anAtom: Atom<Value>) => Value extends Promise<infer V> ? V : Value;
4
+ import { InternalFormId } from "./state/storeFamily";
9
5
  export declare const useInternalFormContext: (formId?: string | symbol | undefined, hookName?: string | undefined) => InternalFormContextValue;
10
6
  export declare function useErrorResponseForForm({ fetcher, subaction, formId, }: InternalFormContextValue): ValidationErrorResponseData | null;
11
7
  export declare const useFieldErrorsForForm: (context: InternalFormContextValue) => Hydratable<FieldErrors | undefined>;
@@ -14,8 +10,23 @@ export declare const useDefaultValuesForForm: (context: InternalFormContextValue
14
10
  [fieldName: string]: any;
15
11
  }>;
16
12
  export declare const useHasActiveFormSubmit: ({ fetcher, }: InternalFormContextValue) => boolean;
17
- export declare const useFieldTouched: (field: string, { formId }: InternalFormContextValue) => [boolean, (update: boolean) => void];
18
- export declare const useFieldError: (name: string, context: InternalFormContextValue) => readonly [string | undefined, (update?: string | undefined) => void];
13
+ export declare const useFieldTouched: (field: string, { formId }: InternalFormContextValue) => readonly [boolean, (touched: boolean) => void];
14
+ export declare const useFieldError: (name: string, context: InternalFormContextValue) => string | undefined;
15
+ export declare const useClearError: (context: InternalFormContextValue) => (field: string) => void;
19
16
  export declare const useFieldDefaultValue: (name: string, context: InternalFormContextValue) => any;
20
- export declare const useClearError: ({ formId }: InternalFormContextValue) => (name: string) => void;
21
- export declare const useSetTouched: ({ formId }: InternalFormContextValue) => (name: string, touched: boolean) => void;
17
+ export declare const useInternalIsSubmitting: (formId: InternalFormId) => boolean;
18
+ export declare const useInternalIsValid: (formId: InternalFormId) => boolean;
19
+ export declare const useInternalHasBeenSubmitted: (formId: InternalFormId) => boolean;
20
+ export declare const useValidateField: (formId: InternalFormId) => (fieldName: string) => Promise<string | null>;
21
+ export declare const useValidate: (formId: InternalFormId) => () => Promise<void>;
22
+ export declare const useRegisterReceiveFocus: (formId: InternalFormId) => (fieldName: string, handler: () => void) => () => void;
23
+ export declare const useSyncedDefaultValues: (formId: InternalFormId) => {
24
+ [fieldName: string]: any;
25
+ };
26
+ export declare const useSetTouched: ({ formId }: InternalFormContextValue) => (field: string, touched: boolean) => void;
27
+ export declare const useTouchedFields: (formId: InternalFormId) => import("..").TouchedFields;
28
+ export declare const useFieldErrors: (formId: InternalFormId) => FieldErrors;
29
+ export declare const useSetFieldErrors: (formId: InternalFormId) => (errors: FieldErrors) => void;
30
+ export declare const useResetFormElement: (formId: InternalFormId) => () => void;
31
+ export declare const useFormActionProp: (formId: InternalFormId) => string | undefined;
32
+ export declare const useFormSubactionProp: (formId: InternalFormId) => string | undefined;
@@ -1,16 +1,11 @@
1
1
  import { useActionData, useMatches, useTransition } from "@remix-run/react";
2
- import { useAtom } from "jotai";
3
- import { useAtomValue, useUpdateAtom } from "jotai/utils";
4
2
  import lodashGet from "lodash/get";
5
3
  import { useCallback, useContext } from "react";
6
4
  import invariant from "tiny-invariant";
7
5
  import { formDefaultValuesKey } from "./constants";
8
6
  import { InternalFormContext } from "./formContext";
9
7
  import { hydratable } from "./hydratable";
10
- import { ATOM_SCOPE, fieldErrorAtom, fieldTouchedAtom, formPropsAtom, isHydratedAtom, setFieldErrorAtom, setTouchedAtom, } from "./state";
11
- export const useFormUpdateAtom = (atom) => useUpdateAtom(atom, ATOM_SCOPE);
12
- export const useFormAtom = (anAtom) => useAtom(anAtom, ATOM_SCOPE);
13
- export const useFormAtomValue = (anAtom) => useAtomValue(anAtom, ATOM_SCOPE);
8
+ import { useFormStore } from "./state/storeHooks";
14
9
  export const useInternalFormContext = (formId, hookName) => {
15
10
  const formContext = useContext(InternalFormContext);
16
11
  if (formId)
@@ -39,7 +34,7 @@ export function useErrorResponseForForm({ fetcher, subaction, formId, }) {
39
34
  }
40
35
  export const useFieldErrorsForForm = (context) => {
41
36
  const response = useErrorResponseForForm(context);
42
- const hydrated = useFormAtomValue(isHydratedAtom(context.formId));
37
+ const hydrated = useFormStore(context.formId, (state) => state.isHydrated);
43
38
  return hydratable.from(response === null || response === void 0 ? void 0 : response.fieldErrors, hydrated);
44
39
  };
45
40
  export const useDefaultValuesFromLoader = ({ formId, }) => {
@@ -57,7 +52,7 @@ export const useDefaultValuesFromLoader = ({ formId, }) => {
57
52
  };
58
53
  export const useDefaultValuesForForm = (context) => {
59
54
  const { formId, defaultValuesProp } = context;
60
- const hydrated = useFormAtomValue(isHydratedAtom(formId));
55
+ const hydrated = useFormStore(formId, (state) => state.isHydrated);
61
56
  const errorResponse = useErrorResponseForForm(context);
62
57
  const defaultValuesFromLoader = useDefaultValuesFromLoader(context);
63
58
  // Typical flow is:
@@ -82,27 +77,41 @@ export const useHasActiveFormSubmit = ({ fetcher, }) => {
82
77
  : !!transition.submission;
83
78
  return hasActiveSubmission;
84
79
  };
85
- export const useFieldTouched = (field, { formId }) => useFormAtom(fieldTouchedAtom({ formId, field }));
80
+ export const useFieldTouched = (field, { formId }) => {
81
+ const touched = useFormStore(formId, (state) => state.touchedFields[field]);
82
+ const setFieldTouched = useFormStore(formId, (state) => state.setTouched);
83
+ const setTouched = useCallback((touched) => setFieldTouched(field, touched), [field, setFieldTouched]);
84
+ return [touched, setTouched];
85
+ };
86
86
  export const useFieldError = (name, context) => {
87
87
  const fieldErrors = useFieldErrorsForForm(context);
88
- const [state, set] = useFormAtom(fieldErrorAtom({ formId: context.formId, field: name }));
89
- return [
90
- fieldErrors.map((fieldErrors) => fieldErrors === null || fieldErrors === void 0 ? void 0 : fieldErrors[name]).hydrateTo(state),
91
- set,
92
- ];
88
+ const state = useFormStore(context.formId, (state) => state.fieldErrors[name]);
89
+ return fieldErrors.map((fieldErrors) => fieldErrors === null || fieldErrors === void 0 ? void 0 : fieldErrors[name]).hydrateTo(state);
90
+ };
91
+ export const useClearError = (context) => {
92
+ const { formId } = context;
93
+ return useFormStore(formId, (state) => state.clearFieldError);
93
94
  };
94
95
  export const useFieldDefaultValue = (name, context) => {
95
96
  const defaultValues = useDefaultValuesForForm(context);
96
- const { defaultValues: state } = useFormAtomValue(formPropsAtom(context.formId));
97
+ const state = useSyncedDefaultValues(context.formId);
97
98
  return defaultValues
98
99
  .map((val) => lodashGet(val, name))
99
100
  .hydrateTo(lodashGet(state, name));
100
101
  };
101
- export const useClearError = ({ formId }) => {
102
- const updateError = useFormUpdateAtom(setFieldErrorAtom(formId));
103
- return useCallback((name) => updateError({ field: name, error: undefined }), [updateError]);
104
- };
105
- export const useSetTouched = ({ formId }) => {
106
- const setTouched = useFormUpdateAtom(setTouchedAtom(formId));
107
- return useCallback((name, touched) => setTouched({ field: name, touched }), [setTouched]);
108
- };
102
+ export const useInternalIsSubmitting = (formId) => useFormStore(formId, (state) => state.isSubmitting);
103
+ export const useInternalIsValid = (formId) => useFormStore(formId, (state) => state.isValid());
104
+ export const useInternalHasBeenSubmitted = (formId) => useFormStore(formId, (state) => state.hasBeenSubmitted);
105
+ export const useValidateField = (formId) => useFormStore(formId, (state) => state.validateField);
106
+ export const useValidate = (formId) => useFormStore(formId, (state) => state.validate);
107
+ const noOpReceiver = () => () => { };
108
+ export const useRegisterReceiveFocus = (formId) => useFormStore(formId, (state) => { var _a, _b; return (_b = (_a = state.formProps) === null || _a === void 0 ? void 0 : _a.registerReceiveFocus) !== null && _b !== void 0 ? _b : noOpReceiver; });
109
+ const defaultDefaultValues = {};
110
+ export const useSyncedDefaultValues = (formId) => useFormStore(formId, (state) => { var _a, _b; return (_b = (_a = state.formProps) === null || _a === void 0 ? void 0 : _a.defaultValues) !== null && _b !== void 0 ? _b : defaultDefaultValues; });
111
+ export const useSetTouched = ({ formId }) => useFormStore(formId, (state) => state.setTouched);
112
+ export const useTouchedFields = (formId) => useFormStore(formId, (state) => state.touchedFields);
113
+ export const useFieldErrors = (formId) => useFormStore(formId, (state) => state.fieldErrors);
114
+ export const useSetFieldErrors = (formId) => useFormStore(formId, (state) => state.setFieldErrors);
115
+ export const useResetFormElement = (formId) => useFormStore(formId, (state) => state.resetFormElement);
116
+ export const useFormActionProp = (formId) => useFormStore(formId, (state) => { var _a; return (_a = state.formProps) === null || _a === void 0 ? void 0 : _a.action; });
117
+ export const useFormSubactionProp = (formId) => useFormStore(formId, (state) => { var _a; return (_a = state.formProps) === null || _a === void 0 ? void 0 : _a.subaction; });
@@ -3,3 +3,13 @@ export const getRadioChecked = (radioValue = "on", newValue) => {
3
3
  return newValue === radioValue;
4
4
  return undefined;
5
5
  };
6
+ if (import.meta.vitest) {
7
+ const { it, expect } = import.meta.vitest;
8
+ it("getRadioChecked", () => {
9
+ expect(getRadioChecked("on", "on")).toBe(true);
10
+ expect(getRadioChecked("on", undefined)).toBe(undefined);
11
+ expect(getRadioChecked("trueValue", undefined)).toBe(undefined);
12
+ expect(getRadioChecked("trueValue", "bob")).toBe(false);
13
+ expect(getRadioChecked("trueValue", "trueValue")).toBe(true);
14
+ });
15
+ }
@@ -0,0 +1,28 @@
1
+ import { InternalFormId } from "./state/atomUtils";
2
+ export declare const resetAtom: {
3
+ (param: InternalFormId): import("jotai").Atom<null> & {
4
+ write: (get: {
5
+ <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
6
+ <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
7
+ <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
8
+ } & {
9
+ <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
10
+ unstable_promise: true;
11
+ }): Value_3 | Promise<Value_3>;
12
+ <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
13
+ unstable_promise: true;
14
+ }): Value_4 | Promise<Value_4>;
15
+ <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
16
+ unstable_promise: true;
17
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
18
+ }, set: {
19
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
20
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
21
+ }, update: unknown) => void;
22
+ onMount?: (<S extends (update?: unknown) => void>(setAtom: S) => void | (() => void)) | undefined;
23
+ } & {
24
+ init: null;
25
+ };
26
+ remove(param: InternalFormId): void;
27
+ setShouldRemove(shouldRemove: ((createdAt: number, param: InternalFormId) => boolean) | null): void;
28
+ };
@@ -0,0 +1,13 @@
1
+ import { atom } from "jotai";
2
+ import { atomFamily } from "jotai/utils";
3
+ import lodashGet from "lodash/get";
4
+ import { fieldErrorsAtom, formPropsAtom, hasBeenSubmittedAtom, touchedFieldsAtom, } from "./state";
5
+ import { controlledFieldsAtom } from "./state/controlledFields";
6
+ export const resetAtom = atomFamily((formId) => atom(null, (get, set) => {
7
+ set(fieldErrorsAtom(formId), {});
8
+ set(touchedFieldsAtom(formId), {});
9
+ set(hasBeenSubmittedAtom(formId), false);
10
+ const { defaultValues } = get(formPropsAtom(formId));
11
+ const controlledFields = get(controlledFieldsAtom(formId));
12
+ Object.entries(controlledFields).forEach(([name, atom]) => set(atom, lodashGet(defaultValues, name)));
13
+ }));
@@ -0,0 +1,2 @@
1
+ import { InternalFormId } from "./storeFamily";
2
+ export declare const cleanupFormState: (formId: InternalFormId) => void;
@@ -0,0 +1,6 @@
1
+ import { controlledFieldStore } from "./controlledFieldStore";
2
+ import { formStore } from "./createFormStore";
3
+ export const cleanupFormState = (formId) => {
4
+ formStore.remove(formId);
5
+ controlledFieldStore.remove(formId);
6
+ };
@@ -0,0 +1,24 @@
1
+ export declare type ControlledFieldState = {
2
+ fields: {
3
+ [fieldName: string]: {
4
+ refCount: number;
5
+ value: unknown;
6
+ defaultValue?: unknown;
7
+ hydrated: boolean;
8
+ valueUpdatePromise: Promise<void> | undefined;
9
+ resolveValueUpdate: (() => void) | undefined;
10
+ } | undefined;
11
+ };
12
+ register: (fieldName: string) => void;
13
+ unregister: (fieldName: string) => void;
14
+ setValue: (fieldName: string, value: unknown) => void;
15
+ hydrateWithDefault: (fieldName: string, defaultValue: unknown) => void;
16
+ awaitValueUpdate: (fieldName: string) => Promise<void>;
17
+ reset: () => void;
18
+ };
19
+ export declare const controlledFieldStore: {
20
+ (formId: import("./storeFamily").InternalFormId): import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<ControlledFieldState>, "setState"> & {
21
+ setState(nextStateOrUpdater: ControlledFieldState | Partial<ControlledFieldState> | ((state: import("immer/dist/internal").WritableDraft<ControlledFieldState>) => void), shouldReplace?: boolean | undefined): void;
22
+ }>;
23
+ remove(formId: import("./storeFamily").InternalFormId): void;
24
+ };
@@ -0,0 +1,57 @@
1
+ import create from "zustand";
2
+ import { immer } from "zustand/middleware/immer";
3
+ import { storeFamily } from "./storeFamily";
4
+ export const controlledFieldStore = storeFamily(() => create()(immer((set, get, api) => ({
5
+ fields: {},
6
+ register: (field) => set((state) => {
7
+ if (state.fields[field]) {
8
+ state.fields[field].refCount++;
9
+ }
10
+ else {
11
+ state.fields[field] = {
12
+ refCount: 1,
13
+ value: undefined,
14
+ hydrated: false,
15
+ valueUpdatePromise: undefined,
16
+ resolveValueUpdate: undefined,
17
+ };
18
+ }
19
+ }),
20
+ unregister: (field) => set((state) => {
21
+ const fieldState = state.fields[field];
22
+ if (!fieldState)
23
+ return;
24
+ fieldState.refCount--;
25
+ if (fieldState.refCount === 0)
26
+ delete state.fields[field];
27
+ }),
28
+ setValue: (field, value) => set((state) => {
29
+ const fieldState = state.fields[field];
30
+ if (!fieldState)
31
+ return;
32
+ fieldState.value = value;
33
+ const promise = new Promise((resolve) => {
34
+ fieldState.resolveValueUpdate = resolve;
35
+ });
36
+ fieldState.valueUpdatePromise = promise;
37
+ }),
38
+ hydrateWithDefault: (field, defaultValue) => set((state) => {
39
+ const fieldState = state.fields[field];
40
+ if (!fieldState)
41
+ return;
42
+ fieldState.value = defaultValue;
43
+ fieldState.defaultValue = defaultValue;
44
+ fieldState.hydrated = true;
45
+ }),
46
+ awaitValueUpdate: async (field) => {
47
+ var _a;
48
+ await ((_a = get().fields[field]) === null || _a === void 0 ? void 0 : _a.valueUpdatePromise);
49
+ },
50
+ reset: () => set((state) => {
51
+ Object.values(state.fields).forEach((field) => {
52
+ if (!field)
53
+ return;
54
+ field.value = field.defaultValue;
55
+ });
56
+ }),
57
+ }))));
@@ -1,62 +1,6 @@
1
- import { PrimitiveAtom } from "jotai";
2
- import { InternalFormId } from "./atomUtils";
3
- export declare const controlledFieldsAtom: {
4
- (param: InternalFormId): import("jotai").Atom<Record<string, PrimitiveAtom<unknown>>> & {
5
- write: (get: {
6
- <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
7
- <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
8
- <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
9
- } & {
10
- <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
11
- unstable_promise: true;
12
- }): Value_3 | Promise<Value_3>;
13
- <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
14
- unstable_promise: true;
15
- }): Value_4 | Promise<Value_4>;
16
- <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
17
- unstable_promise: true;
18
- }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
19
- }, set: {
20
- <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
21
- <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
22
- }, update: Record<string, PrimitiveAtom<unknown>> | ((prev: Record<string, PrimitiveAtom<unknown>>) => Record<string, PrimitiveAtom<unknown>>)) => void;
23
- onMount?: (<S extends (update: Record<string, PrimitiveAtom<unknown>> | ((prev: Record<string, PrimitiveAtom<unknown>>) => Record<string, PrimitiveAtom<unknown>>)) => void>(setAtom: S) => void | (() => void)) | undefined;
24
- } & {
25
- init: Record<string, PrimitiveAtom<unknown>>;
26
- };
27
- remove(param: InternalFormId): void;
28
- setShouldRemove(shouldRemove: ((createdAt: number, param: InternalFormId) => boolean) | null): void;
29
- };
30
- export declare const setControlledFieldValueAtom: import("jotai").Atom<null> & {
31
- write: (get: {
32
- <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
33
- <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
34
- <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
35
- } & {
36
- <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
37
- unstable_promise: true;
38
- }): Value_3 | Promise<Value_3>;
39
- <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
40
- unstable_promise: true;
41
- }): Value_4 | Promise<Value_4>;
42
- <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
43
- unstable_promise: true;
44
- }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
45
- }, set: {
46
- <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
47
- <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
48
- }, update: {
49
- formId: InternalFormId;
50
- field: string;
51
- value: unknown;
52
- }) => Promise<void>;
53
- onMount?: (<S extends (update: {
54
- formId: InternalFormId;
55
- field: string;
56
- value: unknown;
57
- }) => Promise<void>>(setAtom: S) => void | (() => void)) | undefined;
58
- } & {
59
- init: null;
60
- };
61
- export declare const useControlledFieldValue: (formId: InternalFormId, field: string) => any;
62
- export declare const useControllableValue: (formId: InternalFormId, field: string) => readonly [any, (value: unknown) => Promise<void>];
1
+ import { InternalFormContextValue } from "../formContext";
2
+ import { InternalFormId } from "./storeFamily";
3
+ export declare const useControlledFieldValue: (context: InternalFormContextValue, field: string) => any;
4
+ export declare const useControllableValue: (context: InternalFormContextValue, field: string) => readonly [any, (value: unknown) => void];
5
+ export declare const useUpdateControllableValue: (formId: InternalFormId) => (fieldName: string, value: unknown) => void;
6
+ export declare const useAwaitValue: (formId: InternalFormId) => (fieldName: string) => Promise<void>;