remix-validated-form 2.1.0 → 3.1.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 (52) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/.turbo/turbo-test.log +10 -35
  3. package/README.md +20 -8
  4. package/browser/ValidatedForm.d.ts +6 -1
  5. package/browser/ValidatedForm.js +37 -2
  6. package/browser/hooks.d.ts +8 -1
  7. package/browser/hooks.js +8 -3
  8. package/browser/index.d.ts +0 -2
  9. package/browser/index.js +0 -2
  10. package/browser/internal/MultiValueMap.d.ts +0 -0
  11. package/browser/internal/MultiValueMap.js +1 -0
  12. package/browser/internal/SingleTypeMultiValueMap.d.ts +8 -0
  13. package/browser/internal/SingleTypeMultiValueMap.js +40 -0
  14. package/browser/internal/formContext.d.ts +10 -0
  15. package/browser/internal/formContext.js +2 -0
  16. package/browser/server.d.ts +1 -1
  17. package/browser/server.js +11 -1
  18. package/browser/validation/types.d.ts +1 -0
  19. package/build/ValidatedForm.d.ts +6 -1
  20. package/build/ValidatedForm.js +37 -2
  21. package/build/hooks.d.ts +8 -1
  22. package/build/hooks.js +7 -2
  23. package/build/index.d.ts +0 -2
  24. package/build/index.js +0 -2
  25. package/build/internal/SingleTypeMultiValueMap.d.ts +8 -0
  26. package/build/internal/SingleTypeMultiValueMap.js +45 -0
  27. package/build/internal/formContext.d.ts +10 -0
  28. package/build/internal/formContext.js +2 -0
  29. package/build/server.d.ts +1 -1
  30. package/build/server.js +11 -1
  31. package/build/validation/types.d.ts +1 -0
  32. package/package.json +3 -8
  33. package/src/ValidatedForm.tsx +59 -1
  34. package/src/hooks.ts +26 -4
  35. package/src/index.ts +0 -2
  36. package/src/internal/SingleTypeMultiValueMap.ts +37 -0
  37. package/src/internal/formContext.ts +12 -0
  38. package/src/server.ts +18 -2
  39. package/src/validation/types.ts +6 -0
  40. package/build/test-data/testFormData.d.ts +0 -15
  41. package/build/test-data/testFormData.js +0 -50
  42. package/build/validation/validation.test.d.ts +0 -1
  43. package/build/validation/validation.test.js +0 -295
  44. package/build/validation/withYup.d.ts +0 -6
  45. package/build/validation/withYup.js +0 -44
  46. package/build/validation/withZod.d.ts +0 -6
  47. package/build/validation/withZod.js +0 -57
  48. package/jest.config.js +0 -10
  49. package/src/test-data/testFormData.ts +0 -55
  50. package/src/validation/validation.test.ts +0 -322
  51. package/src/validation/withYup.ts +0 -43
  52. package/src/validation/withZod.ts +0 -51
@@ -1,9 +1,9 @@
1
1
  $ npm run build:browser && npm run build:main
2
2
 
3
- > remix-validated-form@2.0.0-beta.2 build:browser
3
+ > remix-validated-form@3.0.0 build:browser
4
4
  > tsc --module ESNext --outDir ./browser
5
5
 
6
6
 
7
- > remix-validated-form@2.0.0-beta.2 build:main
7
+ > remix-validated-form@3.0.0 build:main
8
8
  > tsc --module CommonJS --outDir ./build
9
9
 
@@ -1,36 +1,11 @@
1
1
  $ jest src
