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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/README.md +4 -4
  3. package/browser/ValidatedForm.d.ts +1 -1
  4. package/browser/ValidatedForm.js +99 -140
  5. package/browser/components.d.ts +5 -8
  6. package/browser/components.js +5 -5
  7. package/browser/hooks.d.ts +19 -14
  8. package/browser/hooks.js +36 -40
  9. package/browser/index.d.ts +1 -1
  10. package/browser/index.js +1 -0
  11. package/browser/internal/constants.d.ts +3 -0
  12. package/browser/internal/constants.js +3 -0
  13. package/browser/internal/formContext.d.ts +7 -49
  14. package/browser/internal/formContext.js +1 -1
  15. package/browser/internal/getInputProps.js +4 -3
  16. package/browser/internal/hooks.d.ts +23 -0
  17. package/browser/internal/hooks.js +114 -0
  18. package/browser/internal/state.d.ts +269 -0
  19. package/browser/internal/state.js +82 -0
  20. package/browser/internal/util.d.ts +1 -0
  21. package/browser/internal/util.js +2 -0
  22. package/browser/lowLevelHooks.d.ts +0 -0
  23. package/browser/lowLevelHooks.js +1 -0
  24. package/browser/server.d.ts +5 -0
  25. package/browser/server.js +5 -0
  26. package/browser/userFacingFormContext.d.ts +56 -0
  27. package/browser/userFacingFormContext.js +40 -0
  28. package/browser/validation/createValidator.js +4 -0
  29. package/browser/validation/types.d.ts +3 -0
  30. package/build/ValidatedForm.d.ts +1 -1
  31. package/build/ValidatedForm.js +95 -136
  32. package/build/hooks.d.ts +19 -14
  33. package/build/hooks.js +38 -46
  34. package/build/index.d.ts +1 -1
  35. package/build/index.js +1 -0
  36. package/build/internal/constants.d.ts +3 -0
  37. package/build/internal/constants.js +7 -0
  38. package/build/internal/formContext.d.ts +7 -49
  39. package/build/internal/formContext.js +2 -2
  40. package/build/internal/getInputProps.js +7 -3
  41. package/build/internal/hooks.d.ts +23 -0
  42. package/build/internal/hooks.js +135 -0
  43. package/build/internal/state.d.ts +269 -0
  44. package/build/internal/state.js +92 -0
  45. package/build/internal/util.d.ts +1 -0
  46. package/build/internal/util.js +3 -1
  47. package/build/server.d.ts +5 -0
  48. package/build/server.js +7 -1
  49. package/build/userFacingFormContext.d.ts +56 -0
  50. package/build/userFacingFormContext.js +44 -0
  51. package/build/validation/createValidator.js +4 -0
  52. package/build/validation/types.d.ts +3 -0
  53. package/package.json +3 -1
  54. package/src/ValidatedForm.tsx +150 -181
  55. package/src/hooks.ts +69 -55
  56. package/src/index.ts +1 -1
  57. package/src/internal/constants.ts +4 -0
  58. package/src/internal/formContext.ts +8 -49
  59. package/src/internal/getInputProps.ts +6 -4
  60. package/src/internal/hooks.ts +200 -0
  61. package/src/internal/state.ts +210 -0
  62. package/src/internal/util.ts +4 -0
  63. package/src/server.ts +16 -0
  64. package/src/userFacingFormContext.ts +129 -0
  65. package/src/validation/createValidator.ts +4 -0
  66. package/src/validation/types.ts +3 -1
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useFormContext = void 0;
4
+ const react_1 = require("react");
5
+ const hooks_1 = require("./hooks");
6
+ const hooks_2 = require("./internal/hooks");
7
+ const state_1 = require("./internal/state");
8
+ /**
9
+ * Provides access to some of the internal state of the form.
10
+ */
11
+ const useFormContext = (formId) => {
12
+ // Try to access context so we get our error specific to this hook if it's not there
13
+ const context = (0, hooks_2.useInternalFormContext)(formId, "useFormContext");
14
+ const action = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.actionAtom);
15
+ const isSubmitting = (0, hooks_1.useIsSubmitting)(formId);
16
+ const hasBeenSubmitted = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.hasBeenSubmittedAtom);
17
+ const isValid = (0, hooks_1.useIsValid)(formId);
18
+ const defaultValues = (0, hooks_2.useHydratableSelector)(context, state_1.defaultValuesAtom, (0, hooks_2.useDefaultValuesForForm)(context));
19
+ const fieldErrors = (0, hooks_2.useHydratableSelector)(context, state_1.fieldErrorsAtom, (0, hooks_2.useFieldErrorsForForm)(context));
20
+ const setFieldTouched = (0, hooks_2.useSetTouched)(context);
21
+ const touchedFields = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.touchedFieldsAtom);
22
+ const validateField = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.validateFieldAtom);
23
+ const registerReceiveFocus = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.registerReceiveFocusAtom);
24
+ const internalClearError = (0, hooks_2.useClearError)(context);
25
+ const clearError = (0, react_1.useCallback)((...names) => {
26
+ names.forEach((name) => {
27
+ internalClearError(name);
28
+ });
29
+ }, [internalClearError]);
30
+ return {
31
+ isSubmitting,
32
+ hasBeenSubmitted,
33
+ isValid,
34
+ defaultValues,
35
+ clearError,
36
+ fieldErrors: fieldErrors !== null && fieldErrors !== void 0 ? fieldErrors : {},
37
+ action,
38
+ setFieldTouched,
39
+ touchedFields,
40
+ validateField,
41
+ registerReceiveFocus,
42
+ };
43
+ };
44
+ exports.useFormContext = useFormContext;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createValidator = void 0;
4
+ const constants_1 = require("../internal/constants");
4
5
  const flatten_1 = require("../internal/flatten");
