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
@@ -1,9 +1,9 @@
1
1
  $ npm run build:browser && npm run build:main
2
2
 
3
- > remix-validated-form@4.0.0 build:browser
3
+ > remix-validated-form@4.0.2 build:browser
4
4
  > tsc --module ESNext --outDir ./browser
5
5
 
6
6
 
7
- > remix-validated-form@4.0.0 build:main
7
+ > remix-validated-form@4.0.2 build:main
8
8
  > tsc --module CommonJS --outDir ./build
9
9
 
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  A form library built for [remix](https://remix.run) to make validation easy.
4
4
 
5
- - Client-side, field-by-field validation (e.g. validate on blur) and form-level validation
6
- - Set default values for the entire form in one place
5
+ - Client-side, field-by-field and form-level validation
7
6
  - Re-use validation on the server
8
- - Show validation errors from the server even without JS
9
- - Detect if the current form is submitting when there are multiple forms on the page
7
+ - Set default values for the entire form in one place
10
8
  - Supports nested objects and arrays
9
+ - Easily detect if a specific form is being sumitted
11
10
  - Validation library agnostic
11
+ - Can work without JS
12
12
 
13
13
  # Docs
14
14
 
@@ -47,4 +47,4 @@ export declare type FormProps<DataType> = {
47
47
  /**
48
48
  * The primary form component of `remix-validated-form`.
49
49
  */
50
- export declare function ValidatedForm<DataType>({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, disableFocusOnError, method, replace, ...rest }: FormProps<DataType>): JSX.Element;
50
+ export declare function ValidatedForm<DataType>({ validator, onSubmit, children, fetcher, action, defaultValues: providedDefaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, disableFocusOnError, method, replace, id, ...rest }: FormProps<DataType>): JSX.Element;
@@ -1,69 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Form as RemixForm, useActionData, useFormAction, useSubmit, useTransition, } from "@remix-run/react";
2
+ import { Form as RemixForm, useSubmit } from "@remix-run/react";
3
3
  import uniq from "lodash/uniq";
4
- import React, { useEffect, useMemo, useRef, useState, } from "react";
4
+ import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react";
5
5
  import invariant from "tiny-invariant";
6
- import { FormContext } from "./internal/formContext";
6
+ import { useIsSubmitting, useIsValid } from "./hooks";
7
+ import { FORM_ID_FIELD } from "./internal/constants";
8
+ import { InternalFormContext, } from "./internal/formContext";
9
+ import { useDefaultValuesFromLoader, useErrorResponseForForm, useFormUpdateAtom, useHasActiveFormSubmit, } from "./internal/hooks";
7
10
  import { useMultiValueMap } from "./internal/MultiValueMap";
11
+ import { addErrorAtom, clearErrorAtom, endSubmitAtom, formRegistry, resetAtom, setFieldErrorsAtom, startSubmitAtom, syncFormContextAtom, } from "./internal/state";
8
12
  import { useSubmitComplete } from "./internal/submissionCallbacks";
9
- import { omit, mergeRefs } from "./internal/util";
10
- function useErrorResponseForThisForm(fetcher, subaction) {
11
- var _a;
12
- const actionData = useActionData();
13
- if (fetcher) {
14
- if ((_a = fetcher.data) === null || _a === void 0 ? void 0 : _a.fieldErrors)
15
- return fetcher.data;
16
- return null;
17
- }
18
- if (!(actionData === null || actionData === void 0 ? void 0 : actionData.fieldErrors))
19
- return null;
20
- if ((!subaction && !actionData.subaction) ||
21
- actionData.subaction === subaction)
22
- return actionData;
23
- return null;
24
- }
25
- function useFieldErrors(fieldErrorsFromBackend) {
26
- const [fieldErrors, setFieldErrors] = useState(fieldErrorsFromBackend !== null && fieldErrorsFromBackend !== void 0 ? fieldErrorsFromBackend : {});
27
- useEffect(() => {
28
- if (fieldErrorsFromBackend)
29
- setFieldErrors(fieldErrorsFromBackend);
30
- }, [fieldErrorsFromBackend]);
31
- return [fieldErrors, setFieldErrors];
32
- }
33
- const useIsSubmitting = (action, subaction, fetcher) => {
34
- const actionForCurrentPage = useFormAction();
35
- const pendingFormSubmit = useTransition().submission;
36
- if (fetcher)
37
- return fetcher.state === "submitting";
38
- if (!pendingFormSubmit)
39
- return false;
40
- const { formData, action: pendingAction } = pendingFormSubmit;
41
- const pendingSubAction = formData.get("subaction");
42
- const expectedAction = action !== null && action !== void 0 ? action : actionForCurrentPage;
43
- if (subaction)
44
- return expectedAction === pendingAction && subaction === pendingSubAction;
45
- return expectedAction === pendingAction && !pendingSubAction;
46
- };
13
+ import { mergeRefs, useIsomorphicLayoutEffect as useLayoutEffect, } from "./internal/util";
47
14
  const getDataFromForm = (el) => new FormData(el);
48
- /**
49
- * The purpose for this logic is to handle validation errors when javascript is disabled.
50
- * Normally (without js), when a form is submitted and the action returns the validation errors,
51
- * the form will be reset. The errors will be displayed on the correct fields,
52
- * but all the values in the form will be gone. This is not good UX.
53
- *
54
- * To get around this, we return the submitted form data from the server,
55
- * and use those to populate the form via `defaultValues`.
56
- * This results in a more seamless UX akin to what you would see when js is enabled.
57
- *
58
- * One potential downside is that resetting the form will reset the form
59
- * to the _new_ default values that were returned from the server with the validation errors.
60
- * However, this case is less of a problem than the janky UX caused by losing the form values.
61
- * It will only ever be a problem if the form includes a `<button type="reset" />`
62
- * and only if JS is disabled.
63
- */
64
- function useDefaultValues(repopulateFieldsFromBackend, defaultValues) {
65
- return repopulateFieldsFromBackend !== null && repopulateFieldsFromBackend !== void 0 ? repopulateFieldsFromBackend : defaultValues;
66
- }
67
15
  function nonNull(value) {
68
16
  return value !== null;
69
17
  }
@@ -107,87 +55,101 @@ const focusFirstInvalidInput = (fieldErrors, customFocusHandlers, formElement) =
107
55
  }