2
-  PASS  src/validation/validation.test.ts
3
- Validation
4
- Adapter for yup
5
- validate
6
- ✓ should return the data when valid (2 ms)
7
- ✓ should return field errors when invalid (1 ms)
8
- ✓ should unflatten data when validating
9
- ✓ should accept FormData directly and return errors (1 ms)
10
- ✓ should accept FormData directly and return valid data (1 ms)
11
- validateField
12
- ✓ should not return an error if field is valid
13
- ✓ should not return an error if a nested field is valid (1 ms)
14
- ✓ should return an error if field is invalid (2 ms)
15
- ✓ should return an error if a nested field is invalid
16
- Adapter for zod
17
- validate
18
- ✓ should return the data when valid (1 ms)
19
- ✓ should return field errors when invalid
20
- ✓ should unflatten data when validating (1 ms)
21
- ✓ should accept FormData directly and return errors
22
- ✓ should accept FormData directly and return valid data
23
- validateField
24
- ✓ should not return an error if field is valid (1 ms)
25
- ✓ should not return an error if a nested field is valid
26
- ✓ should return an error if field is invalid
27
- ✓ should return an error if a nested field is invalid
28
- withZod
29
- ✓ returns coherent errors for complex schemas (1 ms)
30
- ✓ returns errors for fields that are unions
31
-
32
- Test Suites: 1 passed, 1 total
33
- Tests: 20 passed, 20 total
34
- Snapshots: 0 total
35
- Time: 1.1 s, estimated 2 s
36
- Ran all test suites matching /src/i.
2
+ No tests found, exiting with code 1
3
+ Run with `--passWithNoTests` to exit with code 0
4
+ In /Users/aaronpettengill/dev/remix-validated-form/packages/remix-validated-form
5
+ 68 files checked.
6
+ testMatch: **/__tests__/**/*.[jt]s?(x), **/?(*.)+(spec|test).[tj]s?(x) - 2 matches
7
+ testPathIgnorePatterns: /node_modules/ - 68 matches
8
+ testRegex: - 0 matches
9
+ Pattern: src - 0 matches
10
+ info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
11
+ error Command failed with exit code 1.
package/README.md CHANGED
@@ -18,22 +18,34 @@ To run `sample-app`:
18
18
 
19
19
  ```
20
20
  git clone https://github.com/airjp73/remix-validated-form
21
- cd remix-validated-form
22
- npm i
23
- cd sample-app
24
- npm i
25
- cd ..
26
- npm run sample-app
21
+ cd ./remix-validated-form
22
+ yarn install
23
+ yarn sample-app
27
24
  ```
28
25
 
29
26
  # Getting started
30
27
 
31
28
  ## Install
32
29
 
30
+ ### Base package
31
+
33
32
  ```bash
34
33
  npm install remix-validated-form
35
34
  ```
36
35
 