5
6
  const preprocessFormData = (data) => {
6
7
  // A slightly janky way of determining if the data is a FormData object
@@ -25,14 +26,17 @@ function createValidator(validator) {
25
26
  error: {
26
27
  fieldErrors: result.error,
27
28
  subaction: data.subaction,
29
+ formId: data[constants_1.FORM_ID_FIELD],
28
30
  },
29
31
  submittedData: data,
32
+ formId: data[constants_1.FORM_ID_FIELD],
30
33
  };
31
34
  }
32
35
  return {
33
36
  data: result.data,
34
37
  error: undefined,
35
38
  submittedData: data,
39
+ formId: data[constants_1.FORM_ID_FIELD],
36
40
  };
37
41
  },
38
42
  validateField: (data, field) => validator.validateField(preprocessFormData(data), field),
@@ -5,15 +5,18 @@ export declare type GenericObject = {
5
5
  };
6
6
  export declare type ValidatorError = {
7
7
  subaction?: string;
8
+ formId?: string;
8
9
  fieldErrors: FieldErrors;
9
10
  };
10
11
  export declare type ValidationErrorResponseData = {
11
12
  subaction?: string;
13
+ formId?: string;
12
14
  fieldErrors: FieldErrors;
13
15
  repopulateFields?: unknown;
14
16
  };
15
17
  export declare type BaseResult = {
16
18
  submittedData: GenericObject;
19
+ formId?: string;
17
20
  };
18
21
  export declare type ErrorResult = BaseResult & {
19
22
  error: ValidatorError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remix-validated-form",
3
- "version": "4.0.1-beta.1",
3
+ "version": "4.1.0-beta.1",
4
4
  "description": "Form component and utils for easy form validation in remix",
5
5
  "browser": "./browser/index.js",
6
6
  "main": "./build/index.js",
@@ -48,6 +48,8 @@
48
48
  "typescript": "^4.5.3"
49
49
  },
50
50
  "dependencies": {
51
+ "immer": "^9.0.12",
52
+ "jotai": "^1.5.3",
51
53
  "lodash": "^4.17.21",
52
54
  "tiny-invariant": "^1.2.0"
53
55
  }
@@ -1,30 +1,45 @@
1
- import {
2
- Form as RemixForm,
3
- useActionData,
4
- useFetcher,
5
- useFormAction,
6
- useSubmit,
7
- useTransition,
8
- } from "@remix-run/react";
1
+ import { Form as RemixForm, useFetcher, useSubmit } from "@remix-run/react";
9
2
  import uniq from "lodash/uniq";
10
3
  import React, {
11
4
  ComponentProps,
5
+ RefObject,
6
+ useCallback,
12
7
  useEffect,
13
8
  useMemo,
14
9
  useRef,
15
10
  useState,
16
11
  } from "react";
17
12
  import invariant from "tiny-invariant";