108
56
  }
109
57
  };
58
+ const useFormId = (providedId) => {
59
+ // We can use a `Symbol` here because we only use it after hydration
60
+ const [symbolId] = useState(() => Symbol("remix-validated-form-id"));
61
+ return providedId !== null && providedId !== void 0 ? providedId : symbolId;
62
+ };
110
63
  /**
111
- * The primary form component of `remix-validated-form`.
64
+ * Use a component to access the state so we don't cause
65
+ * any extra rerenders of the whole form.
112
66
  */
113
- export function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, disableFocusOnError, method, replace, ...rest }) {
114
- var _a;
115
- const backendError = useErrorResponseForThisForm(fetcher, subaction);
116
- const [fieldErrors, setFieldErrors] = useFieldErrors(backendError === null || backendError === void 0 ? void 0 : backendError.fieldErrors);
117
- const isSubmitting = useIsSubmitting(action, subaction, fetcher);
118
- const [isValidating, setIsValidating] = useState(false);
119
- const defaultsToUse = useDefaultValues(backendError === null || backendError === void 0 ? void 0 : backendError.repopulateFields, defaultValues);
120
- const [touchedFields, setTouchedFields] = useState({});
121
- const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
122
- const submit = useSubmit();
123
- const formRef = useRef(null);
67
+ const FormResetter = ({ resetAfterSubmit, formRef, }) => {
68
+ const isSubmitting = useIsSubmitting();
69
+ const isValid = useIsValid();
124
70
  useSubmitComplete(isSubmitting, () => {
125
71
  var _a;
126
- setIsValidating(false);
127
- if (!backendError && resetAfterSubmit) {
72
+ if (isValid && resetAfterSubmit) {
128
73
  (_a = formRef.current) === null || _a === void 0 ? void 0 : _a.reset();
129
74
  }
130
75
  });
131
- const customFocusHandlers = useMultiValueMap();
76
+ return null;
77
+ };
78
+ /**
79
+ * The primary form component of `remix-validated-form`.
80
+ */
81
+ export function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues: providedDefaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit = false, disableFocusOnError, method, replace, id, ...rest }) {
82
+ var _a;
83
+ const formId = useFormId(id);
84
+ const formAtom = formRegistry(formId);
132
85
  const contextValue = useMemo(() => ({
133
- fieldErrors,
86
+ formId,
134
87
  action,
135
- defaultValues: defaultsToUse,
136
- isSubmitting: isValidating || isSubmitting,
137
- isValid: Object.keys(fieldErrors).length === 0,
138
- touchedFields,
139
- setFieldTouched: (fieldName, touched) => setTouchedFields((prev) => ({
140
- ...prev,
141
- [fieldName]: touched,
142
- })),
143
- clearError: (fieldName) => {
144
- setFieldErrors((prev) => omit(prev, fieldName));
145
- },
146
- validateField: async (fieldName) => {
147
- invariant(formRef.current, "Cannot find reference to form");
148
- const { error } = await validator.validateField(getDataFromForm(formRef.current), fieldName);
149
- // By checking and returning `prev` here, we can avoid a re-render
150
- // if the validation state is the same.
151
- if (error) {
152
- setFieldErrors((prev) => {
153
- if (prev[fieldName] === error)
154
- return prev;
155
- return {
156
- ...prev,
157
- [fieldName]: error,
158
- };
159
- });
160
- return error;
161
- }
162
- else {
163
- setFieldErrors((prev) => {
164
- if (!(fieldName in prev))
165
- return prev;
166
- return omit(prev, fieldName);
167
- });
168
- return null;
169
- }
170
- },
171
- registerReceiveFocus: (fieldName, handler) => {
172
- customFocusHandlers().add(fieldName, handler);
173
- return () => {
174
- customFocusHandlers().remove(fieldName, handler);
175
- };
176
- },
177
- hasBeenSubmitted,
178
- }), [
179
- fieldErrors,
88
+ subaction,
89
+ defaultValuesProp: providedDefaultValues,
90
+ fetcher,
91
+ }), [action, fetcher, formId, providedDefaultValues, subaction]);
92
+ const backendError = useErrorResponseForForm(contextValue);
93
+ const backendDefaultValues = useDefaultValuesFromLoader(contextValue);
94
+ const hasActiveSubmission = useHasActiveFormSubmit(contextValue);
95
+ const formRef = useRef(null);
96
+ const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : RemixForm;
97
+ const submit = useSubmit();
98
+ const clearError = useFormUpdateAtom(clearErrorAtom);
99
+ const addError = useFormUpdateAtom(addErrorAtom);
100
+ const setFieldErrors = useFormUpdateAtom(setFieldErrorsAtom);
101
+ const reset = useFormUpdateAtom(resetAtom);
102
+ const startSubmit = useFormUpdateAtom(startSubmitAtom);
103
+ const endSubmit = useFormUpdateAtom(endSubmitAtom);
104
+ const syncFormContext = useFormUpdateAtom(syncFormContextAtom);
105
+ const validateField = useCallback(async (fieldName) => {
106
+ invariant(formRef.current, "Cannot find reference to form");
107
+ const { error } = await validator.validateField(getDataFromForm(formRef.current), fieldName);
108
+ if (error) {
109
+ addError({ formAtom, name: fieldName, error });
110
+ return error;
111
+ }
112
+ else {
113
+ clearError({ name: fieldName, formAtom });
114
+ return null;
115
+ }
116
+ }, [addError, clearError, formAtom, validator]);
117
+ const customFocusHandlers = useMultiValueMap();
118
+ const registerReceiveFocus = useCallback((fieldName, handler) => {
119
+ customFocusHandlers().add(fieldName, handler);
120
+ return () => {
121
+ customFocusHandlers().remove(fieldName, handler);
122
+ };
123
+ }, [customFocusHandlers]);
124
+ useLayoutEffect(() => {
125
+ syncFormContext({
126
+ formAtom,
127
+ action,
128
+ defaultValues: providedDefaultValues !== null && providedDefaultValues !== void 0 ? providedDefaultValues : backendDefaultValues,
129
+ subaction,
130
+ validateField,
131
+ registerReceiveFocus,
132
+ });
133
+ }, [
180
134
  action,
181
- defaultsToUse,
182
- isValidating,
183
- isSubmitting,
184
- touchedFields,
185
- hasBeenSubmitted,
186
- setFieldErrors,
187
- validator,
188
- customFocusHandlers,
135
+ formAtom,
136
+ providedDefaultValues,
137
+ registerReceiveFocus,
138
+ subaction,
139
+ syncFormContext,
140
+ validateField,
141
+ backendDefaultValues,
189
142
  ]);
