remix-validated-form 3.0.0-beta.1 → 3.2.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 +41 -13
- package/browser/ValidatedForm.d.ts +6 -1
- package/browser/ValidatedForm.js +52 -1
- package/browser/hooks.d.ts +25 -1
- package/browser/hooks.js +44 -13
- 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 +25 -1
- package/browser/internal/formContext.js +5 -0
- package/browser/internal/getInputProps.d.ts +28 -0
- package/browser/internal/getInputProps.js +38 -0
- package/{build/validation/validation.test.d.ts → browser/internal/test.d.ts} +0 -0
- package/browser/internal/test.js +10 -0
- package/browser/server.d.ts +1 -1
- package/browser/server.js +11 -1
- package/browser/validation/types.d.ts +2 -0
- package/build/ValidatedForm.d.ts +6 -1
- package/build/ValidatedForm.js +52 -1
- package/build/hooks.d.ts +25 -1
- package/build/hooks.js +43 -12
- package/build/internal/SingleTypeMultiValueMap.d.ts +8 -0
- package/build/internal/SingleTypeMultiValueMap.js +45 -0
- package/build/internal/formContext.d.ts +25 -1
- package/build/internal/formContext.js +5 -0
- package/build/internal/getInputProps.d.ts +28 -0
- package/build/internal/getInputProps.js +42 -0
- package/build/internal/test.d.ts +1 -0
- package/build/internal/test.js +12 -0
- package/build/server.d.ts +1 -1
- package/build/server.js +11 -1
- package/build/validation/types.d.ts +2 -0
- package/package.json +1 -3
- package/src/ValidatedForm.tsx +73 -0
- package/src/hooks.ts +77 -9
- package/src/internal/SingleTypeMultiValueMap.ts +37 -0
- package/src/internal/formContext.ts +30 -1
- package/src/internal/getInputProps.ts +79 -0
- package/src/server.ts +18 -2
- package/src/validation/types.ts +8 -0
- package/build/test-data/testFormData.d.ts +0 -15
- package/build/test-data/testFormData.js +0 -50
- 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/.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.1.1 build:browser
|
4
4
|
> tsc --module ESNext --outDir ./browser
|
5
5
|
|
6
6
|
|
7
|
-
> remix-validated-form@
|
7
|
+
> remix-validated-form@3.1.1 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
@@ -10,6 +10,10 @@ A form library built for [remix](https://remix.run) to make validation easy.
|
|
10
10
|
- Supports nested objects and arrays
|
11
11
|
- Validation library agnostic
|
12
12
|
|
13
|
+
# Docs
|
14
|
+
|
15
|
+
The docs are located a [remix-validated-form.io](https://www.remix-validated-form.io).
|
16
|
+
|
13
17
|
# Demo
|
14
18
|
|
15
19
|
https://user-images.githubusercontent.com/2811287/145734901-700a5085-a10b-4d89-88e1-5de9142b1e85.mov
|
@@ -27,10 +31,25 @@ yarn sample-app
|
|
27
31
|
|
28
32
|
## Install
|
29
33
|
|
34
|
+
### Base package
|
35
|
+
|
30
36
|
```bash
|
31
37
|
npm install remix-validated-form
|
32
38
|
```
|
33
39
|
|
40
|
+
### Validation library adapter
|
41
|
+
|
42
|
+
There are official adapters available for `zod` and `yup`.
|
43
|
+
If you're using a different library,
|
44
|
+
see the [Validation library support](#validation-library-support) section below.
|
45
|
+
|
46
|
+
- @remix-validated-form/with-zod
|
47
|
+
- @remix-validated-form/with-yup
|
48
|
+
|
49
|
+
```bash
|
50
|
+
npm install @remix-validated-form/with-zod
|
51
|
+
```
|
52
|
+
|
34
53
|
## Create an input component
|
35
54
|
|
36
55
|
In order to display field errors or do field-by-field validation,
|
@@ -45,18 +64,14 @@ type MyInputProps = {
|
|
45
64
|
};
|
46
65
|
|
47
66
|
export const MyInput = ({ name, label }: InputProps) => {
|
48
|
-
const {
|
67
|
+
const { error, getInputProps } = useField(name);
|
49
68
|
return (
|
50
69
|
<div>
|
51
70
|
<label htmlFor={name}>{label}</label>
|
52
|
-
<input
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
onChange={clearError}
|
57
|
-
defaultValue={defaultValue}
|
58
|
-
/>
|
59
|
-
{error && <span className="my-error-class">{error}</span>}
|
71
|
+
<input {...getInputProps({ id: name })} />
|
72
|
+
{error && (
|
73
|
+
<span className="my-error-class">{error}</span>
|
74
|
+
)}
|
60
75
|
</div>
|
61
76
|
);
|
62
77
|
};
|
@@ -67,18 +82,22 @@ export const MyInput = ({ name, label }: InputProps) => {
|
|
67
82
|
To best take advantage of the per-form submission detection, we can create a submit button component.
|
68
83
|
|
69
84
|
```tsx
|
70
|
-
import { useIsSubmitting } from "remix-validated-form";
|
85
|
+
import { useFormContext, useIsSubmitting } from "remix-validated-form";
|
71
86
|
|
72
87
|
export const MySubmitButton = () => {
|
73
88
|
const isSubmitting = useIsSubmitting();
|
89
|
+
const { isValid } = useFormContext();
|
90
|
+
const disabled = isSubmitting || !isValid;
|
91
|
+
|
74
92
|
return (
|
75
|
-
<button type="submit" disabled={
|
93
|
+
<button type="submit" disabled={disabled} className={disabled ? "disabled-btn" : "btn"}>
|
76
94
|
{isSubmitting ? "Submitting..." : "Submit"}
|
77
95
|
</button>
|
78
96
|
);
|
79
97
|
};
|
80
98
|
```
|
81
99
|
|
100
|
+
|
82
101
|
## Use the form!
|
83
102
|
|
84
103
|
Now that we have our components, making a form is easy!
|
@@ -164,10 +183,10 @@ export default function MyForm() {
|
|
164
183
|
|
165
184
|
# Validation Library Support
|
166
185
|
|
167
|
-
|
186
|
+
There are official adapters available for `zod` and `yup` ,
|
168
187
|
but you can easily support whatever library you want by creating your own adapter.
|
169
188
|
|
170
|
-
And if you create an adapter for a library, feel free to make a PR on this
|
189
|
+
And if you create an adapter for a library, feel free to make a PR on this repository 😊
|
171
190
|
|
172
191
|
## Creating an adapter
|
173
192
|
|
@@ -230,3 +249,12 @@ This is happening because you or the library you are using is passing the `requi
|
|
230
249
|
This library doesn't take care of eliminating them and it's up to the user how they want to manage the validation errors.
|
231
250
|
If you wan't to disable all native HTML validations you can add `noValidate` to `<ValidatedForm>`.
|
232
251
|
We recommend this approach since the validation will still work even if JS is disabled.
|
252
|
+
|
253
|
+
## How do we trigger toast messages on success?
|
254
|
+
|
255
|
+
Problem: how do we trigger a toast message on success if the action redirects away from the form route? The Remix solution is to flash a message in the session and pick this up in a loader function, probably in root.tsx
|
256
|
+
See the [Remix](https://remix.run/docs/en/v1/api/remix#sessionflashkey-value) documentation for more information.
|
257
|
+
|
258
|
+
## Why is my cancel button triggering form submission?
|
259
|
+
Problem: the cancel button has an onClick handler to navigate away from the form route but instead it is submitting the form.
|
260
|
+
A button defaults to `type="submit"` in a form which will submit the form by default. If you want to prevent this you can add `type="reset"` or `type="button"` to the cancel button.
|
@@ -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,15 +65,39 @@ 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);
|
74
97
|
const isSubmitting = useIsSubmitting(action, subaction, fetcher);
|
75
98
|
const defaultsToUse = useDefaultValues(fieldErrorsFromBackend, defaultValues);
|
99
|
+
const [touchedFields, setTouchedFields] = useState({});
|
100
|
+
const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
|
76
101
|
const formRef = useRef(null);
|
77
102
|
useSubmitComplete(isSubmitting, () => {
|
78
103
|
var _a;
|
@@ -80,11 +105,18 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
80
105
|
(_a = formRef.current) === null || _a === void 0 ? void 0 : _a.reset();
|
81
106
|
}
|
82
107
|
});
|
108
|
+
const customFocusHandlers = useMultiValueMap();
|
83
109
|
const contextValue = useMemo(() => ({
|
84
110
|
fieldErrors,
|
85
111
|
action,
|
86
112
|
defaultValues: defaultsToUse,
|
87
113
|
isSubmitting: isSubmitting !== null && isSubmitting !== void 0 ? isSubmitting : false,
|
114
|
+
isValid: Object.keys(fieldErrors).length === 0,
|
115
|
+
touchedFields,
|
116
|
+
setFieldTouched: (fieldName, touched) => setTouchedFields((prev) => ({
|
117
|
+
...prev,
|
118
|
+
[fieldName]: touched,
|
119
|
+
})),
|
88
120
|
clearError: (fieldName) => {
|
89
121
|
setFieldErrors((prev) => omit(prev, fieldName));
|
90
122
|
},
|
@@ -97,21 +129,38 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
97
129
|
[fieldName]: error,
|
98
130
|
}));
|
99
131
|
}
|
132
|
+
else {
|
133
|
+
setFieldErrors((prev) => omit(prev, fieldName));
|
134
|
+
}
|
135
|
+
},
|
136
|
+
registerReceiveFocus: (fieldName, handler) => {
|
137
|
+
customFocusHandlers().add(fieldName, handler);
|
138
|
+
return () => {
|
139
|
+
customFocusHandlers().remove(fieldName, handler);
|
140
|
+
};
|
100
141
|
},
|
142
|
+
hasBeenSubmitted,
|
101
143
|
}), [
|
102
144
|
fieldErrors,
|
103
145
|
action,
|
104
146
|
defaultsToUse,
|
105
147
|
isSubmitting,
|
148
|
+
touchedFields,
|
149
|
+
hasBeenSubmitted,
|
106
150
|
setFieldErrors,
|
107
151
|
validator,
|
152
|
+
customFocusHandlers,
|
108
153
|
]);
|
109
154
|
const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : RemixForm;
|
110
155
|
return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, action: action, onSubmit: (event) => {
|
156
|
+
setHasBeenSubmitted(true);
|
111
157
|
const result = validator.validate(getDataFromForm(event.currentTarget));
|
112
158
|
if (result.error) {
|
113
159
|
event.preventDefault();
|
114
160
|
setFieldErrors(result.error);
|
161
|
+
if (!disableFocusOnError) {
|
162
|
+
focusFirstInvalidInput(result.error, customFocusHandlers(), formRef.current);
|
163
|
+
}
|
115
164
|
}
|
116
165
|
else {
|
117
166
|
onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(result.data, event);
|
@@ -121,5 +170,7 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
121
170
|
if (event.defaultPrevented)
|
122
171
|
return;
|
123
172
|
setFieldErrors({});
|
173
|
+
setTouchedFields({});
|
174
|
+
setHasBeenSubmitted(false);
|
124
175
|
}, children: _jsxs(FormContext.Provider, { value: contextValue, children: [subaction && (_jsx("input", { type: "hidden", value: subaction, name: "subaction" }, void 0)), children] }, void 0) }, void 0));
|
125
176
|
}
|
package/browser/hooks.d.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import { GetInputProps, ValidationBehaviorOptions } from "./internal/getInputProps";
|
1
2
|
export declare type FieldProps = {
|
2
3
|
/**
|
3
4
|
* The validation error message if there is one.
|
@@ -15,11 +16,34 @@ export declare type FieldProps = {
|
|
15
16
|
* The default value of the field, if there is one.
|
16
17
|
*/
|
17
18
|
defaultValue?: any;
|
19
|
+
/**
|
20
|
+
* Whether or not the field has been touched.
|
21
|
+
*/
|
22
|
+
touched: boolean;
|
23
|
+
/**
|
24
|
+
* Helper to set the touched state of the field.
|
25
|
+
*/
|
26
|
+
setTouched: (touched: boolean) => void;
|
27
|
+
/**
|
28
|
+
* Helper to get all the props necessary for a regular input.
|
29
|
+
*/
|
30
|
+
getInputProps: GetInputProps;
|
18
31
|
};
|
19
32
|
/**
|
20
33
|
* Provides the data and helpers necessary to set up a field.
|
21
34
|
*/
|
22
|
-
export declare const useField: (name: string
|
35
|
+
export declare const useField: (name: string, options?: {
|
36
|
+
/**
|
37
|
+
* Allows you to configure a custom function that will be called
|
38
|
+
* when the input needs to receive focus due to a validation error.
|
39
|
+
* This is useful for custom components that use a hidden input.
|
40
|
+
*/
|
41
|
+
handleReceiveFocus?: (() => void) | undefined;
|
42
|
+
/**
|
43
|
+
* Allows you to specify when a field gets validated (when using getInputProps)
|
44
|
+
*/
|
45
|
+
validationBehavior?: Partial<ValidationBehaviorOptions> | undefined;
|
46
|
+
} | undefined) => FieldProps;
|
23
47
|
/**
|
24
48
|
* Provides access to the entire form context.
|
25
49
|
* This is not usually necessary, but can be useful for advanced use cases.
|
package/browser/hooks.js
CHANGED
@@ -1,22 +1,53 @@
|
|
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
|
+
import { createGetInputProps, } from "./internal/getInputProps";
|
5
6
|
/**
|
6
7
|
* Provides the data and helpers necessary to set up a field.
|
7
8
|
*/
|
8
|
-
export const useField = (name) => {
|
9
|
-
const { fieldErrors, clearError, validateField, defaultValues } = useContext(FormContext);
|
10
|
-
const
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
:
|
19
|
-
|
9
|
+
export const useField = (name, options) => {
|
10
|
+
const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, touchedFields, setFieldTouched, hasBeenSubmitted, } = useContext(FormContext);
|
11
|
+
const isTouched = !!touchedFields[name];
|
12
|
+
const { handleReceiveFocus } = options !== null && options !== void 0 ? options : {};
|
13
|
+
useEffect(() => {
|
14
|
+
if (handleReceiveFocus)
|
15
|
+
return registerReceiveFocus(name, handleReceiveFocus);
|
16
|
+
}, [handleReceiveFocus, name, registerReceiveFocus]);
|
17
|
+
const field = useMemo(() => {
|
18
|
+
const helpers = {
|
19
|
+
error: fieldErrors[name],
|
20
|
+
clearError: () => {
|
21
|
+
clearError(name);
|
22
|
+
},
|
23
|
+
validate: () => validateField(name),
|
24
|
+
defaultValue: defaultValues
|
25
|
+
? get(defaultValues, toPath(name), undefined)
|
26
|
+
: undefined,
|
27
|
+
touched: isTouched,
|
28
|
+
setTouched: (touched) => setFieldTouched(name, touched),
|
29
|
+
};
|
30
|
+
const getInputProps = createGetInputProps({
|
31
|
+
...helpers,
|
32
|
+
name,
|
33
|
+
hasBeenSubmitted,
|
34
|
+
validationBehavior: options === null || options === void 0 ? void 0 : options.validationBehavior,
|
35
|
+
});
|
36
|
+
return {
|
37
|
+
...helpers,
|
38
|
+
getInputProps,
|
39
|
+
};
|
40
|
+
}, [
|
41
|
+
fieldErrors,
|
42
|
+
name,
|
43
|
+
defaultValues,
|
44
|
+
isTouched,
|
45
|
+
hasBeenSubmitted,
|
46
|
+
options === null || options === void 0 ? void 0 : options.validationBehavior,
|
47
|
+
clearError,
|
48
|
+
validateField,
|
49
|
+
setFieldTouched,
|
50
|
+
]);
|
20
51
|
return field;
|
21
52
|
};
|
22
53
|
/**
|
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
|
+
};
|
@@ -1,5 +1,5 @@
|
|
1
1
|
/// <reference types="react" />
|
2
|
-
import { FieldErrors } from "../validation/types";
|
2
|
+
import { FieldErrors, TouchedFields } from "../validation/types";
|
3
3
|
export declare type FormContextValue = {
|
4
4
|
/**
|
5
5
|
* All the errors in all the fields in the form.
|
@@ -21,11 +21,35 @@ export declare type FormContextValue = {
|
|
21
21
|
* Whether or not the form is submitting.
|
22
22
|
*/
|
23
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
|
+
* This is a shortcut for `Object.keys(fieldErrors).length === 0`.
|
33
|
+
*/
|
34
|
+
isValid: boolean;
|
24
35
|
/**
|
25
36
|
* The default values of the form.
|
26
37
|
*/
|
27
38
|
defaultValues?: {
|
28
39
|
[fieldName: string]: any;
|
29
40
|
};
|
41
|
+
/**
|
42
|
+
* Register a custom focus handler to be used when
|
43
|
+
* the field needs to receive focus due to a validation error.
|
44
|
+
*/
|
45
|
+
registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
|
46
|
+
/**
|
47
|
+
* Any fields that have been touched by the user.
|
48
|
+
*/
|
49
|
+
touchedFields: TouchedFields;
|
50
|
+
/**
|
51
|
+
* Change the touched state of the specified field.
|
52
|
+
*/
|
53
|
+
setFieldTouched: (fieldName: string, touched: boolean) => void;
|
30
54
|
};
|
31
55
|
export declare const FormContext: import("react").Context<FormContextValue>;
|
@@ -0,0 +1,28 @@
|
|
1
|
+
export declare type ValidationBehavior = "onBlur" | "onChange" | "onSubmit";
|
2
|
+
export declare type ValidationBehaviorOptions = {
|
3
|
+
initial: ValidationBehavior;
|
4
|
+
whenTouched: ValidationBehavior;
|
5
|
+
whenSubmitted: ValidationBehavior;
|
6
|
+
};
|
7
|
+
export declare type CreateGetInputPropsOptions = {
|
8
|
+
clearError: () => void;
|
9
|
+
validate: () => void;
|
10
|
+
defaultValue?: any;
|
11
|
+
touched: boolean;
|
12
|
+
setTouched: (touched: boolean) => void;
|
13
|
+
hasBeenSubmitted: boolean;
|
14
|
+
validationBehavior?: Partial<ValidationBehaviorOptions>;
|
15
|
+
name: string;
|
16
|
+
};
|
17
|
+
export declare type MinimalInputProps = {
|
18
|
+
onChange?: (...args: any[]) => void;
|
19
|
+
onBlur?: (...args: any[]) => void;
|
20
|
+
};
|
21
|
+
export declare type MinimalResult = {
|
22
|
+
name: string;
|
23
|
+
onChange: (...args: any[]) => void;
|
24
|
+
onBlur: (...args: any[]) => void;
|
25
|
+
defaultValue?: any;
|
26
|
+
};
|
27
|
+
export declare type GetInputProps = <T extends {}>(props?: T & MinimalInputProps) => T & MinimalResult;
|
28
|
+
export declare const createGetInputProps: ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }: CreateGetInputPropsOptions) => GetInputProps;
|
@@ -0,0 +1,38 @@
|
|
1
|
+
const defaultValidationBehavior = {
|
2
|
+
initial: "onBlur",
|
3
|
+
whenTouched: "onChange",
|
4
|
+
whenSubmitted: "onChange",
|
5
|
+
};
|
6
|
+
export const createGetInputProps = ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }) => {
|
7
|
+
const validationBehaviors = {
|
8
|
+
...defaultValidationBehavior,
|
9
|
+
...validationBehavior,
|
10
|
+
};
|
11
|
+
return (props = {}) => {
|
12
|
+
const behavior = hasBeenSubmitted
|
13
|
+
? validationBehaviors.whenSubmitted
|
14
|
+
: touched
|
15
|
+
? validationBehaviors.whenTouched
|
16
|
+
: validationBehaviors.initial;
|
17
|
+
return {
|
18
|
+
...props,
|
19
|
+
onChange: (...args) => {
|
20
|
+
var _a;
|
21
|
+
if (behavior === "onChange")
|
22
|
+
validate();
|
23
|
+
else
|
24
|
+
clearError();
|
25
|
+
return (_a = props === null || props === void 0 ? void 0 : props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, ...args);
|
26
|
+
},
|
27
|
+
onBlur: (...args) => {
|
28
|
+
var _a;
|
29
|
+
if (behavior === "onBlur")
|
30
|
+
validate();
|
31
|
+
setTouched(true);
|
32
|
+
return (_a = props === null || props === void 0 ? void 0 : props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, ...args);
|
33
|
+
},
|
34
|
+
defaultValue,
|
35
|
+
name,
|
36
|
+
};
|
37
|
+
};
|
38
|
+
};
|
File without changes
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
2
|
+
import { createGetInputProps } from "./getInputProps";
|
3
|
+
const CompRequired = (props) => null;
|
4
|
+
const getProps = createGetInputProps({});
|
5
|
+
_jsx(CompRequired, { ...getProps({
|
6
|
+
temp: 21,
|
7
|
+
bob: "ross",
|
8
|
+
onBlur: () => { },
|
9
|
+
// onChange: () => {},
|
10
|
+
}) }, void 0);
|
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
|
+
};
|
@@ -1,4 +1,5 @@
|
|
1
1
|
export declare type FieldErrors = Record<string, string>;
|
2
|
+
export declare type TouchedFields = Record<string, boolean>;
|
2
3
|
export declare type FieldErrorsWithData = FieldErrors & {
|
3
4
|
_submittedData: any;
|
4
5
|
};
|
@@ -28,3 +29,4 @@ export declare type Validator<DataType> = {
|
|
28
29
|
validate: (unvalidatedData: GenericObject) => ValidationResult<DataType>;
|
29
30
|
validateField: (unvalidatedData: GenericObject, field: string) => ValidateFieldResult;
|
30
31
|
};
|
32
|
+
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;
|