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/build/ValidatedForm.js
CHANGED
@@ -9,6 +9,7 @@ const react_1 = require("@remix-run/react");
|
|
9
9
|
const react_2 = require("react");
|
10
10
|
const tiny_invariant_1 = __importDefault(require("tiny-invariant"));
|
11
11
|
const formContext_1 = require("./internal/formContext");
|
12
|
+
const SingleTypeMultiValueMap_1 = require("./internal/SingleTypeMultiValueMap");
|
12
13
|
const submissionCallbacks_1 = require("./internal/submissionCallbacks");
|
13
14
|
const util_1 = require("./internal/util");
|
14
15
|
function useFieldErrorsFromBackend(fetcher, subaction) {
|
@@ -70,15 +71,39 @@ function useDefaultValues(fieldErrors, defaultValues) {
|
|
70
71
|
const defaultsFromValidationError = fieldErrors === null || fieldErrors === void 0 ? void 0 : fieldErrors._submittedData;
|
71
72
|
return defaultsFromValidationError !== null && defaultsFromValidationError !== void 0 ? defaultsFromValidationError : defaultValues;
|
72
73
|
}
|
74
|
+
const focusFirstInvalidInput = (fieldErrors, customFocusHandlers, formElement) => {
|
75
|
+
const invalidInputSelector = Object.keys(fieldErrors)
|
76
|
+
.map((fieldName) => `input[name="${fieldName}"]`)
|
77
|
+
.join(",");
|
78
|
+
const invalidInputs = formElement.querySelectorAll(invalidInputSelector);
|
79
|
+
for (const element of invalidInputs) {
|
80
|
+
const input = element;
|
81
|
+
if (customFocusHandlers.has(input.name)) {
|
82
|
+
customFocusHandlers.getAll(input.name).forEach((handler) => {
|
83
|
+
handler();
|
84
|
+
});
|
85
|
+
break;
|
86
|
+
}
|
87
|
+
// We don't filter these out ahead of time because
|
88
|
+
// they could have a custom focus handler
|
89
|
+
if (input.type === "hidden") {
|
90
|
+
continue;
|
91
|
+
}
|
92
|
+
input.focus();
|
93
|
+
break;
|
94
|
+
}
|
95
|
+
};
|
73
96
|
/**
|
74
97
|
* The primary form component of `remix-validated-form`.
|
75
98
|
*/
|
76
|
-
function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, ...rest }) {
|
99
|
+
function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, disableFocusOnError, ...rest }) {
|
77
100
|
var _a;
|
78
101
|
const fieldErrorsFromBackend = useFieldErrorsFromBackend(fetcher, subaction);
|
79
102
|
const [fieldErrors, setFieldErrors] = useFieldErrors(fieldErrorsFromBackend);
|
80
103
|
const isSubmitting = useIsSubmitting(action, subaction, fetcher);
|
81
104
|
const defaultsToUse = useDefaultValues(fieldErrorsFromBackend, defaultValues);
|
105
|
+
const [touchedFields, setTouchedFields] = (0, react_2.useState)({});
|
106
|
+
const [hasBeenSubmitted, setHasBeenSubmitted] = (0, react_2.useState)(false);
|
82
107
|
const formRef = (0, react_2.useRef)(null);
|
83
108
|
(0, submissionCallbacks_1.useSubmitComplete)(isSubmitting, () => {
|
84
109
|
var _a;
|
@@ -86,11 +111,18 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
86
111
|
(_a = formRef.current) === null || _a === void 0 ? void 0 : _a.reset();
|
87
112
|
}
|
88
113
|
});
|
114
|
+
const customFocusHandlers = (0, SingleTypeMultiValueMap_1.useMultiValueMap)();
|
89
115
|
const contextValue = (0, react_2.useMemo)(() => ({
|
90
116
|
fieldErrors,
|
91
117
|
action,
|
92
118
|
defaultValues: defaultsToUse,
|
93
119
|
isSubmitting: isSubmitting !== null && isSubmitting !== void 0 ? isSubmitting : false,
|
120
|
+
isValid: Object.keys(fieldErrors).length === 0,
|
121
|
+
touchedFields,
|
122
|
+
setFieldTouched: (fieldName, touched) => setTouchedFields((prev) => ({
|
123
|
+
...prev,
|
124
|
+
[fieldName]: touched,
|
125
|
+
})),
|
94
126
|
clearError: (fieldName) => {
|
95
127
|
setFieldErrors((prev) => (0, util_1.omit)(prev, fieldName));
|
96
128
|
},
|
@@ -103,21 +135,38 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
103
135
|
[fieldName]: error,
|
104
136
|
}));
|
105
137
|
}
|
138
|
+
else {
|
139
|
+
setFieldErrors((prev) => (0, util_1.omit)(prev, fieldName));
|
140
|
+
}
|
141
|
+
},
|
142
|
+
registerReceiveFocus: (fieldName, handler) => {
|
143
|
+
customFocusHandlers().add(fieldName, handler);
|
144
|
+
return () => {
|
145
|
+
customFocusHandlers().remove(fieldName, handler);
|
146
|
+
};
|
106
147
|
},
|
148
|
+
hasBeenSubmitted,
|
107
149
|
}), [
|
108
150
|
fieldErrors,
|
109
151
|
action,
|
110
152
|
defaultsToUse,
|
111
153
|
isSubmitting,
|
154
|
+
touchedFields,
|
155
|
+
hasBeenSubmitted,
|
112
156
|
setFieldErrors,
|
113
157
|
validator,
|
158
|
+
customFocusHandlers,
|
114
159
|
]);
|
115
160
|
const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : react_1.Form;
|
116
161
|
return ((0, jsx_runtime_1.jsx)(Form, { ref: (0, util_1.mergeRefs)([formRef, formRefProp]), ...rest, action: action, onSubmit: (event) => {
|
162
|
+
setHasBeenSubmitted(true);
|
117
163
|
const result = validator.validate(getDataFromForm(event.currentTarget));
|
118
164
|
if (result.error) {
|
119
165
|
event.preventDefault();
|
120
166
|
setFieldErrors(result.error);
|
167
|
+
if (!disableFocusOnError) {
|
168
|
+
focusFirstInvalidInput(result.error, customFocusHandlers(), formRef.current);
|
169
|
+
}
|
121
170
|
}
|
122
171
|
else {
|
123
172
|
onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(result.data, event);
|
@@ -127,6 +176,8 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
127
176
|
if (event.defaultPrevented)
|
128
177
|
return;
|
129
178
|
setFieldErrors({});
|
179
|
+
setTouchedFields({});
|
180
|
+
setHasBeenSubmitted(false);
|
130
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));
|
131
182
|
}
|
132
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,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/build/hooks.js
CHANGED
@@ -8,21 +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
|
-
const useField = (name) => {
|
15
|
-
const { fieldErrors, clearError, validateField, defaultValues } = (0, react_1.useContext)(formContext_1.FormContext);
|
16
|
-
const
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
:
|
25
|
-
|
15
|
+
const useField = (name, options) => {
|
16
|
+
const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, touchedFields, setFieldTouched, hasBeenSubmitted, } = (0, react_1.useContext)(formContext_1.FormContext);
|
17
|
+
const isTouched = !!touchedFields[name];
|
18
|
+
const { handleReceiveFocus } = options !== null && options !== void 0 ? options : {};
|
19
|
+
(0, react_1.useEffect)(() => {
|
20
|
+
if (handleReceiveFocus)
|
21
|
+
return registerReceiveFocus(name, handleReceiveFocus);
|
22
|
+
}, [handleReceiveFocus, name, registerReceiveFocus]);
|
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
|
+
]);
|
26
57
|
return field;
|
27
58
|
};
|
28
59
|
exports.useField = useField;
|
@@ -0,0 +1,8 @@
|
|
1
|
+
export declare class MultiValueMap<Key, Value> {
|
2
|
+
private dict;
|
3
|
+
add: (key: Key, value: Value) => void;
|
4
|
+
remove: (key: Key, value: Value) => void;
|
5
|
+
getAll: (key: Key) => Value[];
|
6
|
+
has: (key: Key) => boolean;
|
7
|
+
}
|
8
|
+
export declare const useMultiValueMap: <Key, Value>() => () => MultiValueMap<Key, Value>;
|
@@ -0,0 +1,45 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.useMultiValueMap = exports.MultiValueMap = void 0;
|
4
|
+
const react_1 = require("react");
|
5
|
+
class MultiValueMap {
|
6
|
+
constructor() {
|
7
|
+
this.dict = new Map();
|
8
|
+
this.add = (key, value) => {
|
9
|
+
var _a;
|
10
|
+
this.dict.set(key, [...((_a = this.dict.get(key)) !== null && _a !== void 0 ? _a : []), value]);
|
11
|
+
if (this.dict.has(key)) {
|
12
|
+
this.dict.get(key).push(value);
|
13
|
+
}
|
14
|
+
else {
|
15
|
+
this.dict.set(key, [value]);
|
16
|
+
}
|
17
|
+
};
|
18
|
+
this.remove = (key, value) => {
|
19
|
+
if (!this.dict.has(key))
|
20
|
+
return;
|
21
|
+
const array = this.dict.get(key);
|
22
|
+
const index = array.indexOf(value);
|
23
|
+
if (index !== -1)
|
24
|
+
array.splice(index, 1);
|
25
|
+
if (array.length === 0)
|
26
|
+
this.dict.delete(key);
|
27
|
+
};
|
28
|
+
this.getAll = (key) => {
|
29
|
+
var _a;
|
30
|
+
return (_a = this.dict.get(key)) !== null && _a !== void 0 ? _a : [];
|
31
|
+
};
|
32
|
+
this.has = (key) => this.dict.has(key);
|
33
|
+
}
|
34
|
+
}
|
35
|
+
exports.MultiValueMap = MultiValueMap;
|
36
|
+
const useMultiValueMap = () => {
|
37
|
+
const ref = (0, react_1.useRef)(null);
|
38
|
+
return () => {
|
39
|
+
if (ref.current)
|
40
|
+
return ref.current;
|
41
|
+
ref.current = new MultiValueMap();
|
42
|
+
return ref.current;
|
43
|
+
};
|
44
|
+
};
|
45
|
+
exports.useMultiValueMap = useMultiValueMap;
|
@@ -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>;
|
@@ -7,4 +7,9 @@ exports.FormContext = (0, react_1.createContext)({
|
|
7
7
|
clearError: () => { },
|
8
8
|
validateField: () => { },
|
9
9
|
isSubmitting: false,
|
10
|
+
hasBeenSubmitted: false,
|
11
|
+
isValid: true,
|
12
|
+
registerReceiveFocus: () => () => { },
|
13
|
+
touchedFields: {},
|
14
|
+
setFieldTouched: () => { },
|
10
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/build/server.d.ts
CHANGED
@@ -4,4 +4,4 @@ import { FieldErrors } from "./validation/types";
|
|
4
4
|
* The `ValidatedForm` on the frontend will automatically display the errors
|
5
5
|
* if this is returned from the action.
|
6
6
|
*/
|
7
|
-
export declare const validationError: (errors: FieldErrors) => Response;
|
7
|
+
export declare const validationError: (errors: FieldErrors, submittedData?: unknown) => Response;
|
package/build/server.js
CHANGED
@@ -7,5 +7,15 @@ const server_runtime_1 = require("@remix-run/server-runtime");
|
|
7
7
|
* The `ValidatedForm` on the frontend will automatically display the errors
|
8
8
|
* if this is returned from the action.
|
9
9
|
*/
|
10
|
-
const validationError = (errors
|
10
|
+
const validationError = (errors, submittedData) => {
|
11
|
+
if (submittedData) {
|
12
|
+
return (0, server_runtime_1.json)({
|
13
|
+
fieldErrors: {
|
14
|
+
...errors,
|
15
|
+
_submittedData: submittedData,
|
16
|
+
},
|
17
|
+
}, { status: 422 });
|
18
|
+
}
|
19
|
+
return (0, server_runtime_1.json)({ fieldErrors: errors }, { status: 422 });
|
20
|
+
};
|
11
21
|
exports.validationError = validationError;
|
@@ -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/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "remix-validated-form",
|
3
|
-
"version": "3.
|
3
|
+
"version": "3.2.0",
|
4
4
|
"description": "Form component and utils for easy form validation in remix",
|
5
5
|
"browser": "./browser/index.js",
|
6
6
|
"main": "./build/index.js",
|
@@ -14,8 +14,6 @@
|
|
14
14
|
"build": "npm run build:browser && npm run build:main",
|
15
15
|
"build:browser": "tsc --module ESNext --outDir ./browser",
|
16
16
|
"build:main": "tsc --module CommonJS --outDir ./build",
|
17
|
-
"test": "jest src",
|
18
|
-
"test:watch": "jest src --watch",
|
19
17
|
"prepublishOnly": "cp ../../README.md ./README.md && npm run build",
|
20
18
|
"postpublish": "rm ./README.md"
|
21
19
|
},
|
package/src/ValidatedForm.tsx
CHANGED
@@ -14,12 +14,17 @@ import React, {
|
|
14
14
|
} from "react";
|
15
15
|
import invariant from "tiny-invariant";
|
16
16
|
import { FormContext, FormContextValue } from "./internal/formContext";
|
17
|
+
import {
|
18
|
+
MultiValueMap,
|
19
|
+
useMultiValueMap,
|
20
|
+
} from "./internal/SingleTypeMultiValueMap";
|
17
21
|
import { useSubmitComplete } from "./internal/submissionCallbacks";
|
18
22
|
import { omit, mergeRefs } from "./internal/util";
|
19
23
|
import {
|
20
24
|
FieldErrors,
|
21
25
|
Validator,
|
22
26
|
FieldErrorsWithData,
|
27
|
+
TouchedFields,
|
23
28
|
} from "./validation/types";
|
24
29
|
|
25
30
|
export type FormProps<DataType> = {
|
@@ -59,6 +64,11 @@ export type FormProps<DataType> = {
|
|
59
64
|
* and don't redirect in-between submissions.
|
60
65
|
*/
|
61
66
|
resetAfterSubmit?: boolean;
|
67
|
+
/**
|
68
|
+
* Normally, the first invalid input will be focused when the validation fails on form submit.
|
69
|
+
* Set this to `false` to disable this behavior.
|
70
|
+
*/
|
71
|
+
disableFocusOnError?: boolean;
|
62
72
|
} & Omit<ComponentProps<typeof RemixForm>, "onSubmit">;
|
63
73
|
|
64
74
|
function useFieldErrorsFromBackend(
|
@@ -136,6 +146,36 @@ function useDefaultValues<DataType>(
|
|
136
146
|
return defaultsFromValidationError ?? defaultValues;
|
137
147
|
}
|
138
148
|
|
149
|
+
const focusFirstInvalidInput = (
|
150
|
+
fieldErrors: FieldErrors,
|
151
|
+
customFocusHandlers: MultiValueMap<string, () => void>,
|
152
|
+
formElement: HTMLFormElement
|
153
|
+
) => {
|
154
|
+
const invalidInputSelector = Object.keys(fieldErrors)
|
155
|
+
.map((fieldName) => `input[name="${fieldName}"]`)
|
156
|
+
.join(",");
|
157
|
+
const invalidInputs = formElement.querySelectorAll(invalidInputSelector);
|
158
|
+
for (const element of invalidInputs) {
|
159
|
+
const input = element as HTMLInputElement;
|
160
|
+
|
161
|
+
if (customFocusHandlers.has(input.name)) {
|
162
|
+
customFocusHandlers.getAll(input.name).forEach((handler) => {
|
163
|
+
handler();
|
164
|
+
});
|
165
|
+
break;
|
166
|
+
}
|
167
|
+
|
168
|
+
// We don't filter these out ahead of time because
|
169
|
+
// they could have a custom focus handler
|
170
|
+
if (input.type === "hidden") {
|
171
|
+
continue;
|
172
|
+
}
|
173
|
+
|
174
|
+
input.focus();
|
175
|
+
break;
|
176
|
+
}
|
177
|
+
};
|
178
|
+
|
139
179
|
/**
|
140
180
|
* The primary form component of `remix-validated-form`.
|
141
181
|
*/
|
@@ -150,18 +190,22 @@ export function ValidatedForm<DataType>({
|
|
150
190
|
onReset,
|
151
191
|
subaction,
|
152
192
|
resetAfterSubmit,
|
193
|
+
disableFocusOnError,
|
153
194
|
...rest
|
154
195
|
}: FormProps<DataType>) {
|
155
196
|
const fieldErrorsFromBackend = useFieldErrorsFromBackend(fetcher, subaction);
|
156
197
|
const [fieldErrors, setFieldErrors] = useFieldErrors(fieldErrorsFromBackend);
|
157
198
|
const isSubmitting = useIsSubmitting(action, subaction, fetcher);
|
158
199
|
const defaultsToUse = useDefaultValues(fieldErrorsFromBackend, defaultValues);
|
200
|
+
const [touchedFields, setTouchedFields] = useState<TouchedFields>({});
|
201
|
+
const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
|
159
202
|
const formRef = useRef<HTMLFormElement>(null);
|
160
203
|
useSubmitComplete(isSubmitting, () => {
|
161
204
|
if (!fieldErrorsFromBackend && resetAfterSubmit) {
|
162
205
|
formRef.current?.reset();
|
163
206
|
}
|
164
207
|
});
|
208
|
+
const customFocusHandlers = useMultiValueMap<string, () => void>();
|
165
209
|
|
166
210
|
const contextValue = useMemo<FormContextValue>(
|
167
211
|
() => ({
|
@@ -169,6 +213,13 @@ export function ValidatedForm<DataType>({
|
|
169
213
|
action,
|
170
214
|
defaultValues: defaultsToUse,
|
171
215
|
isSubmitting: isSubmitting ?? false,
|
216
|
+
isValid: Object.keys(fieldErrors).length === 0,
|
217
|
+
touchedFields,
|
218
|
+
setFieldTouched: (fieldName: string, touched: boolean) =>
|
219
|
+
setTouchedFields((prev) => ({
|
220
|
+
...prev,
|
221
|
+
[fieldName]: touched,
|
222
|
+
})),
|
172
223
|
clearError: (fieldName) => {
|
173
224
|
setFieldErrors((prev) => omit(prev, fieldName));
|
174
225
|
},
|
@@ -183,16 +234,28 @@ export function ValidatedForm<DataType>({
|
|
183
234
|
...prev,
|
184
235
|
[fieldName]: error,
|
185
236
|
}));
|
237
|
+
} else {
|
238
|
+
setFieldErrors((prev) => omit(prev, fieldName));
|
186
239
|
}
|
187
240
|
},
|
241
|
+
registerReceiveFocus: (fieldName, handler) => {
|
242
|
+
customFocusHandlers().add(fieldName, handler);
|
243
|
+
return () => {
|
244
|
+
customFocusHandlers().remove(fieldName, handler);
|
245
|
+
};
|
246
|
+
},
|
247
|
+
hasBeenSubmitted,
|
188
248
|
}),
|
189
249
|
[
|
190
250
|
fieldErrors,
|
191
251
|
action,
|
192
252
|
defaultsToUse,
|
193
253
|
isSubmitting,
|
254
|
+
touchedFields,
|
255
|
+
hasBeenSubmitted,
|
194
256
|
setFieldErrors,
|
195
257
|
validator,
|
258
|
+
customFocusHandlers,
|
196
259
|
]
|
197
260
|
);
|
198
261
|
|
@@ -204,10 +267,18 @@ export function ValidatedForm<DataType>({
|
|
204
267
|
{...rest}
|
205
268
|
action={action}
|
206
269
|
onSubmit={(event) => {
|
270
|
+
setHasBeenSubmitted(true);
|
207
271
|
const result = validator.validate(getDataFromForm(event.currentTarget));
|
208
272
|
if (result.error) {
|
209
273
|
event.preventDefault();
|
210
274
|
setFieldErrors(result.error);
|
275
|
+
if (!disableFocusOnError) {
|
276
|
+
focusFirstInvalidInput(
|
277
|
+
result.error,
|
278
|
+
customFocusHandlers(),
|
279
|
+
formRef.current!
|
280
|
+
);
|
281
|
+
}
|
211
282
|
} else {
|
212
283
|
onSubmit?.(result.data, event);
|
213
284
|
}
|
@@ -216,6 +287,8 @@ export function ValidatedForm<DataType>({
|
|
216
287
|
onReset?.(event);
|
217
288
|
if (event.defaultPrevented) return;
|
218
289
|
setFieldErrors({});
|
290
|
+
setTouchedFields({});
|
291
|
+
setHasBeenSubmitted(false);
|
219
292
|
}}
|
220
293
|
>
|
221
294
|
<FormContext.Provider value={contextValue}>
|