190
- const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : RemixForm;
143
+ useEffect(() => {
144
+ var _a;
145
+ setFieldErrors({
146
+ fieldErrors: (_a = backendError === null || backendError === void 0 ? void 0 : backendError.fieldErrors) !== null && _a !== void 0 ? _a : {},
147
+ formAtom,
148
+ });
149
+ }, [backendError === null || backendError === void 0 ? void 0 : backendError.fieldErrors, formAtom, setFieldErrors]);
150
+ useSubmitComplete(hasActiveSubmission, () => {
151
+ endSubmit({ formAtom });
152
+ });
191
153
  let clickedButtonRef = React.useRef();
192
154
  useEffect(() => {
193
155
  let form = formRef.current;
@@ -208,20 +170,19 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
208
170
  window.removeEventListener("click", handleClick);
209
171
  };
210
172
  }, []);
211
- return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, action: action, method: method, replace: replace, onSubmit: async (e) => {
173
+ return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, id: id, action: action, method: method, replace: replace, onSubmit: async (e) => {
212
174
  e.preventDefault();
213
- setHasBeenSubmitted(true);
214
- setIsValidating(true);
175
+ startSubmit({ formAtom });
215
176
  const result = await validator.validate(getDataFromForm(e.currentTarget));
216
177
  if (result.error) {
217
- setIsValidating(false);
218
- setFieldErrors(result.error.fieldErrors);
178
+ endSubmit({ formAtom });
179
+ setFieldErrors({ fieldErrors: result.error.fieldErrors, formAtom });
219
180
  if (!disableFocusOnError) {
220
181
  focusFirstInvalidInput(result.error.fieldErrors, customFocusHandlers(), formRef.current);
221
182
  }
222
183
  }
223
184
  else {
224
- onSubmit && onSubmit(result.data, e);
185
+ onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(result.data, e);
225
186
  if (fetcher)
226
187
  fetcher.submit(clickedButtonRef.current || e.currentTarget);
227
188
  else
@@ -235,8 +196,6 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
235
196
  onReset === null || onReset === void 0 ? void 0 : onReset(event);
236
197
  if (event.defaultPrevented)
237
198
  return;
238
- setFieldErrors({});
239
- setTouchedFields({});
240
- setHasBeenSubmitted(false);
241
- }, children: _jsxs(FormContext.Provider, { value: contextValue, children: [subaction && (_jsx("input", { type: "hidden", value: subaction, name: "subaction" }, void 0)), children] }, void 0) }, void 0));
199
+ reset({ formAtom });
200
+ }, 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));
242
201
  }
