remix-validated-form 3.1.0 → 3.2.2
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/README.md +24 -11
- package/browser/ValidatedForm.js +30 -4
- package/browser/hooks.d.ts +17 -0
- package/browser/hooks.js +37 -11
- package/browser/internal/MultiValueMap.d.ts +0 -0
- package/browser/internal/MultiValueMap.js +0 -0
- package/browser/internal/SingleTypeMultiValueMap.d.ts +0 -0
- package/browser/internal/SingleTypeMultiValueMap.js +0 -0
- package/browser/internal/formContext.d.ts +15 -1
- package/browser/internal/formContext.js +3 -0
- package/browser/internal/getInputProps.d.ts +21 -0
- package/browser/internal/getInputProps.js +39 -0
- package/browser/internal/test.d.ts +1 -0
- package/browser/internal/test.js +10 -0
- package/browser/validation/types.d.ts +1 -0
- package/build/ValidatedForm.js +30 -4
- package/build/hooks.d.ts +17 -0
- package/build/hooks.js +37 -11
- package/build/internal/SingleTypeMultiValueMap.d.ts +0 -0
- package/build/internal/SingleTypeMultiValueMap.js +0 -0
- package/build/internal/formContext.d.ts +15 -1
- package/build/internal/formContext.js +3 -0
- package/build/internal/getInputProps.d.ts +21 -0
- package/build/internal/getInputProps.js +43 -0
- package/build/internal/test.d.ts +1 -0
- package/build/internal/test.js +12 -0
- package/build/validation/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/ValidatedForm.tsx +30 -4
- package/src/hooks.ts +51 -5
- package/src/internal/formContext.ts +18 -1
- package/src/internal/getInputProps.ts +73 -0
- package/src/validation/types.ts +2 -0
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.
|
3
|
+
> remix-validated-form@3.1.1 build:browser
|
4
4
|
> tsc --module ESNext --outDir ./browser
|
5
5
|
|
6
6
|
|
7
|
-
> remix-validated-form@3.
|
7
|
+
> remix-validated-form@3.1.1 build:main
|
8
8
|
> tsc --module CommonJS --outDir ./build
|
9
9
|
|
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
|
@@ -60,18 +64,14 @@ type MyInputProps = {
|
|
60
64
|
};
|
61
65
|
|
62
66
|
export const MyInput = ({ name, label }: InputProps) => {
|
63
|
-
const {
|
67
|
+
const { error, getInputProps } = useField(name);
|
64
68
|
return (
|
65
69
|
<div>
|
66
70
|
<label htmlFor={name}>{label}</label>
|
67
|
-
<input
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
onChange={clearError}
|
72
|
-
defaultValue={defaultValue}
|
73
|
-
/>
|
74
|
-
{error && <span className="my-error-class">{error}</span>}
|
71
|
+
<input {...getInputProps({ id: name })} />
|
72
|
+
{error && (
|
73
|
+
<span className="my-error-class">{error}</span>
|
74
|
+
)}
|
75
75
|
</div>
|
76
76
|
);
|
77
77
|
};
|
@@ -82,18 +82,22 @@ export const MyInput = ({ name, label }: InputProps) => {
|
|
82
82
|
To best take advantage of the per-form submission detection, we can create a submit button component.
|
83
83
|
|
84
84
|
```tsx
|
85
|
-
import { useIsSubmitting } from "remix-validated-form";
|
85
|
+
import { useFormContext, useIsSubmitting } from "remix-validated-form";
|
86
86
|
|
87
87
|
export const MySubmitButton = () => {
|
88
88
|
const isSubmitting = useIsSubmitting();
|
89
|
+
const { isValid } = useFormContext();
|
90
|
+
const disabled = isSubmitting || !isValid;
|
91
|
+
|
89
92
|
return (
|
90
|
-
<button type="submit" disabled={
|
93
|
+
<button type="submit" disabled={disabled} className={disabled ? "disabled-btn" : "btn"}>
|
91
94
|
{isSubmitting ? "Submitting..." : "Submit"}
|
92
95
|
</button>
|
93
96
|
);
|
94
97
|
};
|
95
98
|
```
|
96
99
|
|
100
|
+
|
97
101
|
## Use the form!
|
98
102
|
|
99
103
|
Now that we have our components, making a form is easy!
|
@@ -245,3 +249,12 @@ This is happening because you or the library you are using is passing the `requi
|
|
245
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.
|
246
250
|
If you wan't to disable all native HTML validations you can add `noValidate` to `<ValidatedForm>`.
|
247
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.
|
package/browser/ValidatedForm.js
CHANGED
@@ -96,6 +96,8 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
96
96
|
const [fieldErrors, setFieldErrors] = useFieldErrors(fieldErrorsFromBackend);
|
97
97
|
const isSubmitting = useIsSubmitting(action, subaction, fetcher);
|
98
98
|
const defaultsToUse = useDefaultValues(fieldErrorsFromBackend, defaultValues);
|
99
|
+
const [touchedFields, setTouchedFields] = useState({});
|
100
|
+
const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
|
99
101
|
const formRef = useRef(null);
|
100
102
|
useSubmitComplete(isSubmitting, () => {
|
101
103
|
var _a;
|
@@ -110,17 +112,35 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
110
112
|
defaultValues: defaultsToUse,
|
111
113
|
isSubmitting: isSubmitting !== null && isSubmitting !== void 0 ? isSubmitting : false,
|
112
114
|
isValid: Object.keys(fieldErrors).length === 0,
|
115
|
+
touchedFields,
|
116
|
+
setFieldTouched: (fieldName, touched) => setTouchedFields((prev) => ({
|
117
|
+
...prev,
|
118
|
+
[fieldName]: touched,
|
119
|
+
})),
|
113
120
|
clearError: (fieldName) => {
|
114
121
|
setFieldErrors((prev) => omit(prev, fieldName));
|
115
122
|
},
|
116
123
|
validateField: (fieldName) => {
|
117
124
|
invariant(formRef.current, "Cannot find reference to form");
|
118
125
|
const { error } = validator.validateField(getDataFromForm(formRef.current), fieldName);
|
126
|
+
// By checking and returning `prev` here, we can avoid a re-render
|
127
|
+
// if the validation state is the same.
|
119
128
|
if (error) {
|
120
|
-
setFieldErrors((prev) =>
|
121
|
-
|
122
|
-
|
123
|
-
|
129
|
+
setFieldErrors((prev) => {
|
130
|
+
if (prev[fieldName] === error)
|
131
|
+
return prev;
|
132
|
+
return {
|
133
|
+
...prev,
|
134
|
+
[fieldName]: error,
|
135
|
+
};
|
136
|
+
});
|
137
|
+
}
|
138
|
+
else {
|
139
|
+
setFieldErrors((prev) => {
|
140
|
+
if (!(fieldName in prev))
|
141
|
+
return prev;
|
142
|
+
return omit(prev, fieldName);
|
143
|
+
});
|
124
144
|
}
|
125
145
|
},
|
126
146
|
registerReceiveFocus: (fieldName, handler) => {
|
@@ -129,17 +149,21 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
129
149
|
customFocusHandlers().remove(fieldName, handler);
|
130
150
|
};
|
131
151
|
},
|
152
|
+
hasBeenSubmitted,
|
132
153
|
}), [
|
133
154
|
fieldErrors,
|
134
155
|
action,
|
135
156
|
defaultsToUse,
|
136
157
|
isSubmitting,
|
158
|
+
touchedFields,
|
159
|
+
hasBeenSubmitted,
|
137
160
|
setFieldErrors,
|
138
161
|
validator,
|
139
162
|
customFocusHandlers,
|
140
163
|
]);
|
141
164
|
const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : RemixForm;
|
142
165
|
return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, action: action, onSubmit: (event) => {
|
166
|
+
setHasBeenSubmitted(true);
|
143
167
|
const result = validator.validate(getDataFromForm(event.currentTarget));
|
144
168
|
if (result.error) {
|
145
169
|
event.preventDefault();
|
@@ -156,5 +180,7 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
156
180
|
if (event.defaultPrevented)
|
157
181
|
return;
|
158
182
|
setFieldErrors({});
|
183
|
+
setTouchedFields({});
|
184
|
+
setHasBeenSubmitted(false);
|
159
185
|
}, children: _jsxs(FormContext.Provider, { value: contextValue, children: [subaction && (_jsx("input", { type: "hidden", value: subaction, name: "subaction" }, void 0)), children] }, void 0) }, void 0));
|
160
186
|
}
|
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,6 +16,18 @@ 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.
|
@@ -26,6 +39,10 @@ export declare const useField: (name: string, options?: {
|
|
26
39
|
* This is useful for custom components that use a hidden input.
|
27
40
|
*/
|
28
41
|
handleReceiveFocus?: (() => void) | undefined;
|
42
|
+
/**
|
43
|
+
* Allows you to specify when a field gets validated (when using getInputProps)
|
44
|
+
*/
|
45
|
+
validationBehavior?: Partial<ValidationBehaviorOptions> | undefined;
|
29
46
|
} | undefined) => FieldProps;
|
30
47
|
/**
|
31
48
|
* Provides access to the entire form context.
|
package/browser/hooks.js
CHANGED
@@ -2,26 +2,52 @@ import get from "lodash/get";
|
|
2
2
|
import toPath from "lodash/toPath";
|
3
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
9
|
export const useField = (name, options) => {
|
9
|
-
const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, } = useContext(FormContext);
|
10
|
+
const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, touchedFields, setFieldTouched, hasBeenSubmitted, } = useContext(FormContext);
|
11
|
+
const isTouched = !!touchedFields[name];
|
10
12
|
const { handleReceiveFocus } = options !== null && options !== void 0 ? options : {};
|
11
13
|
useEffect(() => {
|
12
14
|
if (handleReceiveFocus)
|
13
15
|
return registerReceiveFocus(name, handleReceiveFocus);
|
14
16
|
}, [handleReceiveFocus, name, registerReceiveFocus]);
|
15
|
-
const field = useMemo(() =>
|
16
|
-
|
17
|
-
|
18
|
-
clearError(
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
+
]);
|
25
51
|
return field;
|
26
52
|
};
|
27
53
|
/**
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -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,6 +21,12 @@ 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;
|
24
30
|
/**
|
25
31
|
* Whether or not the form is valid.
|
26
32
|
* This is a shortcut for `Object.keys(fieldErrors).length === 0`.
|
@@ -37,5 +43,13 @@ export declare type FormContextValue = {
|
|
37
43
|
* the field needs to receive focus due to a validation error.
|
38
44
|
*/
|
39
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;
|
40
54
|
};
|
41
55
|
export declare const FormContext: import("react").Context<FormContextValue>;
|
@@ -0,0 +1,21 @@
|
|
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
|
+
declare type HandledProps = "name" | "defaultValue";
|
18
|
+
declare type Callbacks = "onChange" | "onBlur";
|
19
|
+
export declare type GetInputProps = <T extends Record<string, any>>(props?: Omit<T, HandledProps | Callbacks> & Partial<Pick<T, Callbacks>>) => T;
|
20
|
+
export declare const createGetInputProps: ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }: CreateGetInputPropsOptions) => GetInputProps;
|
21
|
+
export {};
|
@@ -0,0 +1,39 @@
|
|
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
|
+
const result = {
|
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
|
+
return result;
|
38
|
+
};
|
39
|
+
};
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -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/build/ValidatedForm.js
CHANGED
@@ -102,6 +102,8 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
102
102
|
const [fieldErrors, setFieldErrors] = useFieldErrors(fieldErrorsFromBackend);
|
103
103
|
const isSubmitting = useIsSubmitting(action, subaction, fetcher);
|
104
104
|
const defaultsToUse = useDefaultValues(fieldErrorsFromBackend, defaultValues);
|
105
|
+
const [touchedFields, setTouchedFields] = (0, react_2.useState)({});
|
106
|
+
const [hasBeenSubmitted, setHasBeenSubmitted] = (0, react_2.useState)(false);
|
105
107
|
const formRef = (0, react_2.useRef)(null);
|
106
108
|
(0, submissionCallbacks_1.useSubmitComplete)(isSubmitting, () => {
|
107
109
|
var _a;
|
@@ -116,17 +118,35 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
116
118
|
defaultValues: defaultsToUse,
|
117
119
|
isSubmitting: isSubmitting !== null && isSubmitting !== void 0 ? isSubmitting : false,
|
118
120
|
isValid: Object.keys(fieldErrors).length === 0,
|
121
|
+
touchedFields,
|
122
|
+
setFieldTouched: (fieldName, touched) => setTouchedFields((prev) => ({
|
123
|
+
...prev,
|
124
|
+
[fieldName]: touched,
|
125
|
+
})),
|
119
126
|
clearError: (fieldName) => {
|
120
127
|
setFieldErrors((prev) => (0, util_1.omit)(prev, fieldName));
|
121
128
|
},
|
122
129
|
validateField: (fieldName) => {
|
123
130
|
(0, tiny_invariant_1.default)(formRef.current, "Cannot find reference to form");
|
124
131
|
const { error } = validator.validateField(getDataFromForm(formRef.current), fieldName);
|
132
|
+
// By checking and returning `prev` here, we can avoid a re-render
|
133
|
+
// if the validation state is the same.
|
125
134
|
if (error) {
|
126
|
-
setFieldErrors((prev) =>
|
127
|
-
|
128
|
-
|
129
|
-
|
135
|
+
setFieldErrors((prev) => {
|
136
|
+
if (prev[fieldName] === error)
|
137
|
+
return prev;
|
138
|
+
return {
|
139
|
+
...prev,
|
140
|
+
[fieldName]: error,
|
141
|
+
};
|
142
|
+
});
|
143
|
+
}
|
144
|
+
else {
|
145
|
+
setFieldErrors((prev) => {
|
146
|
+
if (!(fieldName in prev))
|
147
|
+
return prev;
|
148
|
+
return (0, util_1.omit)(prev, fieldName);
|
149
|
+
});
|
130
150
|
}
|
131
151
|
},
|
132
152
|
registerReceiveFocus: (fieldName, handler) => {
|
@@ -135,17 +155,21 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
135
155
|
customFocusHandlers().remove(fieldName, handler);
|
136
156
|
};
|
137
157
|
},
|
158
|
+
hasBeenSubmitted,
|
138
159
|
}), [
|
139
160
|
fieldErrors,
|
140
161
|
action,
|
141
162
|
defaultsToUse,
|
142
163
|
isSubmitting,
|
164
|
+
touchedFields,
|
165
|
+
hasBeenSubmitted,
|
143
166
|
setFieldErrors,
|
144
167
|
validator,
|
145
168
|
customFocusHandlers,
|
146
169
|
]);
|
147
170
|
const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : react_1.Form;
|
148
171
|
return ((0, jsx_runtime_1.jsx)(Form, { ref: (0, util_1.mergeRefs)([formRef, formRefProp]), ...rest, action: action, onSubmit: (event) => {
|
172
|
+
setHasBeenSubmitted(true);
|
149
173
|
const result = validator.validate(getDataFromForm(event.currentTarget));
|
150
174
|
if (result.error) {
|
151
175
|
event.preventDefault();
|
@@ -162,6 +186,8 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
162
186
|
if (event.defaultPrevented)
|
163
187
|
return;
|
164
188
|
setFieldErrors({});
|
189
|
+
setTouchedFields({});
|
190
|
+
setHasBeenSubmitted(false);
|
165
191
|
}, 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));
|
166
192
|
}
|
167
193
|
exports.ValidatedForm = ValidatedForm;
|
package/build/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,6 +16,18 @@ 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.
|
@@ -26,6 +39,10 @@ export declare const useField: (name: string, options?: {
|
|
26
39
|
* This is useful for custom components that use a hidden input.
|
27
40
|
*/
|
28
41
|
handleReceiveFocus?: (() => void) | undefined;
|
42
|
+
/**
|
43
|
+
* Allows you to specify when a field gets validated (when using getInputProps)
|
44
|
+
*/
|
45
|
+
validationBehavior?: Partial<ValidationBehaviorOptions> | undefined;
|
29
46
|
} | undefined) => FieldProps;
|
30
47
|
/**
|
31
48
|
* Provides access to the entire form context.
|
package/build/hooks.js
CHANGED
@@ -8,26 +8,52 @@ const get_1 = __importDefault(require("lodash/get"));
|
|
8
8
|
const toPath_1 = __importDefault(require("lodash/toPath"));
|
9
9
|
const react_1 = require("react");
|
10
10
|
const formContext_1 = require("./internal/formContext");
|
11
|
+
const getInputProps_1 = require("./internal/getInputProps");
|
11
12
|
/**
|
12
13
|
* Provides the data and helpers necessary to set up a field.
|
13
14
|
*/
|
14
15
|
const useField = (name, options) => {
|
15
|
-
const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, } = (0, react_1.useContext)(formContext_1.FormContext);
|
16
|
+
const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, touchedFields, setFieldTouched, hasBeenSubmitted, } = (0, react_1.useContext)(formContext_1.FormContext);
|
17
|
+
const isTouched = !!touchedFields[name];
|
16
18
|
const { handleReceiveFocus } = options !== null && options !== void 0 ? options : {};
|
17
19
|
(0, react_1.useEffect)(() => {
|
18
20
|
if (handleReceiveFocus)
|
19
21
|
return registerReceiveFocus(name, handleReceiveFocus);
|
20
22
|
}, [handleReceiveFocus, name, registerReceiveFocus]);
|
21
|
-
const field = (0, react_1.useMemo)(() =>
|
22
|
-
|
23
|
-
|
24
|
-
clearError(
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
23
|
+
const field = (0, react_1.useMemo)(() => {
|
24
|
+
const helpers = {
|
25
|
+
error: fieldErrors[name],
|
26
|
+
clearError: () => {
|
27
|
+
clearError(name);
|
28
|
+
},
|
29
|
+
validate: () => validateField(name),
|
30
|
+
defaultValue: defaultValues
|
31
|
+
? (0, get_1.default)(defaultValues, (0, toPath_1.default)(name), undefined)
|
32
|
+
: undefined,
|
33
|
+
touched: isTouched,
|
34
|
+
setTouched: (touched) => setFieldTouched(name, touched),
|
35
|
+
};
|
36
|
+
const getInputProps = (0, getInputProps_1.createGetInputProps)({
|
37
|
+
...helpers,
|
38
|
+
name,
|
39
|
+
hasBeenSubmitted,
|
40
|
+
validationBehavior: options === null || options === void 0 ? void 0 : options.validationBehavior,
|
41
|
+
});
|
42
|
+
return {
|
43
|
+
...helpers,
|
44
|
+
getInputProps,
|
45
|
+
};
|
46
|
+
}, [
|
47
|
+
fieldErrors,
|
48
|
+
name,
|
49
|
+
defaultValues,
|
50
|
+
isTouched,
|
51
|
+
hasBeenSubmitted,
|
52
|
+
options === null || options === void 0 ? void 0 : options.validationBehavior,
|
53
|
+
clearError,
|
54
|
+
validateField,
|
55
|
+
setFieldTouched,
|
56
|
+
]);
|
31
57
|
return field;
|
32
58
|
};
|
33
59
|
exports.useField = useField;
|
File without changes
|
File without changes
|
@@ -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,6 +21,12 @@ 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;
|
24
30
|
/**
|
25
31
|
* Whether or not the form is valid.
|
26
32
|
* This is a shortcut for `Object.keys(fieldErrors).length === 0`.
|
@@ -37,5 +43,13 @@ export declare type FormContextValue = {
|
|
37
43
|
* the field needs to receive focus due to a validation error.
|
38
44
|
*/
|
39
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;
|
40
54
|
};
|
41
55
|
export declare const FormContext: import("react").Context<FormContextValue>;
|
@@ -7,6 +7,9 @@ exports.FormContext = (0, react_1.createContext)({
|
|
7
7
|
clearError: () => { },
|
8
8
|
validateField: () => { },
|
9
9
|
isSubmitting: false,
|
10
|
+
hasBeenSubmitted: false,
|
10
11
|
isValid: true,
|
11
12
|
registerReceiveFocus: () => () => { },
|
13
|
+
touchedFields: {},
|
14
|
+
setFieldTouched: () => { },
|
12
15
|
});
|
@@ -0,0 +1,21 @@
|
|
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
|
+
declare type HandledProps = "name" | "defaultValue";
|
18
|
+
declare type Callbacks = "onChange" | "onBlur";
|
19
|
+
export declare type GetInputProps = <T extends Record<string, any>>(props?: Omit<T, HandledProps | Callbacks> & Partial<Pick<T, Callbacks>>) => T;
|
20
|
+
export declare const createGetInputProps: ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }: CreateGetInputPropsOptions) => GetInputProps;
|
21
|
+
export {};
|
@@ -0,0 +1,43 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.createGetInputProps = void 0;
|
4
|
+
const defaultValidationBehavior = {
|
5
|
+
initial: "onBlur",
|
6
|
+
whenTouched: "onChange",
|
7
|
+
whenSubmitted: "onChange",
|
8
|
+
};
|
9
|
+
const createGetInputProps = ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }) => {
|
10
|
+
const validationBehaviors = {
|
11
|
+
...defaultValidationBehavior,
|
12
|
+
...validationBehavior,
|
13
|
+
};
|
14
|
+
return (props = {}) => {
|
15
|
+
const behavior = hasBeenSubmitted
|
16
|
+
? validationBehaviors.whenSubmitted
|
17
|
+
: touched
|
18
|
+
? validationBehaviors.whenTouched
|
19
|
+
: validationBehaviors.initial;
|
20
|
+
const result = {
|
21
|
+
...props,
|
22
|
+
onChange: (...args) => {
|
23
|
+
var _a;
|
24
|
+
if (behavior === "onChange")
|
25
|
+
validate();
|
26
|
+
else
|
27
|
+
clearError();
|
28
|
+
return (_a = props === null || props === void 0 ? void 0 : props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, ...args);
|
29
|
+
},
|
30
|
+
onBlur: (...args) => {
|
31
|
+
var _a;
|
32
|
+
if (behavior === "onBlur")
|
33
|
+
validate();
|
34
|
+
setTouched(true);
|
35
|
+
return (_a = props === null || props === void 0 ? void 0 : props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, ...args);
|
36
|
+
},
|
37
|
+
defaultValue,
|
38
|
+
name,
|
39
|
+
};
|
40
|
+
return result;
|
41
|
+
};
|
42
|
+
};
|
43
|
+
exports.createGetInputProps = createGetInputProps;
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
4
|
+
const getInputProps_1 = require("./getInputProps");
|
5
|
+
const CompRequired = (props) => null;
|
6
|
+
const getProps = (0, getInputProps_1.createGetInputProps)({});
|
7
|
+
(0, jsx_runtime_1.jsx)(CompRequired, { ...getProps({
|
8
|
+
temp: 21,
|
9
|
+
bob: "ross",
|
10
|
+
onBlur: () => { },
|
11
|
+
// onChange: () => {},
|
12
|
+
}) }, void 0);
|
package/package.json
CHANGED
package/src/ValidatedForm.tsx
CHANGED
@@ -24,6 +24,7 @@ import {
|
|
24
24
|
FieldErrors,
|
25
25
|
Validator,
|
26
26
|
FieldErrorsWithData,
|
27
|
+
TouchedFields,
|
27
28
|
} from "./validation/types";
|
28
29
|
|
29
30
|
export type FormProps<DataType> = {
|
@@ -196,6 +197,8 @@ export function ValidatedForm<DataType>({
|
|
196
197
|
const [fieldErrors, setFieldErrors] = useFieldErrors(fieldErrorsFromBackend);
|
197
198
|
const isSubmitting = useIsSubmitting(action, subaction, fetcher);
|
198
199
|
const defaultsToUse = useDefaultValues(fieldErrorsFromBackend, defaultValues);
|
200
|
+
const [touchedFields, setTouchedFields] = useState<TouchedFields>({});
|
201
|
+
const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
|
199
202
|
const formRef = useRef<HTMLFormElement>(null);
|
200
203
|
useSubmitComplete(isSubmitting, () => {
|
201
204
|
if (!fieldErrorsFromBackend && resetAfterSubmit) {
|
@@ -211,6 +214,12 @@ export function ValidatedForm<DataType>({
|
|
211
214
|
defaultValues: defaultsToUse,
|
212
215
|
isSubmitting: isSubmitting ?? false,
|
213
216
|
isValid: Object.keys(fieldErrors).length === 0,
|
217
|
+
touchedFields,
|
218
|
+
setFieldTouched: (fieldName: string, touched: boolean) =>
|
219
|
+
setTouchedFields((prev) => ({
|
220
|
+
...prev,
|
221
|
+
[fieldName]: touched,
|
222
|
+
})),
|
214
223
|
clearError: (fieldName) => {
|
215
224
|
setFieldErrors((prev) => omit(prev, fieldName));
|
216
225
|
},
|
@@ -220,11 +229,22 @@ export function ValidatedForm<DataType>({
|
|
220
229
|
getDataFromForm(formRef.current),
|
221
230
|
fieldName as any
|
222
231
|
);
|
232
|
+
|
233
|
+
// By checking and returning `prev` here, we can avoid a re-render
|
234
|
+
// if the validation state is the same.
|
223
235
|
if (error) {
|
224
|
-
setFieldErrors((prev) =>
|
225
|
-
|
226
|
-
|
227
|
-
|
236
|
+
setFieldErrors((prev) => {
|
237
|
+
if (prev[fieldName] === error) return prev;
|
238
|
+
return {
|
239
|
+
...prev,
|
240
|
+
[fieldName]: error,
|
241
|
+
};
|
242
|
+
});
|
243
|
+
} else {
|
244
|
+
setFieldErrors((prev) => {
|
245
|
+
if (!(fieldName in prev)) return prev;
|
246
|
+
return omit(prev, fieldName);
|
247
|
+
});
|
228
248
|
}
|
229
249
|
},
|
230
250
|
registerReceiveFocus: (fieldName, handler) => {
|
@@ -233,12 +253,15 @@ export function ValidatedForm<DataType>({
|
|
233
253
|
customFocusHandlers().remove(fieldName, handler);
|
234
254
|
};
|
235
255
|
},
|
256
|
+
hasBeenSubmitted,
|
236
257
|
}),
|
237
258
|
[
|
238
259
|
fieldErrors,
|
239
260
|
action,
|
240
261
|
defaultsToUse,
|
241
262
|
isSubmitting,
|
263
|
+
touchedFields,
|
264
|
+
hasBeenSubmitted,
|
242
265
|
setFieldErrors,
|
243
266
|
validator,
|
244
267
|
customFocusHandlers,
|
@@ -253,6 +276,7 @@ export function ValidatedForm<DataType>({
|
|
253
276
|
{...rest}
|
254
277
|
action={action}
|
255
278
|
onSubmit={(event) => {
|
279
|
+
setHasBeenSubmitted(true);
|
256
280
|
const result = validator.validate(getDataFromForm(event.currentTarget));
|
257
281
|
if (result.error) {
|
258
282
|
event.preventDefault();
|
@@ -272,6 +296,8 @@ export function ValidatedForm<DataType>({
|
|
272
296
|
onReset?.(event);
|
273
297
|
if (event.defaultPrevented) return;
|
274
298
|
setFieldErrors({});
|
299
|
+
setTouchedFields({});
|
300
|
+
setHasBeenSubmitted(false);
|
275
301
|
}}
|
276
302
|
>
|
277
303
|
<FormContext.Provider value={contextValue}>
|
package/src/hooks.ts
CHANGED
@@ -2,6 +2,11 @@ import get from "lodash/get";
|
|
2
2
|
import toPath from "lodash/toPath";
|
3
3
|
import { useContext, useEffect, useMemo } from "react";
|
4
4
|
import { FormContext } from "./internal/formContext";
|
5
|
+
import {
|
6
|
+
createGetInputProps,
|
7
|
+
GetInputProps,
|
8
|
+
ValidationBehaviorOptions,
|
9
|
+
} from "./internal/getInputProps";
|
5
10
|
|
6
11
|
export type FieldProps = {
|
7
12
|
/**
|
@@ -20,6 +25,18 @@ export type FieldProps = {
|
|
20
25
|
* The default value of the field, if there is one.
|
21
26
|
*/
|
22
27
|
defaultValue?: any;
|
28
|
+
/**
|
29
|
+
* Whether or not the field has been touched.
|
30
|
+
*/
|
31
|
+
touched: boolean;
|
32
|
+
/**
|
33
|
+
* Helper to set the touched state of the field.
|
34
|
+
*/
|
35
|
+
setTouched: (touched: boolean) => void;
|
36
|
+
/**
|
37
|
+
* Helper to get all the props necessary for a regular input.
|
38
|
+
*/
|
39
|
+
getInputProps: GetInputProps;
|
23
40
|
};
|
24
41
|
|
25
42
|
/**
|
@@ -34,6 +51,10 @@ export const useField = (
|
|
34
51
|
* This is useful for custom components that use a hidden input.
|
35
52
|
*/
|
36
53
|
handleReceiveFocus?: () => void;
|
54
|
+
/**
|
55
|
+
* Allows you to specify when a field gets validated (when using getInputProps)
|
56
|
+
*/
|
57
|
+
validationBehavior?: Partial<ValidationBehaviorOptions>;
|
37
58
|
}
|
38
59
|
): FieldProps => {
|
39
60
|
const {
|
@@ -42,8 +63,12 @@ export const useField = (
|
|
42
63
|
validateField,
|
43
64
|
defaultValues,
|
44
65
|
registerReceiveFocus,
|
66
|
+
touchedFields,
|
67
|
+
setFieldTouched,
|
68
|
+
hasBeenSubmitted,
|
45
69
|
} = useContext(FormContext);
|
46
70
|
|
71
|
+
const isTouched = !!touchedFields[name];
|
47
72
|
const { handleReceiveFocus } = options ?? {};
|
48
73
|
|
49
74
|
useEffect(() => {
|
@@ -51,8 +76,8 @@ export const useField = (
|
|
51
76
|
return registerReceiveFocus(name, handleReceiveFocus);
|
52
77
|
}, [handleReceiveFocus, name, registerReceiveFocus]);
|
53
78
|
|
54
|
-
const field = useMemo<FieldProps>(
|
55
|
-
|
79
|
+
const field = useMemo<FieldProps>(() => {
|
80
|
+
const helpers = {
|
56
81
|
error: fieldErrors[name],
|
57
82
|
clearError: () => {
|
58
83
|
clearError(name);
|
@@ -61,9 +86,30 @@ export const useField = (
|
|
61
86
|
defaultValue: defaultValues
|
62
87
|
? get(defaultValues, toPath(name), undefined)
|
63
88
|
: undefined,
|
64
|
-
|
65
|
-
|
66
|
-
|
89
|
+
touched: isTouched,
|
90
|
+
setTouched: (touched: boolean) => setFieldTouched(name, touched),
|
91
|
+
};
|
92
|
+
const getInputProps = createGetInputProps({
|
93
|
+
...helpers,
|
94
|
+
name,
|
95
|
+
hasBeenSubmitted,
|
96
|
+
validationBehavior: options?.validationBehavior,
|
97
|
+
});
|
98
|
+
return {
|
99
|
+
...helpers,
|
100
|
+
getInputProps,
|
101
|
+
};
|
102
|
+
}, [
|
103
|
+
fieldErrors,
|
104
|
+
name,
|
105
|
+
defaultValues,
|
106
|
+
isTouched,
|
107
|
+
hasBeenSubmitted,
|
108
|
+
options?.validationBehavior,
|
109
|
+
clearError,
|
110
|
+
validateField,
|
111
|
+
setFieldTouched,
|
112
|
+
]);
|
67
113
|
|
68
114
|
return field;
|
69
115
|
};
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { createContext } from "react";
|
2
|
-
import { FieldErrors } from "../validation/types";
|
2
|
+
import { FieldErrors, TouchedFields } from "../validation/types";
|
3
3
|
|
4
4
|
export type FormContextValue = {
|
5
5
|
/**
|
@@ -22,6 +22,12 @@ export type FormContextValue = {
|
|
22
22
|
* Whether or not the form is submitting.
|
23
23
|
*/
|
24
24
|
isSubmitting: boolean;
|
25
|
+
/**
|
26
|
+
* Whether or not a submission has been attempted.
|
27
|
+
* This is true once the form has been submitted, even if there were validation errors.
|
28
|
+
* Resets to false when the form is reset.
|
29
|
+
*/
|
30
|
+
hasBeenSubmitted: boolean;
|
25
31
|
/**
|
26
32
|
* Whether or not the form is valid.
|
27
33
|
* This is a shortcut for `Object.keys(fieldErrors).length === 0`.
|
@@ -36,6 +42,14 @@ export type FormContextValue = {
|
|
36
42
|
* the field needs to receive focus due to a validation error.
|
37
43
|
*/
|
38
44
|
registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
|
45
|
+
/**
|
46
|
+
* Any fields that have been touched by the user.
|
47
|
+
*/
|
48
|
+
touchedFields: TouchedFields;
|
49
|
+
/**
|
50
|
+
* Change the touched state of the specified field.
|
51
|
+
*/
|
52
|
+
setFieldTouched: (fieldName: string, touched: boolean) => void;
|
39
53
|
};
|
40
54
|
|
41
55
|
export const FormContext = createContext<FormContextValue>({
|
@@ -43,6 +57,9 @@ export const FormContext = createContext<FormContextValue>({
|
|
43
57
|
clearError: () => {},
|
44
58
|
validateField: () => {},
|
45
59
|
isSubmitting: false,
|
60
|
+
hasBeenSubmitted: false,
|
46
61
|
isValid: true,
|
47
62
|
registerReceiveFocus: () => () => {},
|
63
|
+
touchedFields: {},
|
64
|
+
setFieldTouched: () => {},
|
48
65
|
});
|
@@ -0,0 +1,73 @@
|
|
1
|
+
export type ValidationBehavior = "onBlur" | "onChange" | "onSubmit";
|
2
|
+
|
3
|
+
export type ValidationBehaviorOptions = {
|
4
|
+
initial: ValidationBehavior;
|
5
|
+
whenTouched: ValidationBehavior;
|
6
|
+
whenSubmitted: ValidationBehavior;
|
7
|
+
};
|
8
|
+
|
9
|
+
export type CreateGetInputPropsOptions = {
|
10
|
+
clearError: () => void;
|
11
|
+
validate: () => void;
|
12
|
+
defaultValue?: any;
|
13
|
+
touched: boolean;
|
14
|
+
setTouched: (touched: boolean) => void;
|
15
|
+
hasBeenSubmitted: boolean;
|
16
|
+
validationBehavior?: Partial<ValidationBehaviorOptions>;
|
17
|
+
name: string;
|
18
|
+
};
|
19
|
+
|
20
|
+
type HandledProps = "name" | "defaultValue";
|
21
|
+
type Callbacks = "onChange" | "onBlur";
|
22
|
+
|
23
|
+
export type GetInputProps = <T extends Record<string, any>>(
|
24
|
+
props?: Omit<T, HandledProps | Callbacks> & Partial<Pick<T, Callbacks>>
|
25
|
+
) => T;
|
26
|
+
|
27
|
+
const defaultValidationBehavior: ValidationBehaviorOptions = {
|
28
|
+
initial: "onBlur",
|
29
|
+
whenTouched: "onChange",
|
30
|
+
whenSubmitted: "onChange",
|
31
|
+
};
|
32
|
+
|
33
|
+
export const createGetInputProps = ({
|
34
|
+
clearError,
|
35
|
+
validate,
|
36
|
+
defaultValue,
|
37
|
+
touched,
|
38
|
+
setTouched,
|
39
|
+
hasBeenSubmitted,
|
40
|
+
validationBehavior,
|
41
|
+
name,
|
42
|
+
}: CreateGetInputPropsOptions): GetInputProps => {
|
43
|
+
const validationBehaviors = {
|
44
|
+
...defaultValidationBehavior,
|
45
|
+
...validationBehavior,
|
46
|
+
};
|
47
|
+
|
48
|
+
return <T extends Record<string, any>>(props = {} as any) => {
|
49
|
+
const behavior = hasBeenSubmitted
|
50
|
+
? validationBehaviors.whenSubmitted
|
51
|
+
: touched
|
52
|
+
? validationBehaviors.whenTouched
|
53
|
+
: validationBehaviors.initial;
|
54
|
+
|
55
|
+
const result: T = {
|
56
|
+
...props,
|
57
|
+
onChange: (...args: unknown[]) => {
|
58
|
+
if (behavior === "onChange") validate();
|
59
|
+
else clearError();
|
60
|
+
return props?.onChange?.(...args);
|
61
|
+
},
|
62
|
+
onBlur: (...args: unknown[]) => {
|
63
|
+
if (behavior === "onBlur") validate();
|
64
|
+
setTouched(true);
|
65
|
+
return props?.onBlur?.(...args);
|
66
|
+
},
|
67
|
+
defaultValue,
|
68
|
+
name,
|
69
|
+
};
|
70
|
+
|
71
|
+
return result;
|
72
|
+
};
|
73
|
+
};
|
package/src/validation/types.ts
CHANGED