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
@@ -0,0 +1,251 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import {
3
+ createGetInputProps,
4
+ CreateGetInputPropsOptions,
5
+ } from "./getInputProps";
6
+
7
+ const fakeEvent = { fake: "event" } as any;
8
+
9
+ describe("getInputProps", () => {
10
+ describe("initial", () => {
11
+ it("should validate on blur by default", () => {
12
+ const options: CreateGetInputPropsOptions = {
13
+ name: "some-field",
14
+ defaultValue: "test default value",
15
+ touched: false,
16
+ hasBeenSubmitted: false,
17
+ setTouched: vi.fn(),
18
+ clearError: vi.fn(),
19
+ validate: vi.fn(),
20
+ };
21
+ const getInputProps = createGetInputProps(options);
22
+
23
+ const provided = {
24
+ onBlur: vi.fn(),
25
+ onChange: vi.fn(),
26
+ };
27
+ const { onChange, onBlur } = getInputProps(provided);
28
+
29
+ onChange!(fakeEvent);
30
+ expect(provided.onChange).toBeCalledTimes(1);
31
+ expect(provided.onChange).toBeCalledWith(fakeEvent);
32
+ expect(options.setTouched).not.toBeCalled();
33
+ expect(options.validate).not.toBeCalled();
34
+
35
+ onBlur!(fakeEvent);
36
+ expect(provided.onBlur).toBeCalledTimes(1);
37
+ expect(provided.onBlur).toBeCalledWith(fakeEvent);
38
+ expect(options.setTouched).toBeCalledTimes(1);
39
+ expect(options.setTouched).toBeCalledWith(true);
40
+ expect(options.validate).toBeCalledTimes(1);
41
+ });
42
+
43
+ it("should respect provided validation behavior", () => {
44
+ const options: CreateGetInputPropsOptions = {
45
+ name: "some-field",
46
+ defaultValue: "test default value",
47
+ touched: false,
48
+ hasBeenSubmitted: false,
49
+ setTouched: vi.fn(),
50
+ clearError: vi.fn(),
51
+ validate: vi.fn(),
52
+ validationBehavior: {
53
+ initial: "onChange",
54
+ },
55
+ };
56
+ const getInputProps = createGetInputProps(options);
57
+
58
+ const provided = {
59
+ onBlur: vi.fn(),
60
+ onChange: vi.fn(),
61
+ };
62
+ const { onChange, onBlur } = getInputProps(provided);
63
+
64
+ onChange!(fakeEvent);
65
+ expect(provided.onChange).toBeCalledTimes(1);
66
+ expect(provided.onChange).toBeCalledWith(fakeEvent);
67
+ expect(options.setTouched).not.toBeCalled();
68
+ expect(options.validate).toBeCalledTimes(1);
69
+
70
+ onBlur!(fakeEvent);
71
+ expect(provided.onBlur).toBeCalledTimes(1);
72
+ expect(provided.onBlur).toBeCalledWith(fakeEvent);
73
+ expect(options.setTouched).toBeCalledTimes(1);
74
+ expect(options.setTouched).toBeCalledWith(true);
75
+ expect(options.validate).toBeCalledTimes(1);
76
+ });
77
+
78
+ it("should not validate when behavior is onSubmit", () => {
79
+ const options: CreateGetInputPropsOptions = {
80
+ name: "some-field",
81
+ defaultValue: "test default value",
82
+ touched: false,
83
+ hasBeenSubmitted: false,
84
+ setTouched: vi.fn(),
85
+ clearError: vi.fn(),
86
+ validate: vi.fn(),
87
+ validationBehavior: {
88
+ initial: "onSubmit",
89
+ },
90
+ };
91
+ const getInputProps = createGetInputProps(options);
92
+
93
+ const provided = {
94
+ onBlur: vi.fn(),
95
+ onChange: vi.fn(),
96
+ };
97
+ const { onChange, onBlur } = getInputProps(provided);
98
+
99
+ onChange!(fakeEvent);
100
+ expect(provided.onChange).toBeCalledTimes(1);
101
+ expect(provided.onChange).toBeCalledWith(fakeEvent);
102
+ expect(options.setTouched).not.toBeCalled();
103
+ expect(options.validate).not.toBeCalled();
104
+
105
+ onBlur!(fakeEvent);
106
+ expect(provided.onBlur).toBeCalledTimes(1);
107
+ expect(provided.onBlur).toBeCalledWith(fakeEvent);
108
+ expect(options.setTouched).toBeCalledTimes(1);
109
+ expect(options.setTouched).toBeCalledWith(true);
110
+ expect(options.validate).not.toBeCalled();
111
+ });
112
+ });
113
+
114
+ describe("whenTouched", () => {
115
+ it("should validate on change by default", () => {
116
+ const options: CreateGetInputPropsOptions = {
117
+ name: "some-field",
118
+ defaultValue: "test default value",
119
+ touched: true,
120
+ hasBeenSubmitted: false,
121
+ setTouched: vi.fn(),
122
+ clearError: vi.fn(),
123
+ validate: vi.fn(),
124
+ };
125
+ const getInputProps = createGetInputProps(options);
126
+
127
+ const provided = {
128
+ onBlur: vi.fn(),
129
+ onChange: vi.fn(),
130
+ };
131
+ const { onChange, onBlur } = getInputProps(provided);
132
+
133
+ onChange!(fakeEvent);
134
+ expect(provided.onChange).toBeCalledTimes(1);
135
+ expect(provided.onChange).toBeCalledWith(fakeEvent);
136
+ expect(options.setTouched).not.toBeCalled();
137
+ expect(options.validate).toBeCalledTimes(1);
138
+
139
+ onBlur!(fakeEvent);
140
+ expect(provided.onBlur).toBeCalledTimes(1);
141
+ expect(provided.onBlur).toBeCalledWith(fakeEvent);
142
+ expect(options.setTouched).toBeCalledTimes(1);
143
+ expect(options.setTouched).toBeCalledWith(true);
144
+ expect(options.validate).toBeCalledTimes(1);
145
+ });
146
+
147
+ it("should respect provided validation behavior", () => {
148
+ const options: CreateGetInputPropsOptions = {
149
+ name: "some-field",
150
+ defaultValue: "test default value",
151
+ touched: true,
152
+ hasBeenSubmitted: false,
153
+ setTouched: vi.fn(),
154
+ clearError: vi.fn(),
155
+ validate: vi.fn(),
156
+ validationBehavior: {
157
+ whenTouched: "onBlur",
158
+ },
159
+ };
160
+ const getInputProps = createGetInputProps(options);
161
+
162
+ const provided = {
163
+ onBlur: vi.fn(),
164
+ onChange: vi.fn(),
165
+ };
166
+ const { onChange, onBlur } = getInputProps(provided);
167
+
168
+ onChange!(fakeEvent);
169
+ expect(provided.onChange).toBeCalledTimes(1);
170
+ expect(provided.onChange).toBeCalledWith(fakeEvent);
171
+ expect(options.setTouched).not.toBeCalled();
172
+ expect(options.validate).not.toBeCalled();
173
+
174
+ onBlur!(fakeEvent);
175
+ expect(provided.onBlur).toBeCalledTimes(1);
176
+ expect(provided.onBlur).toBeCalledWith(fakeEvent);
177
+ expect(options.setTouched).toBeCalledTimes(1);
178
+ expect(options.setTouched).toBeCalledWith(true);
179
+ expect(options.validate).toBeCalledTimes(1);
180
+ });
181
+ });
182
+
183
+ describe("whenSubmitted", () => {
184
+ it("should validate on change by default", () => {
185
+ const options: CreateGetInputPropsOptions = {
186
+ name: "some-field",
187
+ defaultValue: "test default value",
188
+ touched: true,
189
+ hasBeenSubmitted: true,
190
+ setTouched: vi.fn(),
191
+ clearError: vi.fn(),
192
+ validate: vi.fn(),
193
+ };
194
+ const getInputProps = createGetInputProps(options);
195
+
196
+ const provided = {
197
+ onBlur: vi.fn(),
198
+ onChange: vi.fn(),
199
+ };
200
+ const { onChange, onBlur } = getInputProps(provided);
201
+
202
+ onChange!(fakeEvent);
203
+ expect(provided.onChange).toBeCalledTimes(1);
204
+ expect(provided.onChange).toBeCalledWith(fakeEvent);
205
+ expect(options.setTouched).not.toBeCalled();
206
+ expect(options.validate).toBeCalledTimes(1);
207
+
208
+ onBlur!(fakeEvent);
209
+ expect(provided.onBlur).toBeCalledTimes(1);
210
+ expect(provided.onBlur).toBeCalledWith(fakeEvent);
211
+ expect(options.setTouched).toBeCalledTimes(1);
212
+ expect(options.setTouched).toBeCalledWith(true);
213
+ expect(options.validate).toBeCalledTimes(1);
214
+ });
215
+
216
+ it("should respect provided validation behavior", () => {
217
+ const options: CreateGetInputPropsOptions = {
218
+ name: "some-field",
219
+ defaultValue: "test default value",
220
+ touched: true,
221
+ hasBeenSubmitted: true,
222
+ setTouched: vi.fn(),
223
+ clearError: vi.fn(),
224
+ validate: vi.fn(),
225
+ validationBehavior: {
226
+ whenSubmitted: "onBlur",
227
+ },
228
+ };
229
+ const getInputProps = createGetInputProps(options);
230
+
231
+ const provided = {
232
+ onBlur: vi.fn(),
233
+ onChange: vi.fn(),
234
+ };
235
+ const { onChange, onBlur } = getInputProps(provided);
236
+
237
+ onChange!(fakeEvent);
238
+ expect(provided.onChange).toBeCalledTimes(1);
239
+ expect(provided.onChange).toBeCalledWith(fakeEvent);
240
+ expect(options.setTouched).not.toBeCalled();
241
+ expect(options.validate).not.toBeCalled();
242
+
243
+ onBlur!(fakeEvent);
244
+ expect(provided.onBlur).toBeCalledTimes(1);
245
+ expect(provided.onBlur).toBeCalledWith(fakeEvent);
246
+ expect(options.setTouched).toBeCalledTimes(1);
247
+ expect(options.setTouched).toBeCalledWith(true);
248
+ expect(options.validate).toBeCalledTimes(1);
249
+ });
250
+ });
251
+ });
@@ -84,7 +84,8 @@ export const createGetInputProps = ({
84
84
  inputProps.defaultChecked = getCheckboxChecked(props.value, defaultValue);
85
85
  } else if (props.type === "radio") {
86
86
  inputProps.defaultChecked = getRadioChecked(props.value, defaultValue);
87
- } else {
87
+ } else if (props.value === undefined) {
88
+ // We should only set the defaultValue if the input is uncontrolled.
88
89
  inputProps.defaultValue = defaultValue;
89
90
  }
90
91
 
@@ -1,6 +1,4 @@
1
1
  import { useActionData, useMatches, useTransition } from "@remix-run/react";
2
- import { Atom, useAtom, WritableAtom } 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";
@@ -8,25 +6,8 @@ import { FieldErrors, ValidationErrorResponseData } from "..";
8
6
  import { formDefaultValuesKey } from "./constants";
9
7
  import { InternalFormContext, InternalFormContextValue } from "./formContext";
10
8
  import { Hydratable, hydratable } from "./hydratable";
11
- import {
12
- ATOM_SCOPE,
13
- fieldErrorAtom,
14
- fieldTouchedAtom,
15
- formPropsAtom,
16
- isHydratedAtom,
17
- setFieldErrorAtom,
18
- setTouchedAtom,
19
- } from "./state";
20
-
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);
9
+ import { InternalFormId } from "./state/storeFamily";
10
+ import { useFormStore } from "./state/storeHooks";
30
11
 