@@ -1,10 +1,7 @@
1
1
  import { HTMLProps } from "react";
2
- declare type ValidatedInputProps = HTMLProps<HTMLInputElement> & {
3
- name: string;
4
- };
5
- export declare const ValidatedInput: ({ name, ...rest }: ValidatedInputProps) => JSX.Element;
6
- declare type ErrorMessageProps = {
7
- name: string;
8
- };
9
- export declare const ErrorMessage: ({ name }: ErrorMessageProps) => string | undefined;
2
+ declare type WithRequiredName<T extends {
3
+ name?: string;
4
+ }> = T & Required<Pick<T, "name">>;
5
+ export declare const ValidatedInput: ({ name, form, ...rest }: WithRequiredName<HTMLProps<HTMLInputElement>>) => JSX.Element;
6
+ export declare const ValidatedTextarea: ({ name, form, ...rest }: WithRequiredName<HTMLProps<HTMLTextAreaElement>>) => JSX.Element;
10
7
  export {};
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useField } from ".";
3
- export const ValidatedInput = ({ name, ...rest }) => {
4
- const { getInputProps } = useField(name);
3
+ export const ValidatedInput = ({ name, form, ...rest }) => {
4
+ const { getInputProps } = useField(name, { formId: form });
5
5
  return _jsx("input", { ...getInputProps(rest) }, void 0);
6
6
  };
7
- export const ErrorMessage = ({ name }) => {
8
- const { error } = useField(name);
9
- return error;
7
+ export const ValidatedTextarea = ({ name, form, ...rest }) => {
8
+ const { getInputProps } = useField(name, { formId: form });
9
+ return _jsx("textarea", { ...getInputProps(rest) }, void 0);
10
10
  };
@@ -1,4 +1,18 @@
1
1
  import { GetInputProps, ValidationBehaviorOptions } from "./internal/getInputProps";
2
+ /**
3
+ * Returns whether or not the parent form is currently being submitted.
4
+ * This is different from remix's `useTransition().submission` in that it
5
+ * is aware of what form it's in and when _that_ form is being submitted.
6
+ *
7
+ * @param formId
8
+ */
9
+ export declare const useIsSubmitting: (formId?: string | undefined) => boolean;
10
+ /**
11
+ * Returns whether or not the current form is valid.
12
+ *
13
+ * @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
14
+ */
15
+ export declare const useIsValid: (formId?: string | undefined) => boolean;
2
16
  export declare type FieldProps = {
3
17
  /**
4
18
  * The validation error message if there is one.
@@ -43,18 +57,9 @@ export declare const useField: (name: string, options?: {
43
57
  * Allows you to specify when a field gets validated (when using getInputProps)
44
58
  */
45
59
  validationBehavior?: Partial<ValidationBehaviorOptions> | undefined;
60
+ /**
61
+ * The formId of the form you want to use.
62
+ * This is not necesary if the input is used inside a form.
63
+ */
64
+ formId?: string | undefined;
46
65
  } | undefined) => FieldProps;
47
- /**
48
- * Provides access to the entire form context.
49
- */
50
- export declare const useFormContext: () => import("./internal/formContext").FormContextValue;
51
- /**
52
- * Returns whether or not the parent form is currently being submitted.
53
- * This is different from remix's `useTransition().submission` in that it
54
- * is aware of what form it's in and when _that_ form is being submitted.
55
- */
56
- export declare const useIsSubmitting: () => boolean;
57
- /**
58
- * Returns whether or not the current form is valid.
59
- */
60
- export declare const useIsValid: () => boolean;
package/browser/hooks.js CHANGED
@@ -1,39 +1,49 @@
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 { createGetInputProps, } from "./internal/getInputProps";
6
- const useInternalFormContext = (hookName) => {
7
- const context = useContext(FormContext);
8
- if (!context)
9
- throw new Error(`${hookName} must be used within a ValidatedForm component`);
10
- return context;
11
- };
3
+ import { useUnknownFormContextSelectAtom, useInternalFormContext, useFieldTouched, useFieldError, useFieldDefaultValue, useContextSelectAtom, useClearError, useSetTouched, } from "./internal/hooks";
4
+ import { hasBeenSubmittedAtom, isSubmittingAtom, isValidAtom, registerReceiveFocusAtom, validateFieldAtom, } from "./internal/state";
5
+ /**
6
+ * Returns whether or not the parent form is currently being submitted.
7
+ * This is different from remix's `useTransition().submission` in that it
8
+ * is aware of what form it's in and when _that_ form is being submitted.
9
+ *
10
+ * @param formId
11
+ */
12
+ export const useIsSubmitting = (formId) => useUnknownFormContextSelectAtom(formId, isSubmittingAtom, "useIsSubmitting");
13
+ /**
14
+ * Returns whether or not the current form is valid.
15
+ *
16
+ * @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
17
+ */
18
+ export const useIsValid = (formId) => useUnknownFormContextSelectAtom(formId, isValidAtom, "useIsValid");
12
19
  /**
13
20
  * Provides the data and helpers necessary to set up a field.
14
21
  */
15
22
  export const useField = (name, options) => {
16
- const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, touchedFields, setFieldTouched, hasBeenSubmitted, } = useInternalFormContext("useField");
17
- const isTouched = !!touchedFields[name];
18
- const { handleReceiveFocus } = options !== null && options !== void 0 ? options : {};
23
+ const { handleReceiveFocus, formId: providedFormId } = options !== null && options !== void 0 ? options : {};
24
+ const formContext = useInternalFormContext(providedFormId, "useField");
25
+ const defaultValue = useFieldDefaultValue(name, formContext);
26
+ const touched = useFieldTouched(name, formContext);
27
+ const error = useFieldError(name, formContext);
28
+ const clearError = useClearError(formContext);
29
+ const setTouched = useSetTouched(formContext);
30
+ const hasBeenSubmitted = useContextSelectAtom(formContext.formId, hasBeenSubmittedAtom);
31
+ const validateField = useContextSelectAtom(formContext.formId, validateFieldAtom);
32
+ const registerReceiveFocus = useContextSelectAtom(formContext.formId, registerReceiveFocusAtom);
19
33
  useEffect(() => {
20
34
  if (handleReceiveFocus)
21
35
  return registerReceiveFocus(name, handleReceiveFocus);
22
36
  }, [handleReceiveFocus, name, registerReceiveFocus]);
23
37
  const field = useMemo(() => {
24
38
  const helpers = {
25
- error: fieldErrors[name],
26
- clearError: () => {
27
- clearError(name);
28
- },
39
+ error,
40
+ clearError: () => clearError(name),
29
41
  validate: () => {
30
42
  validateField(name);
31
43
  },
32
- defaultValue: defaultValues
33
- ? get(defaultValues, toPath(name), undefined)
34
- : undefined,
35
- touched: isTouched,
36
- setTouched: (touched) => setFieldTouched(name, touched),
44
+ defaultValue,
45
+ touched,
46
+ setTouched: (touched) => setTouched(name, touched),
37
47
  };
38
48
  const getInputProps = createGetInputProps({
39
49
  ...helpers,
@@ -46,29 +56,15 @@ export const useField = (name, options) => {
46
56
  getInputProps,
47
57
  };
48
58
  }, [
49
- fieldErrors,
59
+ error,
60
+ defaultValue,
61
+ touched,
50
62
  name,
51
- defaultValues,
52
- isTouched,
53
63
  hasBeenSubmitted,
54
64
  options === null || options === void 0 ? void 0 : options.validationBehavior,
55
65
  clearError,
56
66
  validateField,
57
- setFieldTouched,
67
+ setTouched,
58
68
  ]);
59
69
  return field;
60
70
  };
61
- /**
62
- * Provides access to the entire form context.
63
- */
64
- export const useFormContext = () => useInternalFormContext("useFormContext");
65
- /**
66
- * Returns whether or not the parent form is currently being submitted.
67
- * This is different from remix's `useTransition().submission` in that it
68
- * is aware of what form it's in and when _that_ form is being submitted.
69
- */
70
- export const useIsSubmitting = () => useInternalFormContext("useIsSubmitting").isSubmitting;
71
- /**
72
- * Returns whether or not the current form is valid.
73
- */
74
- export const useIsValid = () => useInternalFormContext("useIsValid").isValid;
@@ -3,4 +3,4 @@ export * from "./server";
3
3
  export * from "./ValidatedForm";
4
4
  export * from "./validation/types";
5
5
  export * from "./validation/createValidator";
6
- export type { FormContextValue } from "./internal/formContext";
6
+ export * from "./userFacingFormContext";
package/browser/index.js CHANGED
@@ -3,3 +3,4 @@ export * from "./server";
3
3
  export * from "./ValidatedForm";
4
4
  export * from "./validation/types";
5
5
  export * from "./validation/createValidator";
6
+ export * from "./userFacingFormContext";
@@ -0,0 +1,3 @@
1
+ export declare const FORM_ID_FIELD: "__rvfInternalFormId";
2
+ export declare const FORM_DEFAULTS_FIELD: "__rvfInternalFormDefaults";
3
+ export declare const formDefaultValuesKey: (formId: string) => string;
@@ -0,0 +1,3 @@
1
+ export const FORM_ID_FIELD = "__rvfInternalFormId";
2
+ export const FORM_DEFAULTS_FIELD = "__rvfInternalFormDefaults";
3
+ export const formDefaultValuesKey = (formId) => `${FORM_DEFAULTS_FIELD}_${formId}`;
@@ -1,54 +1,12 @@
1
1
  /// <reference types="react" />
2
- import { FieldErrors, TouchedFields } from "../validation/types";
3
- export declare type FormContextValue = {
4
- /**
5
- * All the errors in all the fields in the form.
6
- */
7
- fieldErrors: FieldErrors;
8
- /**
9
- * Clear the errors of the specified fields.
10
- */
11
- clearError: (...names: string[]) => void;
12
- /**
13
- * Validate the specified field.
14
- */
15
- validateField: (fieldName: string) => Promise<string | null>;
16
- /**
17
- * The `action` prop of the form.
18
- */
2
+ import { useFetcher } from "@remix-run/react";
3
+ export declare type InternalFormContextValue = {
4
+ formId: string | symbol;
19
5
  action?: string;
20
- /**
21
- * Whether or not the form is submitting.
22
- */
23
- isSubmitting: boolean;
24
- /**
25
- * Whether or not a submission has been attempted.
26
- * This is true once the form has been submitted, even if there were validation errors.
27
- * Resets to false when the form is reset.
28
- */
29
- hasBeenSubmitted: boolean;
30
- /**
31
- * Whether or not the form is valid.
32
- */
33
- isValid: boolean;
34
- /**
35
- * The default values of the form.
36
- */
37
- defaultValues?: {
6
+ subaction?: string;
7
+ defaultValuesProp?: {
38
8
  [fieldName: string]: any;
39
9
  };
40
- /**
41
- * Register a custom focus handler to be used when
42
- * the field needs to receive focus due to a validation error.
43
- */
44
- registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
45
- /**
46
- * Any fields that have been touched by the user.
47
- */
48
- touchedFields: TouchedFields;
49
- /**
50
- * Change the touched state of the specified field.
51
- */
52
- setFieldTouched: (fieldName: string, touched: boolean) => void;
10
+ fetcher?: ReturnType<typeof useFetcher>;
53
11
  };
54
- export declare const FormContext: import("react").Context<FormContextValue | null>;
12
+ export declare const InternalFormContext: import("react").Context<InternalFormContextValue | null>;
@@ -1,2 +1,2 @@
1
1
  import { createContext } from "react";
2
- export const FormContext = createContext(null);
2
+ export const InternalFormContext = createContext(null);
@@ -1,3 +1,4 @@
1
+ import omitBy from "lodash/omitBy";
1
2
  const defaultValidationBehavior = {
2
3
  initial: "onBlur",
3
4
  whenTouched: "onChange",
@@ -43,17 +44,17 @@ export const createGetInputProps = ({ clearError, validate, defaultValue, touche
43
44
  },
44
45
  name,
45
46
  };
46
- if (inputProps.type === "checkbox") {
47
+ if (props.type === "checkbox") {
47
48
  const value = (_a = props.value) !== null && _a !== void 0 ? _a : "on";
48
49
  inputProps.defaultChecked = getCheckboxDefaultChecked(value, defaultValue);
49
50
  }
50
- else if (inputProps.type === "radio") {
51
+ else if (props.type === "radio") {
51
52
  const value = (_b = props.value) !== null && _b !== void 0 ? _b : "on";
52
53
  inputProps.defaultChecked = defaultValue === value;
53
54
  }
54
55
  else {
55
56
  inputProps.defaultValue = defaultValue;
56
57
  }
57
- return inputProps;
58
+ return omitBy(inputProps, (value) => value === undefined);
58
59
  };
59
60
  };