36
+ ### Validation library adapter
37
+
38
+ There are official adapters available for `zod` and `yup`.
39
+ If you're using a different library,
40
+ see the [Validation library support](#validation-library-support) section below.
41
+
42
+ - @remix-validated-form/with-zod
43
+ - @remix-validated-form/with-yup
44
+
45
+ ```bash
46
+ npm install @remix-validated-form/with-zod
47
+ ```
48
+
37
49
  ## Create an input component
38
50
 
39
51
  In order to display field errors or do field-by-field validation,
@@ -167,10 +179,10 @@ export default function MyForm() {
167
179
 
168
180
  # Validation Library Support
169
181
 
170
- This library currently includes an out-of-the-box adapter for `yup` and `zod`,
182
+ There are official adapters available for `zod` and `yup` ,
171
183
  but you can easily support whatever library you want by creating your own adapter.
172
184
 
173
- And if you create an adapter for a library, feel free to make a PR on this library to add official support 😊
185
+ And if you create an adapter for a library, feel free to make a PR on this repository 😊
174
186
 
175
187
  ## Creating an adapter
176
188
 
@@ -38,8 +38,13 @@ export declare type FormProps<DataType> = {
38
38
  * and don't redirect in-between submissions.
39
39
  */
40
40
  resetAfterSubmit?: boolean;
41
+ /**
42
+ * Normally, the first invalid input will be focused when the validation fails on form submit.
43
+ * Set this to `false` to disable this behavior.
44
+ */
45
+ disableFocusOnError?: boolean;
41
46
  } & Omit<ComponentProps<typeof RemixForm>, "onSubmit">;
42
47
  /**
43
48
  * The primary form component of `remix-validated-form`.
44
49
  */
45
- export declare function ValidatedForm<DataType>({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, ...rest }: FormProps<DataType>): JSX.Element;
50
+ export declare function ValidatedForm<DataType>({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, disableFocusOnError, ...rest }: FormProps<DataType>): JSX.Element;
@@ -3,6 +3,7 @@ import { Form as RemixForm, useActionData, useFormAction, useTransition, } from
3
3
  import { useEffect, useMemo, useRef, useState, } from "react";
4
4
  import invariant from "tiny-invariant";
5
5
  import { FormContext } from "./internal/formContext";
6
+ import { useMultiValueMap, } from "./internal/SingleTypeMultiValueMap";
6
7
  import { useSubmitComplete } from "./internal/submissionCallbacks";
7
8
  import { omit, mergeRefs } from "./internal/util";
8
9
  function useFieldErrorsFromBackend(fetcher, subaction) {
@@ -64,10 +65,32 @@ function useDefaultValues(fieldErrors, defaultValues) {
64
65
  const defaultsFromValidationError = fieldErrors === null || fieldErrors === void 0 ? void 0 : fieldErrors._submittedData;
65
66
  return defaultsFromValidationError !== null && defaultsFromValidationError !== void 0 ? defaultsFromValidationError : defaultValues;
66
67
  }
68
+ const focusFirstInvalidInput = (fieldErrors, customFocusHandlers, formElement) => {
69
+ const invalidInputSelector = Object.keys(fieldErrors)
70
+ .map((fieldName) => `input[name="${fieldName}"]`)
71
+ .join(",");
72
+ const invalidInputs = formElement.querySelectorAll(invalidInputSelector);
73
+ for (const element of invalidInputs) {
74
+ const input = element;
75
+ if (customFocusHandlers.has(input.name)) {
76
+ customFocusHandlers.getAll(input.name).forEach((handler) => {
77
+ handler();
78
+ });
79
+ break;
80
+ }
81
+ // We don't filter these out ahead of time because
82
+ // they could have a custom focus handler
83
+ if (input.type === "hidden") {
84
+ continue;
85
+ }
86
+ input.focus();
87
+ break;
88
+ }
89
+ };
67
90
  /**
68
91
  * The primary form component of `remix-validated-form`.
69
92
  */
70
- export function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, ...rest }) {
93
+ export function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, disableFocusOnError, ...rest }) {
71
94
  var _a;
72
95
  const fieldErrorsFromBackend = useFieldErrorsFromBackend(fetcher, subaction);
73
96
  const [fieldErrors, setFieldErrors] = useFieldErrors(fieldErrorsFromBackend);
@@ -80,11 +103,13 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
80
103
  (_a = formRef.current) === null || _a === void 0 ? void 0 : _a.reset();
81
104
  }
82
105
  });
106
+ const customFocusHandlers = useMultiValueMap();
83
107
  const contextValue = useMemo(() => ({
84
108
  fieldErrors,
85
109
  action,
86
110
  defaultValues: defaultsToUse,
87
111
  isSubmitting: isSubmitting !== null && isSubmitting !== void 0 ? isSubmitting : false,
112
+ isValid: Object.keys(fieldErrors).length === 0,
88
113
  clearError: (fieldName) => {
89
114
  setFieldErrors((prev) => omit(prev, fieldName));
90
115
  },
@@ -98,6 +123,12 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
98
123
  }));
99
124
  }
100
125
  },
126
+ registerReceiveFocus: (fieldName, handler) => {
127
+ customFocusHandlers().add(fieldName, handler);
128
+ return () => {
129
+ customFocusHandlers().remove(fieldName, handler);
130
+ };
131
+ },
101
132
  }), [
102
133
  fieldErrors,
103
134
  action,
@@ -105,6 +136,7 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
105
136
  isSubmitting,
106
137
  setFieldErrors,
107
138
  validator,
139
+ customFocusHandlers,
108
140
  ]);
109
141
  const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : RemixForm;