31
12
  export const useInternalFormContext = (
32
13
  formId?: string | symbol,
@@ -72,7 +53,7 @@ export const useFieldErrorsForForm = (
72
53
  context: InternalFormContextValue
73
54
  ): Hydratable<FieldErrors | undefined> => {
74
55
  const response = useErrorResponseForForm(context);
75
- const hydrated = useFormAtomValue(isHydratedAtom(context.formId));
56
+ const hydrated = useFormStore(context.formId, (state) => state.isHydrated);
76
57
  return hydratable.from(response?.fieldErrors, hydrated);
77
58
  };
78
59
 
@@ -97,7 +78,7 @@ export const useDefaultValuesForForm = (
97
78
  context: InternalFormContextValue
98
79
  ): Hydratable<{ [fieldName: string]: any }> => {
99
80
  const { formId, defaultValuesProp } = context;
100
- const hydrated = useFormAtomValue(isHydratedAtom(formId));
81
+ const hydrated = useFormStore(formId, (state) => state.isHydrated);
101
82
  const errorResponse = useErrorResponseForForm(context);
102
83
  const defaultValuesFromLoader = useDefaultValuesFromLoader(context);
103
84
 
@@ -133,20 +114,31 @@ export const useHasActiveFormSubmit = ({
133
114
  export const useFieldTouched = (
134
115
  field: string,
135
116
  { formId }: InternalFormContextValue
136
- ) => useFormAtom(fieldTouchedAtom({ formId, field }));
117
+ ) => {
118
+ const touched = useFormStore(formId, (state) => state.touchedFields[field]);
119
+ const setFieldTouched = useFormStore(formId, (state) => state.setTouched);
120
+ const setTouched = useCallback(
121
+ (touched: boolean) => setFieldTouched(field, touched),
122
+ [field, setFieldTouched]
123
+ );
124
+ return [touched, setTouched] as const;
125
+ };
137
126
 
138
127
  export const useFieldError = (
139
128
  name: string,
140
129
  context: InternalFormContextValue
141
130
  ) => {
142
131
  const fieldErrors = useFieldErrorsForForm(context);
143
- const [state, set] = useFormAtom(
144
- fieldErrorAtom({ formId: context.formId, field: name })
132
+ const state = useFormStore(
133
+ context.formId,
134
+ (state) => state.fieldErrors[name]
145
135
  );
146
- return [
147
- fieldErrors.map((fieldErrors) => fieldErrors?.[name]).hydrateTo(state),
148
- set,
149
- ] as const;
136
+ return fieldErrors.map((fieldErrors) => fieldErrors?.[name]).hydrateTo(state);
137
+ };
138
+
139
+ export const useClearError = (context: InternalFormContextValue) => {
140
+ const { formId } = context;
141
+ return useFormStore(formId, (state) => state.clearFieldError);
150
142
  };
151
143
 
152
144
  export const useFieldDefaultValue = (
@@ -154,26 +146,58 @@ export const useFieldDefaultValue = (
154
146
  context: InternalFormContextValue
155
147
  ) => {
156
148
  const defaultValues = useDefaultValuesForForm(context);
157
- const { defaultValues: state } = useFormAtomValue(
158
- formPropsAtom(context.formId)
159
- );
149
+ const state = useSyncedDefaultValues(context.formId);
160
150
  return defaultValues
161
151
  .map((val) => lodashGet(val, name))
162
152
  .hydrateTo(lodashGet(state, name));
163
153
  };
164
154
 
165
- export const useClearError = ({ formId }: InternalFormContextValue) => {
166
- const updateError = useFormUpdateAtom(setFieldErrorAtom(formId));
167
- return useCallback(
168
- (name: string) => updateError({ field: name, error: undefined }),
169
- [updateError]
155
+ export const useInternalIsSubmitting = (formId: InternalFormId) =>
156
+ useFormStore(formId, (state) => state.isSubmitting);
157
+
158
+ export const useInternalIsValid = (formId: InternalFormId) =>
159
+ useFormStore(formId, (state) => state.isValid());
160
+
161
+ export const useInternalHasBeenSubmitted = (formId: InternalFormId) =>
162
+ useFormStore(formId, (state) => state.hasBeenSubmitted);
163
+
164
+ export const useValidateField = (formId: InternalFormId) =>
165
+ useFormStore(formId, (state) => state.validateField);
166
+
167
+ export const useValidate = (formId: InternalFormId) =>
168
+ useFormStore(formId, (state) => state.validate);
169
+
170
+ const noOpReceiver = () => () => {};
171
+ export const useRegisterReceiveFocus = (formId: InternalFormId) =>
172
+ useFormStore(
173
+ formId,
174
+ (state) => state.formProps?.registerReceiveFocus ?? noOpReceiver
170
175
  );
171
- };
172
176
 
173
- export const useSetTouched = ({ formId }: InternalFormContextValue) => {
174
- const setTouched = useFormUpdateAtom(setTouchedAtom(formId));
175
- return useCallback(
176
- (name: string, touched: boolean) => setTouched({ field: name, touched }),
177
- [setTouched]
177
+ const defaultDefaultValues = {};
178
+ export const useSyncedDefaultValues = (formId: InternalFormId) =>
179
+ useFormStore(
180
+ formId,
181
+ (state) => state.formProps?.defaultValues ?? defaultDefaultValues
178
182
  );
179
- };
183
+
184
+ export const useSetTouched = ({ formId }: InternalFormContextValue) =>
185
+ useFormStore(formId, (state) => state.setTouched);
186
+
187
+ export const useTouchedFields = (formId: InternalFormId) =>
188
+ useFormStore(formId, (state) => state.touchedFields);
189
+
190
+ export const useFieldErrors = (formId: InternalFormId) =>
191
+ useFormStore(formId, (state) => state.fieldErrors);
192
+
193
+ export const useSetFieldErrors = (formId: InternalFormId) =>
194
+ useFormStore(formId, (state) => state.setFieldErrors);
195
+
196
+ export const useResetFormElement = (formId: InternalFormId) =>
197
+ useFormStore(formId, (state) => state.resetFormElement);
198
+
199
+ export const useFormActionProp = (formId: InternalFormId) =>
200
+ useFormStore(formId, (state) => state.formProps?.action);
201
+
202
+ export const useFormSubactionProp = (formId: InternalFormId) =>
203
+ useFormStore(formId, (state) => state.formProps?.subaction);
@@ -5,3 +5,14 @@ export const getRadioChecked = (
5
5
  if (typeof newValue === "string") return newValue === radioValue;
6
6
  return undefined;
7
7
  };
8
+
9
+ if (import.meta.vitest) {
10
+ const { it, expect } = import.meta.vitest;
11
+ it("getRadioChecked", () => {
12
+ expect(getRadioChecked("on", "on")).toBe(true);
13
+ expect(getRadioChecked("on", undefined)).toBe(undefined);
14
+ expect(getRadioChecked("trueValue", undefined)).toBe(undefined);
15
+ expect(getRadioChecked("trueValue", "bob")).toBe(false);
16
+ expect(getRadioChecked("trueValue", "trueValue")).toBe(true);
17
+ });
18
+ }
@@ -0,0 +1,8 @@
1
+ import { controlledFieldStore } from "./controlledFieldStore";
2
+ import { formStore } from "./createFormStore";
3
+ import { InternalFormId } from "./storeFamily";
4
+
5
+ export const cleanupFormState = (formId: InternalFormId) => {
6
+ formStore.remove(formId);
7
+ controlledFieldStore.remove(formId);
8
+ };
@@ -0,0 +1,91 @@
1
+ import invariant from "tiny-invariant";
2
+ import create from "zustand";
3
+ import { immer } from "zustand/middleware/immer";
4
+ import { storeFamily } from "./storeFamily";
5
+
6
+ export type ControlledFieldState = {
7
+ fields: {
8
+ [fieldName: string]:
9
+ | {
10
+ refCount: number;
11
+ value: unknown;
12
+ defaultValue?: unknown;
13
+ hydrated: boolean;
14
+ valueUpdatePromise: Promise<void> | undefined;
15
+ resolveValueUpdate: (() => void) | undefined;
16
+ }
17
+ | undefined;
18
+ };
19
+ register: (fieldName: string) => void;
20
+ unregister: (fieldName: string) => void;
21
+ setValue: (fieldName: string, value: unknown) => void;
22
+ hydrateWithDefault: (fieldName: string, defaultValue: unknown) => void;
23
+ awaitValueUpdate: (fieldName: string) => Promise<void>;
24
+ reset: () => void;
25
+ };
26
+
27
+ export const controlledFieldStore = storeFamily(() =>
28
+ create<ControlledFieldState>()(
29
+ immer((set, get, api) => ({
30
+ fields: {},
31
+
32
+ register: (field) =>
33
+ set((state) => {
34
+ if (state.fields[field]) {
35
+ state.fields[field]!.refCount++;
36
+ } else {
37
+ state.fields[field] = {
38
+ refCount: 1,
39
+ value: undefined,
40
+ hydrated: false,
41
+ valueUpdatePromise: undefined,
42
+ resolveValueUpdate: undefined,
43
+ };
44
+ }
45
+ }),
46
+
47
+ unregister: (field) =>
48
+ set((state) => {
49
+ const fieldState = state.fields[field];
50
+ if (!fieldState) return;
51
+
52
+ fieldState.refCount--;
53
+ if (fieldState.refCount === 0) delete state.fields[field];
54
+ }),
55
+
56
+ setValue: (field, value) =>
57
+ set((state) => {
58
+ const fieldState = state.fields[field];
59
+ if (!fieldState) return;
60
+
61
+ fieldState.value = value;
62
+ const promise = new Promise<void>((resolve) => {
63
+ fieldState.resolveValueUpdate = resolve;
64
+ });
65
+ fieldState.valueUpdatePromise = promise;
66
+ }),
67
+
68
+ hydrateWithDefault: (field, defaultValue) =>
69
+ set((state) => {
70
+ const fieldState = state.fields[field];
71
+ if (!fieldState) return;
72
+
73
+ fieldState.value = defaultValue;
74
+ fieldState.defaultValue = defaultValue;
75
+ fieldState.hydrated = true;
76
+ }),
77
+
78
+ awaitValueUpdate: async (field) => {
79
+ await get().fields[field]?.valueUpdatePromise;
80
+ },
81
+
82
+ reset: () =>
83
+ set((state) => {
84
+ Object.values(state.fields).forEach((field) => {
85
+ if (!field) return;
86
+ field.value = field.defaultValue;
87
+ });
88
+ }),
89
+ }))
90
+ )
91
+ );
@@ -0,0 +1,78 @@
1
+ import { useCallback, useEffect } from "react";
2
+ import { InternalFormContextValue } from "../formContext";
3
+ import { useFieldDefaultValue } from "../hooks";
4
+ import { controlledFieldStore } from "./controlledFieldStore";
5
+ import { formStore } from "./createFormStore";
6
+ import { InternalFormId } from "./storeFamily";
7
+
8
+ export const useControlledFieldValue = (
9
+ context: InternalFormContextValue,
10
+ field: string
11
+ ) => {
12
+ const useValueStore = controlledFieldStore(context.formId);
13
+ const value = useValueStore((state) => state.fields[field]?.value);
14
+
15
+ const useFormStore = formStore(context.formId);
16
+ const isFormHydrated = useFormStore((state) => state.isHydrated);
17
+ const defaultValue = useFieldDefaultValue(field, context);
18
+
19
+ const isFieldHydrated = useValueStore(
20
+ (state) => state.fields[field]?.hydrated ?? false
21
+ );
22
+ const hydrateWithDefault = useValueStore((state) => state.hydrateWithDefault);
23
+
24
+ useEffect(() => {
25
+ if (isFormHydrated && !isFieldHydrated) {
26
+ hydrateWithDefault(field, defaultValue);
27
+ }
28
+ }, [
29
+ defaultValue,
30
+ field,
31
+ hydrateWithDefault,
32
+ isFieldHydrated,
33
+ isFormHydrated,
34
+ ]);
35
+
36
+ return isFieldHydrated ? value : defaultValue;
37
+ };
38
+
39
+ export const useControllableValue = (
40
+ context: InternalFormContextValue,
41
+ field: string
42
+ ) => {
43
+ const useValueStore = controlledFieldStore(context.formId);
44
+
45
+ const resolveUpdate = useValueStore(
46
+ (state) => state.fields[field]?.resolveValueUpdate
47
+ );
48
+ useEffect(() => {
49
+ resolveUpdate?.();
50
+ }, [resolveUpdate]);
51
+
52
+ const register = useValueStore((state) => state.register);
53
+ const unregister = useValueStore((state) => state.unregister);
54
+ useEffect(() => {
55
+ register(field);
56
+ return () => unregister(field);
57
+ }, [context.formId, field, register, unregister]);
58
+
59
+ const setControlledFieldValue = useValueStore((state) => state.setValue);
60
+ const setValue = useCallback(
61
+ (value: unknown) => setControlledFieldValue(field, value),
62
+ [field, setControlledFieldValue]
63
+ );
64
+
65
+ const value = useControlledFieldValue(context, field);
66
+
67
+ return [value, setValue] as const;
68
+ };
69
+
70
+ export const useUpdateControllableValue = (formId: InternalFormId) => {
71
+ const useValueStore = controlledFieldStore(formId);
72
+ return useValueStore((state) => state.setValue);
73
+ };
74
+
75
+ export const useAwaitValue = (formId: InternalFormId) => {
76
+ const useValueStore = controlledFieldStore(formId);
77
+ return useValueStore((state) => state.awaitValueUpdate);
78
+ };