remix-validated-form 3.1.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/README.md +10 -10
- package/browser/ValidatedForm.js +16 -0
- package/browser/hooks.d.ts +17 -0
- package/browser/hooks.js +37 -11
- package/browser/internal/formContext.d.ts +15 -1
- package/browser/internal/formContext.js +3 -0
- package/browser/internal/getInputProps.d.ts +28 -0
- package/browser/internal/getInputProps.js +38 -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 +16 -0
- package/build/hooks.d.ts +17 -0
- package/build/hooks.js +37 -11
- package/build/internal/formContext.d.ts +15 -1
- package/build/internal/formContext.js +3 -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/validation/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/ValidatedForm.tsx +17 -0
- package/src/hooks.ts +51 -5
- package/src/internal/formContext.ts +18 -1
- package/src/internal/getInputProps.ts +79 -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.1.
|
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.1.
|
7
|
+
> remix-validated-form@3.1.1 build:main
|
8
8
|
> tsc --module CommonJS --outDir ./build
|
9
9
|
|
package/README.md
CHANGED
@@ -64,18 +64,14 @@ type MyInputProps = {
|
|
64
64
|
};
|
65
65
|
|
66
66
|
export const MyInput = ({ name, label }: InputProps) => {
|
67
|
-
const {
|
67
|
+
const { error, getInputProps } = useField(name);
|
68
68
|
return (
|
69
69
|
<div>
|
70
70
|
<label htmlFor={name}>{label}</label>
|
71
|
-
<input
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
onChange={clearError}
|
76
|
-
defaultValue={defaultValue}
|
77
|
-
/>
|
78
|
-
{error && <span className="my-error-class">{error}</span>}
|
71
|
+
<input {...getInputProps({ id: name })} />
|
72
|
+
{error && (
|
73
|
+
<span className="my-error-class">{error}</span>
|
74
|
+
)}
|
79
75
|
</div>
|
80
76
|
);
|
81
77
|
};
|
@@ -257,4 +253,8 @@ We recommend this approach since the validation will still work even if JS is di
|
|
257
253
|
## How do we trigger toast messages on success?
|
258
254
|
|
259
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
|
260
|
-
See the [Remix](https://remix.run/docs/en/v1/api/remix#sessionflashkey-value) documentation for more information.
|
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,6 +112,11 @@ 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
|
},
|
@@ -122,6 +129,9 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
122
129
|
[fieldName]: error,
|
123
130
|
}));
|
124
131
|
}
|
132
|
+
else {
|
133
|
+
setFieldErrors((prev) => omit(prev, fieldName));
|
134
|
+
}
|
125
135
|
},
|
126
136
|
registerReceiveFocus: (fieldName, handler) => {
|
127
137
|
customFocusHandlers().add(fieldName, handler);
|
@@ -129,17 +139,21 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
129
139
|
customFocusHandlers().remove(fieldName, handler);
|
130
140
|
};
|
131
141
|
},
|
142
|
+
hasBeenSubmitted,
|
132
143
|
}), [
|
133
144
|
fieldErrors,
|
134
145
|
action,
|
135
146
|
defaultsToUse,
|
136
147
|
isSubmitting,
|
148
|
+
touchedFields,
|
149
|
+
hasBeenSubmitted,
|
137
150
|
setFieldErrors,
|
138
151
|
validator,
|
139
152
|
customFocusHandlers,
|
140
153
|
]);
|
141
154
|
const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : RemixForm;
|
142
155
|
return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, action: action, onSubmit: (event) => {
|
156
|
+
setHasBeenSubmitted(true);
|
143
157
|
const result = validator.validate(getDataFromForm(event.currentTarget));
|
144
158
|
if (result.error) {
|
145
159
|
event.preventDefault();
|
@@ -156,5 +170,7 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
156
170
|
if (event.defaultPrevented)
|
157
171
|
return;
|
158
172
|
setFieldErrors({});
|
173
|
+
setTouchedFields({});
|
174
|
+
setHasBeenSubmitted(false);
|
159
175
|
}, children: _jsxs(FormContext.Provider, { value: contextValue, children: [subaction && (_jsx("input", { type: "hidden", value: subaction, name: "subaction" }, void 0)), children] }, void 0) }, void 0));
|
160
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,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
|
/**
|
@@ -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,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
|
+
};
|
@@ -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,6 +118,11 @@ 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
|
},
|
@@ -128,6 +135,9 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
128
135
|
[fieldName]: error,
|
129
136
|
}));
|
130
137
|
}
|
138
|
+
else {
|
139
|
+
setFieldErrors((prev) => (0, util_1.omit)(prev, fieldName));
|
140
|
+
}
|
131
141
|
},
|
132
142
|
registerReceiveFocus: (fieldName, handler) => {
|
133
143
|
customFocusHandlers().add(fieldName, handler);
|
@@ -135,17 +145,21 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
135
145
|
customFocusHandlers().remove(fieldName, handler);
|
136
146
|
};
|
137
147
|
},
|
148
|
+
hasBeenSubmitted,
|
138
149
|
}), [
|
139
150
|
fieldErrors,
|
140
151
|
action,
|
141
152
|
defaultsToUse,
|
142
153
|
isSubmitting,
|
154
|
+
touchedFields,
|
155
|
+
hasBeenSubmitted,
|
143
156
|
setFieldErrors,
|
144
157
|
validator,
|
145
158
|
customFocusHandlers,
|
146
159
|
]);
|
147
160
|
const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : react_1.Form;
|
148
161
|
return ((0, jsx_runtime_1.jsx)(Form, { ref: (0, util_1.mergeRefs)([formRef, formRefProp]), ...rest, action: action, onSubmit: (event) => {
|
162
|
+
setHasBeenSubmitted(true);
|
149
163
|
const result = validator.validate(getDataFromForm(event.currentTarget));
|
150
164
|
if (result.error) {
|
151
165
|
event.preventDefault();
|
@@ -162,6 +176,8 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
162
176
|
if (event.defaultPrevented)
|
163
177
|
return;
|
164
178
|
setFieldErrors({});
|
179
|
+
setTouchedFields({});
|
180
|
+
setHasBeenSubmitted(false);
|
165
181
|
}, 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
182
|
}
|
167
183
|
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;
|
@@ -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,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,42 @@
|
|
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
|
+
return {
|
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
|
+
};
|
41
|
+
};
|
42
|
+
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
|
},
|
@@ -225,6 +234,8 @@ export function ValidatedForm<DataType>({
|
|
225
234
|
...prev,
|
226
235
|
[fieldName]: error,
|
227
236
|
}));
|
237
|
+
} else {
|
238
|
+
setFieldErrors((prev) => omit(prev, fieldName));
|
228
239
|
}
|
229
240
|
},
|
230
241
|
registerReceiveFocus: (fieldName, handler) => {
|
@@ -233,12 +244,15 @@ export function ValidatedForm<DataType>({
|
|
233
244
|
customFocusHandlers().remove(fieldName, handler);
|
234
245
|
};
|
235
246
|
},
|
247
|
+
hasBeenSubmitted,
|
236
248
|
}),
|
237
249
|
[
|
238
250
|
fieldErrors,
|
239
251
|
action,
|
240
252
|
defaultsToUse,
|
241
253
|
isSubmitting,
|
254
|
+
touchedFields,
|
255
|
+
hasBeenSubmitted,
|
242
256
|
setFieldErrors,
|
243
257
|
validator,
|
244
258
|
customFocusHandlers,
|
@@ -253,6 +267,7 @@ export function ValidatedForm<DataType>({
|
|
253
267
|
{...rest}
|
254
268
|
action={action}
|
255
269
|
onSubmit={(event) => {
|
270
|
+
setHasBeenSubmitted(true);
|
256
271
|
const result = validator.validate(getDataFromForm(event.currentTarget));
|
257
272
|
if (result.error) {
|
258
273
|
event.preventDefault();
|
@@ -272,6 +287,8 @@ export function ValidatedForm<DataType>({
|
|
272
287
|
onReset?.(event);
|
273
288
|
if (event.defaultPrevented) return;
|
274
289
|
setFieldErrors({});
|
290
|
+
setTouchedFields({});
|
291
|
+
setHasBeenSubmitted(false);
|
275
292
|
}}
|
276
293
|
>
|
277
294
|
<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,79 @@
|
|
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
|
+
export type MinimalInputProps = {
|
21
|
+
onChange?: (...args: any[]) => void;
|
22
|
+
onBlur?: (...args: any[]) => void;
|
23
|
+
};
|
24
|
+
|
25
|
+
export type MinimalResult = {
|
26
|
+
name: string;
|
27
|
+
onChange: (...args: any[]) => void;
|
28
|
+
onBlur: (...args: any[]) => void;
|
29
|
+
defaultValue?: any;
|
30
|
+
};
|
31
|
+
|
32
|
+
export type GetInputProps = <T extends {}>(
|
33
|
+
props?: T & MinimalInputProps
|
34
|
+
) => T & MinimalResult;
|
35
|
+
|
36
|
+
const defaultValidationBehavior: ValidationBehaviorOptions = {
|
37
|
+
initial: "onBlur",
|
38
|
+
whenTouched: "onChange",
|
39
|
+
whenSubmitted: "onChange",
|
40
|
+
};
|
41
|
+
|
42
|
+
export const createGetInputProps = ({
|
43
|
+
clearError,
|
44
|
+
validate,
|
45
|
+
defaultValue,
|
46
|
+
touched,
|
47
|
+
setTouched,
|
48
|
+
hasBeenSubmitted,
|
49
|
+
validationBehavior,
|
50
|
+
name,
|
51
|
+
}: CreateGetInputPropsOptions): GetInputProps => {
|
52
|
+
const validationBehaviors = {
|
53
|
+
...defaultValidationBehavior,
|
54
|
+
...validationBehavior,
|
55
|
+
};
|
56
|
+
|
57
|
+
return (props = {} as any) => {
|
58
|
+
const behavior = hasBeenSubmitted
|
59
|
+
? validationBehaviors.whenSubmitted
|
60
|
+
: touched
|
61
|
+
? validationBehaviors.whenTouched
|
62
|
+
: validationBehaviors.initial;
|
63
|
+
return {
|
64
|
+
...props,
|
65
|
+
onChange: (...args) => {
|
66
|
+
if (behavior === "onChange") validate();
|
67
|
+
else clearError();
|
68
|
+
return props?.onChange?.(...args);
|
69
|
+
},
|
70
|
+
onBlur: (...args) => {
|
71
|
+
if (behavior === "onBlur") validate();
|
72
|
+
setTouched(true);
|
73
|
+
return props?.onBlur?.(...args);
|
74
|
+
},
|
75
|
+
defaultValue,
|
76
|
+
name,
|
77
|
+
};
|
78
|
+
};
|
79
|
+
};
|
package/src/validation/types.ts
CHANGED