remix-validated-form 3.3.1 → 4.0.0-beta.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 +3 -1
- package/browser/ValidatedForm.d.ts +1 -1
- package/browser/ValidatedForm.js +89 -41
- package/browser/components.d.ts +10 -0
- package/browser/components.js +10 -0
- package/browser/hooks.d.ts +13 -1
- package/browser/hooks.js +32 -7
- package/browser/internal/flatten.d.ts +0 -3
- package/browser/internal/flatten.js +0 -33
- package/browser/internal/formContext.d.ts +2 -3
- package/browser/internal/formContext.js +1 -11
- package/browser/internal/getInputProps.d.ts +10 -4
- package/browser/internal/getInputProps.js +23 -3
- package/browser/server.d.ts +13 -4
- package/browser/server.js +18 -13
- package/browser/types.d.ts +1 -0
- package/browser/types.js +1 -0
- package/browser/validation/createValidator.d.ts +2 -2
- package/browser/validation/createValidator.js +15 -8
- package/browser/validation/types.d.ts +35 -12
- package/build/ValidatedForm.d.ts +1 -1
- package/build/ValidatedForm.js +107 -40
- package/build/hooks.d.ts +13 -1
- package/build/hooks.js +33 -7
- package/build/internal/flatten.d.ts +0 -3
- package/build/internal/flatten.js +1 -35
- package/build/internal/formContext.d.ts +2 -3
- package/build/internal/formContext.js +1 -11
- package/build/internal/getInputProps.d.ts +10 -4
- package/build/internal/getInputProps.js +23 -3
- package/build/server.d.ts +13 -4
- package/build/server.js +18 -13
- package/build/types.d.ts +1 -0
- package/build/types.js +2 -0
- package/build/validation/createValidator.d.ts +2 -2
- package/build/validation/createValidator.js +15 -8
- package/build/validation/types.d.ts +35 -12
- package/package.json +1 -1
- package/src/ValidatedForm.tsx +112 -42
- package/src/hooks.ts +43 -7
- package/src/internal/flatten.ts +0 -44
- package/src/internal/formContext.ts +2 -13
- package/src/internal/getInputProps.ts +36 -9
- package/src/server.ts +28 -21
- package/src/types.ts +1 -0
- package/src/validation/createValidator.ts +21 -10
- package/src/validation/types.ts +38 -7
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.4.2 build:browser
|
4
4
|
> tsc --module ESNext --outDir ./browser
|
5
5
|
|
6
6
|
|
7
|
-
> remix-validated-form@3.
|
7
|
+
> remix-validated-form@3.4.2 build:main
|
8
8
|
> tsc --module CommonJS --outDir ./build
|
9
9
|
|
package/README.md
CHANGED
@@ -50,6 +50,8 @@ see the [Validation library support](#validation-library-support) section below.
|
|
50
50
|
npm install @remix-validated-form/with-zod
|
51
51
|
```
|
52
52
|
|
53
|
+
If you're using zod, you might also find `zod-form-data` helpful.
|
54
|
+
|
53
55
|
## Create an input component
|
54
56
|
|
55
57
|
In order to display field errors or do field-by-field validation,
|
@@ -118,7 +120,7 @@ const validator = withYup(
|
|
118
120
|
);
|
119
121
|
|
120
122
|
export const action: ActionFunction = async ({ request }) => {
|
121
|
-
const fieldValues = validator.validate(await request.formData());
|
123
|
+
const fieldValues = await validator.validate(await request.formData());
|
122
124
|
if (fieldValues.error) return validationError(fieldValues.error);
|
123
125
|
const { firstName, lastName, email } = fieldValues.data;
|
124
126
|
|
@@ -10,7 +10,7 @@ export declare type FormProps<DataType> = {
|
|
10
10
|
* A submit callback that gets called when the form is submitted
|
11
11
|
* after all validations have been run.
|
12
12
|
*/
|
13
|
-
onSubmit?: (data: DataType, event: React.FormEvent<HTMLFormElement>) => void
|
13
|
+
onSubmit?: (data: DataType, event: React.FormEvent<HTMLFormElement>) => Promise<void>;
|
14
14
|
/**
|
15
15
|
* Allows you to provide a `fetcher` from remix's `useFetcher` hook.
|
16
16
|
* The form will use the fetcher for loading states, action data, etc
|
package/browser/ValidatedForm.js
CHANGED
@@ -1,25 +1,25 @@
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
2
|
-
import { Form as RemixForm, useActionData, useFormAction, useTransition, } from "@remix-run/react";
|
3
|
-
import
|
2
|
+
import { Form as RemixForm, useActionData, useFormAction, useSubmit, useTransition, } from "@remix-run/react";
|
3
|
+
import uniq from "lodash/uniq";
|
4
|
+
import React, { useEffect, useMemo, useRef, useState, } from "react";
|
4
5
|
import invariant from "tiny-invariant";
|
5
6
|
import { FormContext } from "./internal/formContext";
|
6
7
|
import { useMultiValueMap } from "./internal/MultiValueMap";
|
7
8
|
import { useSubmitComplete } from "./internal/submissionCallbacks";
|
8
9
|
import { omit, mergeRefs } from "./internal/util";
|
9
|
-
function
|
10
|
-
var _a
|
10
|
+
function useErrorResponseForThisForm(fetcher, subaction) {
|
11
|
+
var _a;
|
11
12
|
const actionData = useActionData();
|
12
|
-
if (fetcher)
|
13
|
-
|
14
|
-
|
13
|
+
if (fetcher) {
|
14
|
+
if ((_a = fetcher.data) === null || _a === void 0 ? void 0 : _a.fieldErrors)
|
15
|
+
return fetcher.data;
|
15
16
|
return null;
|
16
|
-
if (actionData.fieldErrors) {
|
17
|
-
const submittedData = (_b = actionData.fieldErrors) === null || _b === void 0 ? void 0 : _b._submittedData;
|
18
|
-
const subactionsMatch = subaction
|
19
|
-
? subaction === (submittedData === null || submittedData === void 0 ? void 0 : submittedData.subaction)
|
20
|
-
: !(submittedData === null || submittedData === void 0 ? void 0 : submittedData.subaction);
|
21
|
-
return subactionsMatch ? actionData.fieldErrors : null;
|
22
17
|
}
|
18
|
+
if (!(actionData === null || actionData === void 0 ? void 0 : actionData.fieldErrors))
|
19
|
+
return null;
|
20
|
+
if ((!subaction && !actionData.subaction) ||
|
21
|
+
actionData.subaction === subaction)
|
22
|
+
return actionData;
|
23
23
|
return null;
|
24
24
|
}
|
25
25
|
function useFieldErrors(fieldErrorsFromBackend) {
|
@@ -61,30 +61,50 @@ const getDataFromForm = (el) => new FormData(el);
|
|
61
61
|
* It will only ever be a problem if the form includes a `<button type="reset" />`
|
62
62
|
* and only if JS is disabled.
|
63
63
|
*/
|
64
|
-
function useDefaultValues(
|
65
|
-
|
66
|
-
|
64
|
+
function useDefaultValues(repopulateFieldsFromBackend, defaultValues) {
|
65
|
+
return repopulateFieldsFromBackend !== null && repopulateFieldsFromBackend !== void 0 ? repopulateFieldsFromBackend : defaultValues;
|
66
|
+
}
|
67
|
+
function nonNull(value) {
|
68
|
+
return value !== null;
|
67
69
|
}
|
68
70
|
const focusFirstInvalidInput = (fieldErrors, customFocusHandlers, formElement) => {
|
69
|
-
|
70
|
-
|
71
|
-
.
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
71
|
+
var _a;
|
72
|
+
const namesInOrder = [...formElement.elements]
|
73
|
+
.map((el) => {
|
74
|
+
const input = el instanceof RadioNodeList ? el[0] : el;
|
75
|
+
if (input instanceof HTMLInputElement)
|
76
|
+
return input.name;
|
77
|
+
return null;
|
78
|
+
})
|
79
|
+
.filter(nonNull)
|
80
|
+
.filter((name) => name in fieldErrors);
|
81
|
+
const uniqueNamesInOrder = uniq(namesInOrder);
|
82
|
+
for (const fieldName of uniqueNamesInOrder) {
|
83
|
+
if (customFocusHandlers.has(fieldName)) {
|
84
|
+
customFocusHandlers.getAll(fieldName).forEach((handler) => {
|
77
85
|
handler();
|
78
86
|
});
|
79
87
|
break;
|
80
88
|
}
|
81
|
-
|
82
|
-
|
83
|
-
if (input.type === "hidden") {
|
89
|
+
const elem = formElement.elements.namedItem(fieldName);
|
90
|
+
if (!elem)
|
84
91
|
continue;
|
92
|
+
if (elem instanceof RadioNodeList) {
|
93
|
+
const selectedRadio = (_a = [...elem]
|
94
|
+
.filter((item) => item instanceof HTMLInputElement)
|
95
|
+
.find((item) => item.value === elem.value)) !== null && _a !== void 0 ? _a : elem[0];
|
96
|
+
if (selectedRadio && selectedRadio instanceof HTMLInputElement) {
|
97
|
+
selectedRadio.focus();
|
98
|
+
break;
|
99
|
+
}
|
100
|
+
}
|
101
|
+
if (elem instanceof HTMLInputElement) {
|
102
|
+
if (elem.type === "hidden") {
|
103
|
+
continue;
|
104
|
+
}
|
105
|
+
elem.focus();
|
106
|
+
break;
|
85
107
|
}
|
86
|
-
input.focus();
|
87
|
-
break;
|
88
108
|
}
|
89
109
|
};
|
90
110
|
/**
|
@@ -92,16 +112,17 @@ const focusFirstInvalidInput = (fieldErrors, customFocusHandlers, formElement) =
|
|
92
112
|
*/
|
93
113
|
export function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, disableFocusOnError, ...rest }) {
|
94
114
|
var _a;
|
95
|
-
const
|
96
|
-
const [fieldErrors, setFieldErrors] = useFieldErrors(
|
115
|
+
const backendError = useErrorResponseForThisForm(fetcher, subaction);
|
116
|
+
const [fieldErrors, setFieldErrors] = useFieldErrors(backendError === null || backendError === void 0 ? void 0 : backendError.fieldErrors);
|
97
117
|
const isSubmitting = useIsSubmitting(action, subaction, fetcher);
|
98
|
-
const defaultsToUse = useDefaultValues(
|
118
|
+
const defaultsToUse = useDefaultValues(backendError === null || backendError === void 0 ? void 0 : backendError.repopulateFields, defaultValues);
|
99
119
|
const [touchedFields, setTouchedFields] = useState({});
|
100
120
|
const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
|
121
|
+
const submit = useSubmit();
|
101
122
|
const formRef = useRef(null);
|
102
123
|
useSubmitComplete(isSubmitting, () => {
|
103
124
|
var _a;
|
104
|
-
if (!
|
125
|
+
if (!backendError && resetAfterSubmit) {
|
105
126
|
(_a = formRef.current) === null || _a === void 0 ? void 0 : _a.reset();
|
106
127
|
}
|
107
128
|
});
|
@@ -120,9 +141,9 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
120
141
|
clearError: (fieldName) => {
|
121
142
|
setFieldErrors((prev) => omit(prev, fieldName));
|
122
143
|
},
|
123
|
-
validateField: (fieldName) => {
|
144
|
+
validateField: async (fieldName) => {
|
124
145
|
invariant(formRef.current, "Cannot find reference to form");
|
125
|
-
const { error } = validator.validateField(getDataFromForm(formRef.current), fieldName);
|
146
|
+
const { error } = await validator.validateField(getDataFromForm(formRef.current), fieldName);
|
126
147
|
// By checking and returning `prev` here, we can avoid a re-render
|
127
148
|
// if the validation state is the same.
|
128
149
|
if (error) {
|
@@ -134,6 +155,7 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
134
155
|
[fieldName]: error,
|
135
156
|
};
|
136
157
|
});
|
158
|
+
return error;
|
137
159
|
}
|
138
160
|
else {
|
139
161
|
setFieldErrors((prev) => {
|
@@ -141,6 +163,7 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
141
163
|
return prev;
|
142
164
|
return omit(prev, fieldName);
|
143
165
|
});
|
166
|
+
return null;
|
144
167
|
}
|
145
168
|
},
|
146
169
|
registerReceiveFocus: (fieldName, handler) => {
|
@@ -162,18 +185,43 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
162
185
|
customFocusHandlers,
|
163
186
|
]);
|
164
187
|
const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : RemixForm;
|
165
|
-
|
188
|
+
let clickedButtonRef = React.useRef();
|
189
|
+
useEffect(() => {
|
190
|
+
let form = formRef.current;
|
191
|
+
if (!form)
|
192
|
+
return;
|
193
|
+
function handleClick(event) {
|
194
|
+
if (!(event.target instanceof HTMLElement))
|
195
|
+
return;
|
196
|
+
let submitButton = event.target.closest("button,input[type=submit]");
|
197
|
+
if (submitButton &&
|
198
|
+
submitButton.form === form &&
|
199
|
+
submitButton.type === "submit") {
|
200
|
+
clickedButtonRef.current = submitButton;
|
201
|
+
}
|
202
|
+
}
|
203
|
+
window.addEventListener("click", handleClick);
|
204
|
+
return () => {
|
205
|
+
window.removeEventListener("click", handleClick);
|
206
|
+
};
|
207
|
+
}, []);
|
208
|
+
return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, action: action, onSubmit: async (e) => {
|
209
|
+
e.preventDefault();
|
166
210
|
setHasBeenSubmitted(true);
|
167
|
-
const result = validator.validate(getDataFromForm(
|
211
|
+
const result = await validator.validate(getDataFromForm(e.currentTarget));
|
168
212
|
if (result.error) {
|
169
|
-
|
170
|
-
setFieldErrors(result.error);
|
213
|
+
setFieldErrors(result.error.fieldErrors);
|
171
214
|
if (!disableFocusOnError) {
|
172
|
-
focusFirstInvalidInput(result.error, customFocusHandlers(), formRef.current);
|
215
|
+
focusFirstInvalidInput(result.error.fieldErrors, customFocusHandlers(), formRef.current);
|
173
216
|
}
|
174
217
|
}
|
175
218
|
else {
|
176
|
-
onSubmit
|
219
|
+
onSubmit && onSubmit(result.data, e);
|
220
|
+
if (fetcher)
|
221
|
+
fetcher.submit(clickedButtonRef.current || e.currentTarget);
|
222
|
+
else
|
223
|
+
submit(clickedButtonRef.current || e.currentTarget);
|
224
|
+
clickedButtonRef.current = null;
|
177
225
|
}
|
178
226
|
}, onReset: (event) => {
|
179
227
|
onReset === null || onReset === void 0 ? void 0 : onReset(event);
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { HTMLProps } from "react";
|
2
|
+
declare type ValidatedInputProps = HTMLProps<HTMLInputElement> & {
|
3
|
+
name: string;
|
4
|
+
};
|
5
|
+
export declare const ValidatedInput: ({ name, ...rest }: ValidatedInputProps) => JSX.Element;
|
6
|
+
declare type ErrorMessageProps = {
|
7
|
+
name: string;
|
8
|
+
};
|
9
|
+
export declare const ErrorMessage: ({ name }: ErrorMessageProps) => string | undefined;
|
10
|
+
export {};
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
2
|
+
import { useField } from ".";
|
3
|
+
export const ValidatedInput = ({ name, ...rest }) => {
|
4
|
+
const { getInputProps } = useField(name);
|
5
|
+
return _jsx("input", { ...getInputProps(rest) }, void 0);
|
6
|
+
};
|
7
|
+
export const ErrorMessage = ({ name }) => {
|
8
|
+
const { error } = useField(name);
|
9
|
+
return error;
|
10
|
+
};
|
package/browser/hooks.d.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import { GetInputProps, ValidationBehaviorOptions } from "./internal/getInputProps";
|
2
|
+
import { ValidationState } from "./types";
|
2
3
|
export declare type FieldProps = {
|
3
4
|
/**
|
4
5
|
* The validation error message if there is one.
|
@@ -12,6 +13,14 @@ export declare type FieldProps = {
|
|
12
13
|
* Validates the field.
|
13
14
|
*/
|
14
15
|
validate: () => void;
|
16
|
+
/**
|
17
|
+
* The validation state of the field.
|
18
|
+
* - idle: the field has not been validated yet.
|
19
|
+
* - validating: the field is currently being validated.
|
20
|
+
* - valid: the field is valid.
|
21
|
+
* - invalid: the field is invalid.
|
22
|
+
*/
|
23
|
+
validationState: ValidationState;
|
15
24
|
/**
|
16
25
|
* The default value of the field, if there is one.
|
17
26
|
*/
|
@@ -46,7 +55,6 @@ export declare const useField: (name: string, options?: {
|
|
46
55
|
} | undefined) => FieldProps;
|
47
56
|
/**
|
48
57
|
* Provides access to the entire form context.
|
49
|
-
* This is not usually necessary, but can be useful for advanced use cases.
|
50
58
|
*/
|
51
59
|
export declare const useFormContext: () => import("./internal/formContext").FormContextValue;
|
52
60
|
/**
|
@@ -55,3 +63,7 @@ export declare const useFormContext: () => import("./internal/formContext").Form
|
|
55
63
|
* is aware of what form it's in and when _that_ form is being submitted.
|
56
64
|
*/
|
57
65
|
export declare const useIsSubmitting: () => boolean;
|
66
|
+
/**
|
67
|
+
* Returns whether or not the current form is valid.
|
68
|
+
*/
|
69
|
+
export declare const useIsValid: () => boolean;
|
package/browser/hooks.js
CHANGED
@@ -1,31 +1,52 @@
|
|
1
1
|
import get from "lodash/get";
|
2
2
|
import toPath from "lodash/toPath";
|
3
|
-
import { useContext, useEffect, useMemo } from "react";
|
3
|
+
import { useContext, useEffect, useMemo, useState } from "react";
|
4
4
|
import { FormContext } from "./internal/formContext";
|
5
5
|
import { createGetInputProps, } from "./internal/getInputProps";
|
6
|
+
const useInternalFormContext = (hookName) => {
|
7
|
+
const context = useContext(FormContext);
|
8
|
+
if (!context)
|
9
|
+
throw new Error(`${hookName} must be used within a ValidatedForm component`);
|
10
|
+
return context;
|
11
|
+
};
|
6
12
|
/**
|
7
13
|
* Provides the data and helpers necessary to set up a field.
|
8
14
|
*/
|
9
15
|
export const useField = (name, options) => {
|
10
|
-
const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, touchedFields, setFieldTouched, hasBeenSubmitted, } =
|
16
|
+
const { fieldErrors, clearError, validateField, defaultValues, registerReceiveFocus, touchedFields, setFieldTouched, hasBeenSubmitted, } = useInternalFormContext("useField");
|
11
17
|
const isTouched = !!touchedFields[name];
|
12
18
|
const { handleReceiveFocus } = options !== null && options !== void 0 ? options : {};
|
19
|
+
const [isValidating, setValidating] = useState(false);
|
13
20
|
useEffect(() => {
|
14
21
|
if (handleReceiveFocus)
|
15
22
|
return registerReceiveFocus(name, handleReceiveFocus);
|
16
23
|
}, [handleReceiveFocus, name, registerReceiveFocus]);
|
17
24
|
const field = useMemo(() => {
|
25
|
+
const error = fieldErrors[name];
|
26
|
+
const getValidationState = () => {
|
27
|
+
if (isValidating)
|
28
|
+
return "validating";
|
29
|
+
if (error)
|
30
|
+
return "invalid";
|
31
|
+
if (!isTouched && !hasBeenSubmitted)
|
32
|
+
return "idle";
|
33
|
+
return "valid";
|
34
|
+
};
|
18
35
|
const helpers = {
|
19
|
-
error
|
36
|
+
error,
|
20
37
|
clearError: () => {
|
21
38
|
clearError(name);
|
22
39
|
},
|
23
|
-
validate: () =>
|
40
|
+
validate: () => {
|
41
|
+
setValidating(true);
|
42
|
+
validateField(name).then((error) => setValidating(false));
|
43
|
+
},
|
24
44
|
defaultValue: defaultValues
|
25
45
|
? get(defaultValues, toPath(name), undefined)
|
26
46
|
: undefined,
|
27
47
|
touched: isTouched,
|
28
48
|
setTouched: (touched) => setFieldTouched(name, touched),
|
49
|
+
validationState: getValidationState(),
|
29
50
|
};
|
30
51
|
const getInputProps = createGetInputProps({
|
31
52
|
...helpers,
|
@@ -44,6 +65,7 @@ export const useField = (name, options) => {
|
|
44
65
|
isTouched,
|
45
66
|
hasBeenSubmitted,
|
46
67
|
options === null || options === void 0 ? void 0 : options.validationBehavior,
|
68
|
+
isValidating,
|
47
69
|
clearError,
|
48
70
|
validateField,
|
49
71
|
setFieldTouched,
|
@@ -52,12 +74,15 @@ export const useField = (name, options) => {
|
|
52
74
|
};
|
53
75
|
/**
|
54
76
|
* Provides access to the entire form context.
|
55
|
-
* This is not usually necessary, but can be useful for advanced use cases.
|
56
77
|
*/
|
57
|
-
export const useFormContext = () =>
|
78
|
+
export const useFormContext = () => useInternalFormContext("useFormContext");
|
58
79
|
/**
|
59
80
|
* Returns whether or not the parent form is currently being submitted.
|
60
81
|
* This is different from remix's `useTransition().submission` in that it
|
61
82
|
* is aware of what form it's in and when _that_ form is being submitted.
|
62
83
|
*/
|
63
|
-
export const useIsSubmitting = () =>
|
84
|
+
export const useIsSubmitting = () => useInternalFormContext("useIsSubmitting").isSubmitting;
|
85
|
+
/**
|
86
|
+
* Returns whether or not the current form is valid.
|
87
|
+
*/
|
88
|
+
export const useIsValid = () => useInternalFormContext("useIsValid").isValid;
|
@@ -1,4 +1 @@
|
|
1
|
-
import { GenericObject } from "..";
|
2
1
|
export declare const objectFromPathEntries: (entries: [string, any][]) => {};
|
3
|
-
/** Flatten an object so there are no nested objects or arrays */
|
4
|
-
export declare function flatten(obj: GenericObject, preserveEmpty?: boolean): GenericObject;
|
@@ -1,40 +1,7 @@
|
|
1
|
-
// `flatten` is taken from https://github.com/richie5um/FlattenJS. Decided to implement them here instead of using that package because this is a core functionality of the library and this will add more flexibility in case we need to change the implementation.
|
2
|
-
import assign from "lodash/assign";
|
3
|
-
import isArray from "lodash/isArray";
|
4
|
-
import isObject from "lodash/isObject";
|
5
|
-
import keys from "lodash/keys";
|
6
|
-
import mapKeys from "lodash/mapKeys";
|
7
1
|
import set from "lodash/set";
|
8
|
-
import transform from "lodash/transform";
|
9
2
|
import { MultiValueMap } from "./MultiValueMap";
|
10
3
|
export const objectFromPathEntries = (entries) => {
|
11
4
|
const map = new MultiValueMap();
|
12
5
|
entries.forEach(([key, value]) => map.add(key, value));
|
13
6
|
return [...map.entries()].reduce((acc, [key, value]) => set(acc, key, value.length === 1 ? value[0] : value), {});
|
14
7
|
};
|
15
|
-
/** Flatten an object so there are no nested objects or arrays */
|
16
|
-
export function flatten(obj, preserveEmpty = false) {
|
17
|
-
return transform(obj, function (result, value, key) {
|
18
|
-
if (isObject(value)) {
|
19
|
-
let flatMap = mapKeys(flatten(value, preserveEmpty), function (_mvalue, mkey) {
|
20
|
-
if (isArray(value)) {
|
21
|
-
let index = mkey.indexOf(".");
|
22
|
-
if (-1 !== index) {
|
23
|
-
return `${key}[${mkey.slice(0, index)}]${mkey.slice(index)}`;
|
24
|
-
}
|
25
|
-
return `${key}[${mkey}]`;
|
26
|
-
}
|
27
|
-
return `${key}.${mkey}`;
|
28
|
-
});
|
29
|
-
assign(result, flatMap);
|
30
|
-
// Preverve Empty arrays and objects
|
31
|
-
if (preserveEmpty && keys(flatMap).length === 0) {
|
32
|
-
result[key] = value;
|
33
|
-
}
|
34
|
-
}
|
35
|
-
else {
|
36
|
-
result[key] = value;
|
37
|
-
}
|
38
|
-
return result;
|
39
|
-
}, {});
|
40
|
-
}
|
@@ -12,7 +12,7 @@ export declare type FormContextValue = {
|
|
12
12
|
/**
|
13
13
|
* Validate the specified field.
|
14
14
|
*/
|
15
|
-
validateField: (fieldName: string) =>
|
15
|
+
validateField: (fieldName: string) => Promise<string | null>;
|
16
16
|
/**
|
17
17
|
* The `action` prop of the form.
|
18
18
|
*/
|
@@ -29,7 +29,6 @@ export declare type FormContextValue = {
|
|
29
29
|
hasBeenSubmitted: boolean;
|
30
30
|
/**
|
31
31
|
* Whether or not the form is valid.
|
32
|
-
* This is a shortcut for `Object.keys(fieldErrors).length === 0`.
|
33
32
|
*/
|
34
33
|
isValid: boolean;
|
35
34
|
/**
|
@@ -52,4 +51,4 @@ export declare type FormContextValue = {
|
|
52
51
|
*/
|
53
52
|
setFieldTouched: (fieldName: string, touched: boolean) => void;
|
54
53
|
};
|
55
|
-
export declare const FormContext: import("react").Context<FormContextValue>;
|
54
|
+
export declare const FormContext: import("react").Context<FormContextValue | null>;
|
@@ -1,12 +1,2 @@
|
|
1
1
|
import { createContext } from "react";
|
2
|
-
export const FormContext = createContext(
|
3
|
-
fieldErrors: {},
|
4
|
-
clearError: () => { },
|
5
|
-
validateField: () => { },
|
6
|
-
isSubmitting: false,
|
7
|
-
hasBeenSubmitted: false,
|
8
|
-
isValid: true,
|
9
|
-
registerReceiveFocus: () => () => { },
|
10
|
-
touchedFields: {},
|
11
|
-
setFieldTouched: () => { },
|
12
|
-
});
|
2
|
+
export const FormContext = createContext(null);
|
@@ -14,10 +14,16 @@ export declare type CreateGetInputPropsOptions = {
|
|
14
14
|
validationBehavior?: Partial<ValidationBehaviorOptions>;
|
15
15
|
name: string;
|
16
16
|
};
|
17
|
-
declare type
|
18
|
-
|
19
|
-
|
17
|
+
declare type HandledProps = "name" | "defaultValue" | "defaultChecked";
|
18
|
+
declare type Callbacks = "onChange" | "onBlur";
|
19
|
+
declare type MinimalInputProps = {
|
20
|
+
onChange?: (...args: any[]) => void;
|
21
|
+
onBlur?: (...args: any[]) => void;
|
22
|
+
defaultValue?: any;
|
23
|
+
defaultChecked?: boolean;
|
24
|
+
name?: string;
|
25
|
+
type?: string;
|
20
26
|
};
|
21
|
-
export declare type GetInputProps = <T extends
|
27
|
+
export declare type GetInputProps = <T extends MinimalInputProps>(props?: Omit<T, HandledProps | Callbacks> & Partial<Pick<T, Callbacks>>) => T;
|
22
28
|
export declare const createGetInputProps: ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }: CreateGetInputPropsOptions) => GetInputProps;
|
23
29
|
export {};
|
@@ -3,18 +3,28 @@ const defaultValidationBehavior = {
|
|
3
3
|
whenTouched: "onChange",
|
4
4
|
whenSubmitted: "onChange",
|
5
5
|
};
|
6
|
+
const getCheckboxDefaultChecked = (value, defaultValue) => {
|
7
|
+
if (Array.isArray(defaultValue))
|
8
|
+
return defaultValue.includes(value);
|
9
|
+
if (typeof defaultValue === "boolean")
|
10
|
+
return defaultValue;
|
11
|
+
if (typeof defaultValue === "string")
|
12
|
+
return defaultValue === value;
|
13
|
+
return undefined;
|
14
|
+
};
|
6
15
|
export const createGetInputProps = ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }) => {
|
7
16
|
const validationBehaviors = {
|
8
17
|
...defaultValidationBehavior,
|
9
18
|
...validationBehavior,
|
10
19
|
};
|
11
20
|
return (props = {}) => {
|
21
|
+
var _a, _b;
|
12
22
|
const behavior = hasBeenSubmitted
|
13
23
|
? validationBehaviors.whenSubmitted
|
14
24
|
: touched
|
15
25
|
? validationBehaviors.whenTouched
|
16
26
|
: validationBehaviors.initial;
|
17
|
-
const
|
27
|
+
const inputProps = {
|
18
28
|
...props,
|
19
29
|
onChange: (...args) => {
|
20
30
|
var _a;
|
@@ -31,9 +41,19 @@ export const createGetInputProps = ({ clearError, validate, defaultValue, touche
|
|
31
41
|
setTouched(true);
|
32
42
|
return (_a = props === null || props === void 0 ? void 0 : props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, ...args);
|
33
43
|
},
|
34
|
-
defaultValue,
|
35
44
|
name,
|
36
45
|
};
|
37
|
-
|
46
|
+
if (inputProps.type === "checkbox") {
|
47
|
+
const value = (_a = props.value) !== null && _a !== void 0 ? _a : "on";
|
48
|
+
inputProps.defaultChecked = getCheckboxDefaultChecked(value, defaultValue);
|
49
|
+
}
|
50
|
+
else if (inputProps.type === "radio") {
|
51
|
+
const value = (_b = props.value) !== null && _b !== void 0 ? _b : "on";
|
52
|
+
inputProps.defaultChecked = defaultValue === value;
|
53
|
+
}
|
54
|
+
else {
|
55
|
+
inputProps.defaultValue = defaultValue;
|
56
|
+
}
|
57
|
+
return inputProps;
|
38
58
|
};
|
39
59
|
};
|
package/browser/server.d.ts
CHANGED
@@ -1,7 +1,16 @@
|
|
1
|
-
import {
|
1
|
+
import { ValidatorError } from "./validation/types";
|
2
2
|
/**
|
3
3
|
* Takes the errors from a `Validator` and returns a `Response`.
|
4
|
-
*
|
5
|
-
*
|
4
|
+
* When you return this from your action, `ValidatedForm` on the frontend will automatically
|
5
|
+
* display the errors on the correct fields on the correct form.
|
6
|
+
*
|
7
|
+
* _Recommended_: You can also provide a second argument to `validationError`
|
8
|
+
* to specify how to repopulate the form when JS is disabled.
|
9
|
+
*
|
10
|
+
* @example
|
11
|
+
* ```ts
|
12
|
+
* const result = validator.validate(await request.formData());
|
13
|
+
* if (result.error) return validationError(result.error, result.submittedData);
|
14
|
+
* ```
|
6
15
|
*/
|
7
|
-
export declare
|
16
|
+
export declare function validationError(error: ValidatorError, repopulateFields?: unknown): Response;
|
package/browser/server.js
CHANGED
@@ -1,17 +1,22 @@
|
|
1
1
|
import { json } from "@remix-run/server-runtime";
|
2
2
|
/**
|
3
3
|
* Takes the errors from a `Validator` and returns a `Response`.
|
4
|
-
*
|
5
|
-
*
|
4
|
+
* When you return this from your action, `ValidatedForm` on the frontend will automatically
|
5
|
+
* display the errors on the correct fields on the correct form.
|
6
|
+
*
|
7
|
+
* _Recommended_: You can also provide a second argument to `validationError`
|
8
|
+
* to specify how to repopulate the form when JS is disabled.
|
9
|
+
*
|
10
|
+
* @example
|
11
|
+
* ```ts
|
12
|
+
* const result = validator.validate(await request.formData());
|
13
|
+
* if (result.error) return validationError(result.error, result.submittedData);
|
14
|
+
* ```
|
6
15
|
*/
|
7
|
-
export
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
}, { status: 422 });
|
15
|
-
}
|
16
|
-
return json({ fieldErrors: errors }, { status: 422 });
|
17
|
-
};
|
16
|
+
export function validationError(error, repopulateFields) {
|
17
|
+
return json({
|
18
|
+
fieldErrors: error.fieldErrors,
|
19
|
+
subaction: error.subaction,
|
20
|
+
repopulateFields,
|
21
|
+
}, { status: 422 });
|
22
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare type ValidationState = "idle" | "validating" | "valid" | "invalid";
|
package/browser/types.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -1,7 +1,7 @@
|
|
1
|
-
import { Validator } from "..";
|
1
|
+
import { CreateValidatorArg, Validator } from "..";
|
2
2
|
/**
|
3
3
|
* Used to create a validator for a form.
|
4
4
|
* It provides built-in handling for unflattening nested objects and
|
5
5
|
* extracting the values from FormData.
|
6
6
|
*/
|
7
|
-
export declare function createValidator<T>(validator:
|
7
|
+
export declare function createValidator<T>(validator: CreateValidatorArg<T>): Validator<T>;
|