18
- import { FormContext, FormContextValue } from "./internal/formContext";
13
+ import { useIsSubmitting, useIsValid } from "./hooks";
14
+ import { FORM_ID_FIELD } from "./internal/constants";
15
+ import {
16
+ InternalFormContext,
17
+ InternalFormContextValue,
18
+ } from "./internal/formContext";
19
+ import {
20
+ useDefaultValuesFromLoader,
21
+ useErrorResponseForForm,
22
+ useFormUpdateAtom,
23
+ useHasActiveFormSubmit,
24
+ } from "./internal/hooks";
19
25
  import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
26
+ import {
27
+ addErrorAtom,
28
+ clearErrorAtom,
29
+ endSubmitAtom,
30
+ formRegistry,
31
+ FormState,
32
+ resetAtom,
33
+ setFieldErrorsAtom,
34
+ startSubmitAtom,
35
+ syncFormContextAtom,
36
+ } from "./internal/state";
20
37
  import { useSubmitComplete } from "./internal/submissionCallbacks";
21
- import { omit, mergeRefs } from "./internal/util";
22
38
  import {
23
- FieldErrors,
24
- Validator,
25
- TouchedFields,
26
- ValidationErrorResponseData,
27
- } from "./validation/types";
39
+ mergeRefs,
40
+ useIsomorphicLayoutEffect as useLayoutEffect,
41
+ } from "./internal/util";
42
+ import { FieldErrors, Validator } from "./validation/types";
28
43
 
29
44
  export type FormProps<DataType> = {
30
45
  /**
@@ -73,82 +88,8 @@ export type FormProps<DataType> = {
73
88
  disableFocusOnError?: boolean;
74
89
  } & Omit<ComponentProps<typeof RemixForm>, "onSubmit">;
75
90
 
76
- function useErrorResponseForThisForm(
77
- fetcher?: ReturnType<typeof useFetcher>,
78
- subaction?: string
79
- ): ValidationErrorResponseData | null {
80
- const actionData = useActionData<any>();
81
- if (fetcher) {
82
- if ((fetcher.data as any)?.fieldErrors) return fetcher.data as any;
83
- return null;
84
- }
85
-
86
- if (!actionData?.fieldErrors) return null;
87
- if (
88
- (!subaction && !actionData.subaction) ||
89
- actionData.subaction === subaction
90
- )
91
- return actionData;
92
- return null;
93
- }
94
-
95
- function useFieldErrors(
96
- fieldErrorsFromBackend?: FieldErrors
97
- ): [FieldErrors, React.Dispatch<React.SetStateAction<FieldErrors>>] {
98
- const [fieldErrors, setFieldErrors] = useState<FieldErrors>(
99
- fieldErrorsFromBackend ?? {}
100
- );
101
- useEffect(() => {
102
- if (fieldErrorsFromBackend) setFieldErrors(fieldErrorsFromBackend);
103
- }, [fieldErrorsFromBackend]);
104
-
105
- return [fieldErrors, setFieldErrors];
106
- }
107
-
108
- const useIsSubmitting = (
109
- action?: string,
110
- subaction?: string,
111
- fetcher?: ReturnType<typeof useFetcher>
112
- ) => {
113
- const actionForCurrentPage = useFormAction();
114
- const pendingFormSubmit = useTransition().submission;
115
-
116
- if (fetcher) return fetcher.state === "submitting";
117
- if (!pendingFormSubmit) return false;
118
-
119
- const { formData, action: pendingAction } = pendingFormSubmit;
120
- const pendingSubAction = formData.get("subaction");
121
- const expectedAction = action ?? actionForCurrentPage;
122
- if (subaction)
123
- return expectedAction === pendingAction && subaction === pendingSubAction;
124
- return expectedAction === pendingAction && !pendingSubAction;
125
- };
126
-
127
91
  const getDataFromForm = (el: HTMLFormElement) => new FormData(el);
128
92
 
129
- /**
130
- * The purpose for this logic is to handle validation errors when javascript is disabled.
131
- * Normally (without js), when a form is submitted and the action returns the validation errors,
132
- * the form will be reset. The errors will be displayed on the correct fields,
133
- * but all the values in the form will be gone. This is not good UX.
134
- *
135
- * To get around this, we return the submitted form data from the server,
136
- * and use those to populate the form via `defaultValues`.
137
- * This results in a more seamless UX akin to what you would see when js is enabled.
138
- *
139
- * One potential downside is that resetting the form will reset the form
140
- * to the _new_ default values that were returned from the server with the validation errors.
141
- * However, this case is less of a problem than the janky UX caused by losing the form values.
142
- * It will only ever be a problem if the form includes a `<button type="reset" />`
143
- * and only if JS is disabled.
144
- */
145
- function useDefaultValues<DataType>(
146
- repopulateFieldsFromBackend?: any,
147
- defaultValues?: Partial<DataType>
148
- ) {
149
- return repopulateFieldsFromBackend ?? defaultValues;
150
- }
151
-
152
93
  function nonNull<T>(value: T | null | undefined): value is T {
153
94
  return value !== null;
154
95
  }
@@ -203,6 +144,33 @@ const focusFirstInvalidInput = (
203
144
  }
204
145
  };