110
142
  return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, action: action, onSubmit: (event) => {
@@ -112,6 +144,9 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
112
144
  if (result.error) {
113
145
  event.preventDefault();
114
146
  setFieldErrors(result.error);
147
+ if (!disableFocusOnError) {
148
+ focusFirstInvalidInput(result.error, customFocusHandlers(), formRef.current);
149
+ }
115
150
  }
116
151
  else {
117
152
  onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(result.data, event);
@@ -121,5 +156,5 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
121
156
  if (event.defaultPrevented)
122
157
  return;
123
158
  setFieldErrors({});
124
- }, children: _jsxs(FormContext.Provider, { value: contextValue, children: [_jsx("input", { type: "hidden", value: subaction, name: "subaction" }, void 0), children] }, void 0) }, void 0));
159
+ }, children: _jsxs(FormContext.Provider, { value: contextValue, children: [subaction && (_jsx("input", { type: "hidden", value: subaction, name: "subaction" }, void 0)), children] }, void 0) }, void 0));
125
160
  }
@@ -19,7 +19,14 @@ export declare type FieldProps = {
19
19
  /**
20
20
  * Provides the data and helpers necessary to set up a field.
21
21
  */
22
- export declare const useField: (name: string) => FieldProps;
22
+ export declare const useField: (name: string, options?: {
23
+ /**
24
+ * Allows you to configure a custom function that will be called
25
+ * when the input needs to receive focus due to a validation error.
26
+ * This is useful for custom components that use a hidden input.
27
+ */
28
+ handleReceiveFocus?: (() => void) | undefined;
29
+ } | undefined) => FieldProps;
23
30
  /**
24
31
  * Provides access to the entire form context.
25
32
  * This is not usually necessary, but can be useful for advanced use cases.
package/browser/hooks.js CHANGED
@@ -1,12 +1,17 @@
1
1
  import get from "lodash/get";
2
2
  import toPath from "lodash/toPath";
3
- import { useContext, useMemo } from "react";
3
+ import { useContext, useEffect, useMemo } from "react";
4
4
  import { FormContext } from "./internal/formContext";
5
5
  /**
6
6
  * Provides the data and helpers necessary to set up a field.
7
7
  */
8
- export const useField = (name) => {
9
- const { fieldErrors, clearError, validateField, defaultValues } = useContext(FormContext);
8
+ export const useField = (name, options) => {
9
+ const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, } = useContext(FormContext);
10
+ const { handleReceiveFocus } = options !== null && options !== void 0 ? options : {};
11
+ useEffect(() => {
12
+ if (handleReceiveFocus)
13
+ return registerReceiveFocus(name, handleReceiveFocus);
14
+ }, [handleReceiveFocus, name, registerReceiveFocus]);
10
15
  const field = useMemo(() => ({
11
16
  error: fieldErrors[name],
12
17
  clearError: () => {
@@ -2,7 +2,5 @@ export * from "./hooks";
2
2
  export * from "./server";
3
3
  export * from "./ValidatedForm";
4
4
  export * from "./validation/types";
5
- export * from "./validation/withYup";
6
- export * from "./validation/withZod";
7
5
  export * from "./validation/createValidator";
8
6
  export type { FormContextValue } from "./internal/formContext";
package/browser/index.js CHANGED
@@ -2,6 +2,4 @@ export * from "./hooks";
2
2
  export * from "./server";
3
3
  export * from "./ValidatedForm";
4
4
  export * from "./validation/types";
5
- export * from "./validation/withYup";
6
- export * from "./validation/withZod";
7
5
  export * from "./validation/createValidator";
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,8 @@
1
+ export declare class MultiValueMap<Key, Value> {
2
+ private dict;
3
+ add: (key: Key, value: Value) => void;
4
+ remove: (key: Key, value: Value) => void;
5
+ getAll: (key: Key) => Value[];
6
+ has: (key: Key) => boolean;
7
+ }
8
+ export declare const useMultiValueMap: <Key, Value>() => () => MultiValueMap<Key, Value>;
@@ -0,0 +1,40 @@
1
+ import { useRef } from "react";
2
+ export class MultiValueMap {
3
+ constructor() {
4
+ this.dict = new Map();
5
+ this.add = (key, value) => {
6
+ var _a;
7
+ this.dict.set(key, [...((_a = this.dict.get(key)) !== null && _a !== void 0 ? _a : []), value]);
8
+ if (this.dict.has(key)) {
9
+ this.dict.get(key).push(value);
10
+ }
11
+ else {
12
+ this.dict.set(key, [value]);
13
+ }
14
+ };
15
+ this.remove = (key, value) => {
16
+ if (!this.dict.has(key))
17
+ return;
18
+ const array = this.dict.get(key);
19
+ const index = array.indexOf(value);
20
+ if (index !== -1)
21
+ array.splice(index, 1);
22
+ if (array.length === 0)
23
+ this.dict.delete(key);
24
+ };
25
+ this.getAll = (key) => {
26
+ var _a;
27
+ return (_a = this.dict.get(key)) !== null && _a !== void 0 ? _a : [];
28
+ };
29
+ this.has = (key) => this.dict.has(key);
30
+ }
31
+ }
32
+ export const useMultiValueMap = () => {
33
+ const ref = useRef(null);
34
+ return () => {
35
+ if (ref.current)
36
+ return ref.current;
37
+ ref.current = new MultiValueMap();
38
+ return ref.current;
39
+ };
40
+ };
@@ -21,11 +21,21 @@ export declare type FormContextValue = {
21
21
  * Whether or not the form is submitting.
22
22
  */
23
23
  isSubmitting: boolean;
24
+ /**
25
+ * Whether or not the form is valid.
26
+ * This is a shortcut for `Object.keys(fieldErrors).length === 0`.
27
+ */
28
+ isValid: boolean;
24
29
  /**
25
30
  * The default values of the form.
26
31
  */
27
32
  defaultValues?: {
28
33
  [fieldName: string]: any;
29
34
  };
35
+ /**
36
+ * Register a custom focus handler to be used when
37
+ * the field needs to receive focus due to a validation error.
38
+ */
39
+ registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
30
40
  };
31
41
  export declare const FormContext: import("react").Context<FormContextValue>;
@@ -4,4 +4,6 @@ export const FormContext = createContext({
4
4
  clearError: () => { },
5
5
  validateField: () => { },
6
6
  isSubmitting: false,
7
+ isValid: true,
8
+ registerReceiveFocus: () => () => { },
7
9
  });
@@ -4,4 +4,4 @@ import { FieldErrors } from "./validation/types";
4
4
  * The `ValidatedForm` on the frontend will automatically display the errors
5
5
  * if this is returned from the action.
6
6
  */
7
- export declare const validationError: (errors: FieldErrors) => Response;
7
+ export declare const validationError: (errors: FieldErrors, submittedData?: unknown) => Response;
package/browser/server.js CHANGED
@@ -4,4 +4,14 @@ import { json } from "@remix-run/server-runtime";
4
4
  * The `ValidatedForm` on the frontend will automatically display the errors
5
5
  * if this is returned from the action.
6
6
  */
7
- export const validationError = (errors) => json({ fieldErrors: errors }, { status: 422 });
7
+ export const validationError = (errors, submittedData) => {
8
+ if (submittedData) {
9
+ return json({
10
+ fieldErrors: {
11
+ ...errors,
12
+ _submittedData: submittedData,
13
+ },
14
+ }, { status: 422 });
15
+ }
16
+ return json({ fieldErrors: errors }, { status: 422 });
17
+ };
@@ -28,3 +28,4 @@ export declare type Validator<DataType> = {
28
28
  validate: (unvalidatedData: GenericObject) => ValidationResult<DataType>;
29
29
  validateField: (unvalidatedData: GenericObject, field: string) => ValidateFieldResult;
30
30
  };
31
+ export declare type ValidatorData<T extends Validator<any>> = T extends Validator<infer U> ? U : never;
@@ -38,8 +38,13 @@ export declare type FormProps<DataType> = {
38
38
  * and don't redirect in-between submissions.
39
39
  */
40
40
  resetAfterSubmit?: boolean;
41
+ /**
42
+ * Normally, the first invalid input will be focused when the validation fails on form submit.
43
+ * Set this to `false` to disable this behavior.
44
+ */
45
+ disableFocusOnError?: boolean;
41
46
  } & Omit<ComponentProps<typeof RemixForm>, "onSubmit">;
42
47
  /**
43
48
  * The primary form component of `remix-validated-form`.
44
49
  */
45
- export declare function ValidatedForm<DataType>({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, ...rest }: FormProps<DataType>): JSX.Element;
50
+ export declare function ValidatedForm<DataType>({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, disableFocusOnError, ...rest }: FormProps<DataType>): JSX.Element;
@@ -9,6 +9,7 @@ const react_1 = require("@remix-run/react");
9
9
  const react_2 = require("react");
10
10
  const tiny_invariant_1 = __importDefault(require("tiny-invariant"));
11
11
  const formContext_1 = require("./internal/formContext");
12
+ const SingleTypeMultiValueMap_1 = require("./internal/SingleTypeMultiValueMap");
12
13
  const submissionCallbacks_1 = require("./internal/submissionCallbacks");
13
14
  const util_1 = require("./internal/util");
14
15
  function useFieldErrorsFromBackend(fetcher, subaction) {
@@ -70,10 +71,32 @@ function useDefaultValues(fieldErrors, defaultValues) {
70
71
  const defaultsFromValidationError = fieldErrors === null || fieldErrors === void 0 ? void 0 : fieldErrors._submittedData;
71
72
  return defaultsFromValidationError !== null && defaultsFromValidationError !== void 0 ? defaultsFromValidationError : defaultValues;
72
73
  }
74
+ const focusFirstInvalidInput = (fieldErrors, customFocusHandlers, formElement) => {
75
+ const invalidInputSelector = Object.keys(fieldErrors)
76
+ .map((fieldName) => `input[name="${fieldName}"]`)
77
+ .join(",");
78
+ const invalidInputs = formElement.querySelectorAll(invalidInputSelector);
79
+ for (const element of invalidInputs) {
80
+ const input = element;
81
+ if (customFocusHandlers.has(input.name)) {
82
+ customFocusHandlers.getAll(input.name).forEach((handler) => {
83
+ handler();
84
+ });
85
+ break;
86
+ }
87
+ // We don't filter these out ahead of time because
88
+ // they could have a custom focus handler
89
+ if (input.type === "hidden") {
90
+ continue;
91
+ }
92
+ input.focus();
93
+ break;
94
+ }
95
+ };
73
96
  /**
74
97
  * The primary form component of `remix-validated-form`.
75
98
  */
76
- function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, ...rest }) {
99
+ function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, disableFocusOnError, ...rest }) {
77
100
  var _a;
78
101
  const fieldErrorsFromBackend = useFieldErrorsFromBackend(fetcher, subaction);
79
102
  const [fieldErrors, setFieldErrors] = useFieldErrors(fieldErrorsFromBackend);
@@ -86,11 +109,13 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
86
109
  (_a = formRef.current) === null || _a === void 0 ? void 0 : _a.reset();
87
110
  }
88
111
  });
112
+ const customFocusHandlers = (0, SingleTypeMultiValueMap_1.useMultiValueMap)();
89
113
  const contextValue = (0, react_2.useMemo)(() => ({
90
114
  fieldErrors,
91
115
  action,
92
116
  defaultValues: defaultsToUse,
93
117
  isSubmitting: isSubmitting !== null && isSubmitting !== void 0 ? isSubmitting : false,
118
+ isValid: Object.keys(fieldErrors).length === 0,
94
119
  clearError: (fieldName) => {
95
120
  setFieldErrors((prev) => (0, util_1.omit)(prev, fieldName));
96
121
  },
@@ -104,6 +129,12 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
104
129
  }));
105
130
  }
106
131
  },
