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.
- package/.turbo/turbo-build.log +2 -2
- package/.turbo/turbo-test.log +10 -35
- package/README.md +20 -8
- package/browser/ValidatedForm.d.ts +6 -1
- package/browser/ValidatedForm.js +37 -2
- package/browser/hooks.d.ts +8 -1
- package/browser/hooks.js +8 -3
- package/browser/index.d.ts +0 -2
- package/browser/index.js +0 -2
- package/browser/internal/MultiValueMap.d.ts +0 -0
- package/browser/internal/MultiValueMap.js +1 -0
- package/browser/internal/SingleTypeMultiValueMap.d.ts +8 -0
- package/browser/internal/SingleTypeMultiValueMap.js +40 -0
- package/browser/internal/formContext.d.ts +10 -0
- package/browser/internal/formContext.js +2 -0
- package/browser/server.d.ts +1 -1
- package/browser/server.js +11 -1
- package/browser/validation/types.d.ts +1 -0
- package/build/ValidatedForm.d.ts +6 -1
- package/build/ValidatedForm.js +37 -2
- package/build/hooks.d.ts +8 -1
- package/build/hooks.js +7 -2
- package/build/index.d.ts +0 -2
- package/build/index.js +0 -2
- package/build/internal/SingleTypeMultiValueMap.d.ts +8 -0
- package/build/internal/SingleTypeMultiValueMap.js +45 -0
- package/build/internal/formContext.d.ts +10 -0
- package/build/internal/formContext.js +2 -0
- package/build/server.d.ts +1 -1
- package/build/server.js +11 -1
- package/build/validation/types.d.ts +1 -0
- package/package.json +3 -8
- package/src/ValidatedForm.tsx +59 -1
- package/src/hooks.ts +26 -4
- package/src/index.ts +0 -2
- package/src/internal/SingleTypeMultiValueMap.ts +37 -0
- package/src/internal/formContext.ts +12 -0
- package/src/server.ts +18 -2
- package/src/validation/types.ts +6 -0
- package/build/test-data/testFormData.d.ts +0 -15
- package/build/test-data/testFormData.js +0 -50
- package/build/validation/validation.test.d.ts +0 -1
- package/build/validation/validation.test.js +0 -295
- package/build/validation/withYup.d.ts +0 -6
- package/build/validation/withYup.js +0 -44
- package/build/validation/withZod.d.ts +0 -6
- package/build/validation/withZod.js +0 -57
- package/jest.config.js +0 -10
- package/src/test-data/testFormData.ts +0 -55
- package/src/validation/validation.test.ts +0 -322
- package/src/validation/withYup.ts +0 -43
- package/src/validation/withZod.ts +0 -51
package/.turbo/turbo-build.log
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
[2K[1G[2m$ npm run build:browser && npm run build:main[22m
|
2
2
|
|
3
|
-
> remix-validated-form@
|
3
|
+
> remix-validated-form@3.0.0 build:browser
|
4
4
|
> tsc --module ESNext --outDir ./browser
|
5
5
|
|
6
6
|
|
7
|
-
> remix-validated-form@
|
7
|
+
> remix-validated-form@3.0.0 build:main
|
8
8
|
> tsc --module CommonJS --outDir ./build
|
9
9
|
|
package/.turbo/turbo-test.log
CHANGED
@@ -1,36 +1,11 @@
|
|
1
1
|
[2K[1G[2m$ jest src[22m
|
2
|
-
[
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
[32m✓[39m [2mshould not return an error if field is valid[22m
|
13
|
-
[32m✓[39m [2mshould not return an error if a nested field is valid (1 ms)[22m
|
14
|
-
[32m✓[39m [2mshould return an error if field is invalid (2 ms)[22m
|
15
|
-
[32m✓[39m [2mshould return an error if a nested field is invalid[22m
|
16
|
-
Adapter for zod
|
17
|
-
validate
|
18
|
-
[32m✓[39m [2mshould return the data when valid (1 ms)[22m
|
19
|
-
[32m✓[39m [2mshould return field errors when invalid[22m
|
20
|
-
[32m✓[39m [2mshould unflatten data when validating (1 ms)[22m
|
21
|
-
[32m✓[39m [2mshould accept FormData directly and return errors[22m
|
22
|
-
[32m✓[39m [2mshould accept FormData directly and return valid data[22m
|
23
|
-
validateField
|
24
|
-
[32m✓[39m [2mshould not return an error if field is valid (1 ms)[22m
|
25
|
-
[32m✓[39m [2mshould not return an error if a nested field is valid[22m
|
26
|
-
[32m✓[39m [2mshould return an error if field is invalid[22m
|
27
|
-
[32m✓[39m [2mshould return an error if a nested field is invalid[22m
|
28
|
-
withZod
|
29
|
-
[32m✓[39m [2mreturns coherent errors for complex schemas (1 ms)[22m
|
30
|
-
[32m✓[39m [2mreturns errors for fields that are unions[22m
|
31
|
-
|
32
|
-
[1mTest Suites: [22m[1m[32m1 passed[39m[22m, 1 total
|
33
|
-
[1mTests: [22m[1m[32m20 passed[39m[22m, 20 total
|
34
|
-
[1mSnapshots: [22m0 total
|
35
|
-
[1mTime:[22m 1.1 s, estimated 2 s
|
36
|
-
[2mRan all test suites[22m[2m matching [22m/src/i[2m.[22m
|
2
|
+
[1mNo tests found, exiting with code 1[22m
|
3
|
+
Run with `--passWithNoTests` to exit with code 0
|
4
|
+
In [1m/Users/aaronpettengill/dev/remix-validated-form/packages/remix-validated-form[22m
|
5
|
+
68 files checked.
|
6
|
+
testMatch: [33m**/__tests__/**/*.[jt]s?(x), **/?(*.)+(spec|test).[tj]s?(x)[39m - 2 matches
|
7
|
+
testPathIgnorePatterns: [33m/node_modules/[39m - 68 matches
|
8
|
+
testRegex: - 0 matches
|
9
|
+
Pattern: [33msrc[39m - 0 matches
|
10
|
+
[2K[1G[34minfo[39m Visit [1mhttps://yarnpkg.com/en/docs/cli/run[22m for documentation about this command.
|
11
|
+
[2K[1G[31merror[39m 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
|
-
|
23
|
-
|
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
|
-
|
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
|
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;
|
package/browser/ValidatedForm.js
CHANGED
@@ -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
|
}
|
package/browser/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
|
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: () => {
|
package/browser/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/browser/index.js
CHANGED
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>;
|
package/browser/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;
|
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) =>
|
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;
|
package/build/ValidatedForm.d.ts
CHANGED
@@ -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;
|
package/build/ValidatedForm.js
CHANGED
@@ -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
|
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>;
|
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;
|