205
146
 
147
+ const useFormId = (providedId?: string): string | symbol => {
148
+ // We can use a `Symbol` here because we only use it after hydration
149
+ const [symbolId] = useState(() => Symbol("remix-validated-form-id"));
150
+ return providedId ?? symbolId;
151
+ };
152
+
153
+ /**
154
+ * Use a component to access the state so we don't cause
155
+ * any extra rerenders of the whole form.
156
+ */
157
+ const FormResetter = ({
158
+ resetAfterSubmit,
159
+ formRef,
160
+ }: {
161
+ resetAfterSubmit: boolean;
162
+ formRef: RefObject<HTMLFormElement>;
163
+ }) => {
164
+ const isSubmitting = useIsSubmitting();
165
+ const isValid = useIsValid();
166
+ useSubmitComplete(isSubmitting, () => {
167
+ if (isValid && resetAfterSubmit) {
168
+ formRef.current?.reset();
169
+ }
170
+ });
171
+ return null;
172
+ };
173
+
206
174
  /**
207
175
  * The primary form component of `remix-validated-form`.
208
176
  */
@@ -212,103 +180,104 @@ export function ValidatedForm<DataType>({
212
180
  children,
213
181
  fetcher,
214
182
  action,
215
- defaultValues,
183
+ defaultValues: providedDefaultValues,
216
184
  formRef: formRefProp,
217
185
  onReset,
218
186
  subaction,
219
- resetAfterSubmit,
187
+ resetAfterSubmit = false,
220
188
  disableFocusOnError,
221
189
  method,
222
190
  replace,
191
+ id,
223
192
  ...rest
224
193
  }: FormProps<DataType>) {
225
- const backendError = useErrorResponseForThisForm(fetcher, subaction);
226
- const [fieldErrors, setFieldErrors] = useFieldErrors(
227
- backendError?.fieldErrors
228
- );
229
- const isSubmitting = useIsSubmitting(action, subaction, fetcher);
230
- const [isValidating, setIsValidating] = useState(false);
231
- const defaultsToUse = useDefaultValues(
232
- backendError?.repopulateFields,
233
- defaultValues
194
+ const formId = useFormId(id);
195
+ const formAtom = formRegistry(formId);
196
+ const contextValue = useMemo<InternalFormContextValue>(
197
+ () => ({
198
+ formId,
199
+ action,
200
+ subaction,
201
+ defaultValuesProp: providedDefaultValues,
202
+ fetcher,
203
+ }),
204
+ [action, fetcher, formId, providedDefaultValues, subaction]
234
205
  );
235
- const [touchedFields, setTouchedFields] = useState<TouchedFields>({});
236
- const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
237
- const submit = useSubmit();
206
+ const backendError = useErrorResponseForForm(contextValue);
207
+ const backendDefaultValues = useDefaultValuesFromLoader(contextValue);
208
+ const hasActiveSubmission = useHasActiveFormSubmit(contextValue);
238
209
  const formRef = useRef<HTMLFormElement>(null);
239
- useSubmitComplete(isSubmitting, () => {
240
- setIsValidating(false);
241
- if (!backendError && resetAfterSubmit) {
242
- formRef.current?.reset();
243
- }
244
- });
210
+ const Form = fetcher?.Form ?? RemixForm;
211
+
212
+ const submit = useSubmit();
213
+ const clearError = useFormUpdateAtom(clearErrorAtom);
214
+ const addError = useFormUpdateAtom(addErrorAtom);
215
+ const setFieldErrors = useFormUpdateAtom(setFieldErrorsAtom);
216
+ const reset = useFormUpdateAtom(resetAtom);
217
+ const startSubmit = useFormUpdateAtom(startSubmitAtom);
218
+ const endSubmit = useFormUpdateAtom(endSubmitAtom);
219
+ const syncFormContext = useFormUpdateAtom(syncFormContextAtom);
220
+
221
+ const validateField: FormState["validateField"] = useCallback(
222
+ async (fieldName) => {
223
+ invariant(formRef.current, "Cannot find reference to form");
224
+ const { error } = await validator.validateField(
225
+ getDataFromForm(formRef.current),
226
+ fieldName as any
227
+ );
228
+
229
+ if (error) {
230
+ addError({ formAtom, name: fieldName, error });
231
+ return error;
232
+ } else {
233
+ clearError({ name: fieldName, formAtom });
234
+ return null;
235
+ }
236
+ },
237
+ [addError, clearError, formAtom, validator]
238
+ );
239
+
245
240
  const customFocusHandlers = useMultiValueMap<string, () => void>();
241
+ const registerReceiveFocus: FormState["registerReceiveFocus"] = useCallback(
242
+ (fieldName, handler) => {
243
+ customFocusHandlers().add(fieldName, handler);
244
+ return () => {
245
+ customFocusHandlers().remove(fieldName, handler);
246
+ };
247
+ },
248
+ [customFocusHandlers]
249
+ );
246
250
 
247
- const contextValue = useMemo<FormContextValue>(
248
- () => ({
249
- fieldErrors,
251
+ useLayoutEffect(() => {
252
+ syncFormContext({
253
+ formAtom,
250
254
  action,
251
- defaultValues: defaultsToUse,
252
- isSubmitting: isValidating || isSubmitting,
253
- isValid: Object.keys(fieldErrors).length === 0,
254
- touchedFields,
255
- setFieldTouched: (fieldName: string, touched: boolean) =>
256
- setTouchedFields((prev) => ({
257
- ...prev,
258
- [fieldName]: touched,
259
- })),
260
- clearError: (fieldName) => {
261
- setFieldErrors((prev) => omit(prev, fieldName));
262
- },
263
- validateField: async (fieldName) => {
264
- invariant(formRef.current, "Cannot find reference to form");
265
- const { error } = await validator.validateField(
266
- getDataFromForm(formRef.current),
267
- fieldName as any
268
- );
255
+ defaultValues: providedDefaultValues ?? backendDefaultValues,
256
+ subaction,
257
+ validateField,
258
+ registerReceiveFocus,
259
+ });
260
+ }, [
261
+ action,
262
+ formAtom,
263
+ providedDefaultValues,
264
+ registerReceiveFocus,
265
+ subaction,
266
+ syncFormContext,
267
+ validateField,
268
+ backendDefaultValues,
269
+ ]);
269
270
 
270
- // By checking and returning `prev` here, we can avoid a re-render
271
- // if the validation state is the same.
272
- if (error) {
273
- setFieldErrors((prev) => {
274
- if (prev[fieldName] === error) return prev;
275
- return {
276
- ...prev,
277
- [fieldName]: error,
278
- };
279
- });
280
- return error;
281
- } else {
282
- setFieldErrors((prev) => {
283
- if (!(fieldName in prev)) return prev;
284
- return omit(prev, fieldName);
285
- });
286
- return null;
287
- }
288
- },
289
- registerReceiveFocus: (fieldName, handler) => {
290
- customFocusHandlers().add(fieldName, handler);
291
- return () => {
292
- customFocusHandlers().remove(fieldName, handler);
293
- };
294
- },
295
- hasBeenSubmitted,
296
- }),
297
- [
298
- fieldErrors,
299
- action,
300
- defaultsToUse,
301
- isValidating,
302
- isSubmitting,
303
- touchedFields,
304
- hasBeenSubmitted,
305
- setFieldErrors,
306
- validator,
307
- customFocusHandlers,
308
- ]
309
- );
271
+ useEffect(() => {
272
+ setFieldErrors({
273
+ fieldErrors: backendError?.fieldErrors ?? {},
274
+ formAtom,
275
+ });
276
+ }, [backendError?.fieldErrors, formAtom, setFieldErrors]);
310
277
 
311
- const Form = fetcher?.Form ?? RemixForm;
278
+ useSubmitComplete(hasActiveSubmission, () => {
279
+ endSubmit({ formAtom });
280
+ });
312
281
 
313
282
  let clickedButtonRef = React.useRef<any>();
314
283
  useEffect(() => {
@@ -340,19 +309,19 @@ export function ValidatedForm<DataType>({
340
309
  <Form
341
310
  ref={mergeRefs([formRef, formRefProp])}
342
311
  {...rest}
312
+ id={id}
343
313
  action={action}
344
314
  method={method}
345
315
  replace={replace}
346
316
  onSubmit={async (e) => {
347
317
  e.preventDefault();
348
- setHasBeenSubmitted(true);
349
- setIsValidating(true);
318
+ startSubmit({ formAtom });
350
319
  const result = await validator.validate(
351
320
  getDataFromForm(e.currentTarget)
352
321
  );
353
322
  if (result.error) {
354
- setIsValidating(false);
355
- setFieldErrors(result.error.fieldErrors);
323
+ endSubmit({ formAtom });
324
+ setFieldErrors({ fieldErrors: result.error.fieldErrors, formAtom });
356
325
  if (!disableFocusOnError) {
357
326
  focusFirstInvalidInput(
358
327
  result.error.fieldErrors,
@@ -361,7 +330,7 @@ export function ValidatedForm<DataType>({
361
330
  );
362
331
  }
363
332
  } else {
364
- onSubmit && onSubmit(result.data, e);
333
+ onSubmit?.(result.data, e);
365
334
  if (fetcher)
366
335
  fetcher.submit(clickedButtonRef.current || e.currentTarget);
367
336
  else
@@ -375,17 +344,17 @@ export function ValidatedForm<DataType>({
375
344
  onReset={(event) => {
376
345
  onReset?.(event);
377
346
  if (event.defaultPrevented) return;
378
- setFieldErrors({});
379
- setTouchedFields({});
380
- setHasBeenSubmitted(false);
347
+ reset({ formAtom });
381
348
  }}
382
349
  >
383
- <FormContext.Provider value={contextValue}>
350
+ <InternalFormContext.Provider value={contextValue}>
351
+ <FormResetter formRef={formRef} resetAfterSubmit={resetAfterSubmit} />
384
352
  {subaction && (
385
353
  <input type="hidden" value={subaction} name="subaction" />
386
354
  )}
355
+ {id && <input type="hidden" value={id} name={FORM_ID_FIELD} />}
387
356
  {children}
388
- </FormContext.Provider>
357
+ </InternalFormContext.Provider>
389
358
  </Form>
390
359
  );
391
360
  }
package/src/hooks.ts CHANGED
@@ -1,21 +1,44 @@
1
- import get from "lodash/get";
2
- import toPath from "lodash/toPath";
3
- import { useContext, useEffect, useMemo } from "react";
4
- import { FormContext } from "./internal/formContext";
1
+ import { useEffect, useMemo } from "react";
5
2
  import {
6
3
  createGetInputProps,
7
4
  GetInputProps,
8
5
  ValidationBehaviorOptions,
9
6
  } from "./internal/getInputProps";
7
+ import {
8
+ useUnknownFormContextSelectAtom,
9
+ useInternalFormContext,
10
+ useFieldTouched,
11
+ useFieldError,
12
+ useFieldDefaultValue,
13
+ useContextSelectAtom,
14
+ useClearError,
15
+ useSetTouched,
16
+ } from "./internal/hooks";
17
+ import {
18
+ hasBeenSubmittedAtom,
19
+ isSubmittingAtom,
20
+ isValidAtom,
21
+ registerReceiveFocusAtom,
22
+ validateFieldAtom,
23
+ } from "./internal/state";
10
24
 
11
- const useInternalFormContext = (hookName: string) => {
12
- const context = useContext(FormContext);
13
- if (!context)
14
- throw new Error(
15
- `${hookName} must be used within a ValidatedForm component`
16
- );
17
- return context;
18
- };
25
+ /**
26
+ * Returns whether or not the parent form is currently being submitted.
27
+ * This is different from remix's `useTransition().submission` in that it
28
+ * is aware of what form it's in and when _that_ form is being submitted.
29
+ *
30
+ * @param formId
31
+ */
32
+ export const useIsSubmitting = (formId?: string) =>
33
+ useUnknownFormContextSelectAtom(formId, isSubmittingAtom, "useIsSubmitting");
34
+
35
+ /**
36
+ * Returns whether or not the current form is valid.
37
+ *
38
+ * @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
39
+ */
40
+ export const useIsValid = (formId?: string) =>
41
+ useUnknownFormContextSelectAtom(formId, isValidAtom, "useIsValid");
19
42
 
20
43
  export type FieldProps = {
21
44
  /**
@@ -64,21 +87,34 @@ export const useField = (
64
87
  * Allows you to specify when a field gets validated (when using getInputProps)
65
88
  */
66
89
  validationBehavior?: Partial<ValidationBehaviorOptions>;
90
+ /**
91
+ * The formId of the form you want to use.
92
+ * This is not necesary if the input is used inside a form.
93
+ */
94
+ formId?: string;
67
95
  }
68
96
  ): FieldProps => {
69
- const {
70
- fieldErrors,
71
- clearError,
72
- validateField,
73
- defaultValues,
74
- registerReceiveFocus,
75
- touchedFields,
76
- setFieldTouched,
77
- hasBeenSubmitted,
78
- } = useInternalFormContext("useField");
97
+ const { handleReceiveFocus, formId: providedFormId } = options ?? {};
98
+ const formContext = useInternalFormContext(providedFormId, "useField");
99
+
100
+ const defaultValue = useFieldDefaultValue(name, formContext);
101
+ const touched = useFieldTouched(name, formContext);
102
+ const error = useFieldError(name, formContext);
79
103
 
80
- const isTouched = !!touchedFields[name];
81
- const { handleReceiveFocus } = options ?? {};
104
+ const clearError = useClearError(formContext);
105
+ const setTouched = useSetTouched(formContext);
106
+ const hasBeenSubmitted = useContextSelectAtom(
107
+ formContext.formId,
108
+ hasBeenSubmittedAtom
109
+ );
110
+ const validateField = useContextSelectAtom(
111
+ formContext.formId,
112
+ validateFieldAtom
113
+ );
114
+ const registerReceiveFocus = useContextSelectAtom(
115
+ formContext.formId,
116
+ registerReceiveFocusAtom
117
+ );
82
118
 
83
119
  useEffect(() => {
84
120
  if (handleReceiveFocus)
@@ -87,18 +123,14 @@ export const useField = (
87
123
 
88
124
  const field = useMemo<FieldProps>(() => {
89
125
  const helpers = {
90
- error: fieldErrors[name],
91
- clearError: () => {
92
- clearError(name);
93
- },
126
+ error,
127
+ clearError: () => clearError(name),
94
128
  validate: () => {
95
129
  validateField(name);
96
130
  },
97
- defaultValue: defaultValues
98
- ? get(defaultValues, toPath(name), undefined)
99
- : undefined,
100
- touched: isTouched,
101
- setTouched: (touched: boolean) => setFieldTouched(name, touched),
131
+ defaultValue,
132
+ touched,
133
+ setTouched: (touched: boolean) => setTouched(name, touched),
102
134
  };
103
135
  const getInputProps = createGetInputProps({
104
136
  ...helpers,
@@ -111,34 +143,16 @@ export const useField = (
111
143
  getInputProps,
112
144
  };
113
145
  }, [
114
- fieldErrors,
146
+ error,
147
+ defaultValue,
148
+ touched,
115
149
  name,
116
- defaultValues,
117
- isTouched,
118
150
  hasBeenSubmitted,
119
151
  options?.validationBehavior,
120
152
  clearError,
121
153
  validateField,
122
- setFieldTouched,
154
+ setTouched,
123
155
  ]);
124
156
 
125
157
  return field;
126
158
  };
127
-
128
- /**
129
- * Provides access to the entire form context.
130
- */
131
- export const useFormContext = () => useInternalFormContext("useFormContext");
132
-
133
- /**
134
- * Returns whether or not the parent form is currently being submitted.
135
- * This is different from remix's `useTransition().submission` in that it
136
- * is aware of what form it's in and when _that_ form is being submitted.
137
- */
138
- export const useIsSubmitting = () =>
139
- useInternalFormContext("useIsSubmitting").isSubmitting;
140
-
141
- /**
142
- * Returns whether or not the current form is valid.
143
- */
144
- export const useIsValid = () => useInternalFormContext("useIsValid").isValid;