132
+ registerReceiveFocus: (fieldName, handler) => {
133
+ customFocusHandlers().add(fieldName, handler);
134
+ return () => {
135
+ customFocusHandlers().remove(fieldName, handler);
136
+ };
137
+ },
107
138
  }), [
108
139
  fieldErrors,
109
140
  action,
@@ -111,6 +142,7 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
111
142
  isSubmitting,
112
143
  setFieldErrors,
113
144
  validator,
145
+ customFocusHandlers,
114
146
  ]);
115
147
  const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : react_1.Form;
116
148
  return ((0, jsx_runtime_1.jsx)(Form, { ref: (0, util_1.mergeRefs)([formRef, formRefProp]), ...rest, action: action, onSubmit: (event) => {
@@ -118,6 +150,9 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
118
150
  if (result.error) {
119
151
  event.preventDefault();
120
152
  setFieldErrors(result.error);
153
+ if (!disableFocusOnError) {
154
+ focusFirstInvalidInput(result.error, customFocusHandlers(), formRef.current);
155
+ }
121
156
  }
122
157
  else {
123
158
  onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(result.data, event);
@@ -127,6 +162,6 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
127
162
  if (event.defaultPrevented)
128
163
  return;
129
164
  setFieldErrors({});
130
- }, children: (0, jsx_runtime_1.jsxs)(formContext_1.FormContext.Provider, { value: contextValue, children: [(0, jsx_runtime_1.jsx)("input", { type: "hidden", value: subaction, name: "subaction" }, void 0), children] }, void 0) }, void 0));
165
+ }, children: (0, jsx_runtime_1.jsxs)(formContext_1.FormContext.Provider, { value: contextValue, children: [subaction && ((0, jsx_runtime_1.jsx)("input", { type: "hidden", value: subaction, name: "subaction" }, void 0)), children] }, void 0) }, void 0));
131
166
  }
132
167
  exports.ValidatedForm = ValidatedForm;
package/build/hooks.d.ts CHANGED
@@ -19,7 +19,14 @@ export declare type FieldProps = {
19
19
  /**
20
20
  * Provides the data and helpers necessary to set up a field.
21
21
  */
22
- export declare const useField: (name: string) => FieldProps;
22
+ export declare const useField: (name: string, options?: {
23
+ /**
24
+ * Allows you to configure a custom function that will be called
25
+ * when the input needs to receive focus due to a validation error.
26
+ * This is useful for custom components that use a hidden input.
27
+ */
28
+ handleReceiveFocus?: (() => void) | undefined;
29
+ } | undefined) => FieldProps;
23
30
  /**
24
31
  * Provides access to the entire form context.
25
32
  * This is not usually necessary, but can be useful for advanced use cases.
package/build/hooks.js CHANGED
@@ -11,8 +11,13 @@ const formContext_1 = require("./internal/formContext");
11
11
  /**
12
12
  * Provides the data and helpers necessary to set up a field.
13
13
  */
14
- const useField = (name) => {
15
- const { fieldErrors, clearError, validateField, defaultValues } = (0, react_1.useContext)(formContext_1.FormContext);
14
+ const useField = (name, options) => {
15
+ const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, } = (0, react_1.useContext)(formContext_1.FormContext);
16
+ const { handleReceiveFocus } = options !== null && options !== void 0 ? options : {};
17
+ (0, react_1.useEffect)(() => {
18
+ if (handleReceiveFocus)
19
+ return registerReceiveFocus(name, handleReceiveFocus);
20
+ }, [handleReceiveFocus, name, registerReceiveFocus]);
16
21
  const field = (0, react_1.useMemo)(() => ({
17
22
  error: fieldErrors[name],
18
23
  clearError: () => {
package/build/index.d.ts CHANGED
@@ -2,7 +2,5 @@ export * from "./hooks";
2
2
  export * from "./server";
3
3
  export * from "./ValidatedForm";
4
4
  export * from "./validation/types";
5
- export * from "./validation/withYup";
6
- export * from "./validation/withZod";
7
5
  export * from "./validation/createValidator";
8
6
  export type { FormContextValue } from "./internal/formContext";
package/build/index.js CHANGED
@@ -14,6 +14,4 @@ __exportStar(require("./hooks"), exports);
14
14
  __exportStar(require("./server"), exports);
15
15
  __exportStar(require("./ValidatedForm"), exports);
16
16
  __exportStar(require("./validation/types"), exports);
17
- __exportStar(require("./validation/withYup"), exports);
18
- __exportStar(require("./validation/withZod"), exports);
19
17
  __exportStar(require("./validation/createValidator"), exports);
@@ -0,0 +1,8 @@
1
+ export declare class MultiValueMap<Key, Value> {
2
+ private dict;
3
+ add: (key: Key, value: Value) => void;
4
+ remove: (key: Key, value: Value) => void;
5
+ getAll: (key: Key) => Value[];
6
+ has: (key: Key) => boolean;
7
+ }
8
+ export declare const useMultiValueMap: <Key, Value>() => () => MultiValueMap<Key, Value>;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useMultiValueMap = exports.MultiValueMap = void 0;
4
+ const react_1 = require("react");
5
+ class MultiValueMap {
6
+ constructor() {
7
+ this.dict = new Map();
8
+ this.add = (key, value) => {
9
+ var _a;
10
+ this.dict.set(key, [...((_a = this.dict.get(key)) !== null && _a !== void 0 ? _a : []), value]);
11
+ if (this.dict.has(key)) {
12
+ this.dict.get(key).push(value);
13
+ }
14
+ else {
15
+ this.dict.set(key, [value]);
16
+ }
17
+ };
18
+ this.remove = (key, value) => {
19
+ if (!this.dict.has(key))
20
+ return;
21
+ const array = this.dict.get(key);
22
+ const index = array.indexOf(value);
23
+ if (index !== -1)
24
+ array.splice(index, 1);
25
+ if (array.length === 0)
26
+ this.dict.delete(key);
27
+ };
28
+ this.getAll = (key) => {
29
+ var _a;
30
+ return (_a = this.dict.get(key)) !== null && _a !== void 0 ? _a : [];
31
+ };
32
+ this.has = (key) => this.dict.has(key);
33
+ }
34
+ }
35
+ exports.MultiValueMap = MultiValueMap;
36
+ const useMultiValueMap = () => {
37
+ const ref = (0, react_1.useRef)(null);
38
+ return () => {
39
+ if (ref.current)
40
+ return ref.current;
41
+ ref.current = new MultiValueMap();
42
+ return ref.current;
43
+ };
44
+ };
45
+ exports.useMultiValueMap = useMultiValueMap;
@@ -21,11 +21,21 @@ export declare type FormContextValue = {
21
21
  * Whether or not the form is submitting.
22
22
  */
23
23
  isSubmitting: boolean;
24
+ /**
25
+ * Whether or not the form is valid.
26
+ * This is a shortcut for `Object.keys(fieldErrors).length === 0`.
27
+ */
28
+ isValid: boolean;
24
29
  /**
25
30
  * The default values of the form.
26
31
  */
27
32
  defaultValues?: {
28
33
  [fieldName: string]: any;
29
34
  };
35
+ /**
36
+ * Register a custom focus handler to be used when
37
+ * the field needs to receive focus due to a validation error.
38
+ */
39
+ registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
30
40
  };
31
41
  export declare const FormContext: import("react").Context<FormContextValue>;
@@ -7,4 +7,6 @@ exports.FormContext = (0, react_1.createContext)({
7
7
  clearError: () => { },
8
8
  validateField: () => { },
9
9
  isSubmitting: false,
10
+ isValid: true,
11
+ registerReceiveFocus: () => () => { },
10
12
  });
package/build/server.d.ts CHANGED
@@ -4,4 +4,4 @@ import { FieldErrors } from "./validation/types";
4
4
  * The `ValidatedForm` on the frontend will automatically display the errors
5
5
  * if this is returned from the action.
6
6
  */
7
- export declare const validationError: (errors: FieldErrors) => Response;
7
+ export declare const validationError: (errors: FieldErrors, submittedData?: unknown) => Response;