remix-validated-form 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.eslintcache +1 -0
- package/README.md +180 -1
- package/browser/ValidatedForm.d.ts +11 -0
- package/browser/ValidatedForm.js +69 -0
- package/browser/hooks.d.ts +8 -0
- package/browser/hooks.js +17 -0
- package/{src/index.ts → browser/index.d.ts} +0 -0
- package/browser/index.js +5 -0
- package/browser/internal/formContext.d.ts +13 -0
- package/browser/internal/formContext.js +7 -0
- package/browser/internal/util.d.ts +3 -0
- package/browser/internal/util.js +19 -0
- package/browser/server.d.ts +2 -0
- package/browser/server.js +2 -0
- package/browser/validation/types.d.ts +15 -0
- package/browser/validation/types.js +1 -0
- package/browser/validation/validation.test.d.ts +1 -0
- package/browser/validation/validation.test.js +56 -0
- package/browser/validation/withYup.d.ts +3 -0
- package/browser/validation/withYup.js +34 -0
- package/build/ValidatedForm.d.ts +11 -0
- package/build/ValidatedForm.js +76 -0
- package/build/hooks.d.ts +8 -0
- package/build/hooks.js +23 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +17 -0
- package/build/internal/formContext.d.ts +13 -0
- package/build/internal/formContext.js +10 -0
- package/build/internal/util.d.ts +3 -0
- package/build/internal/util.js +24 -0
- package/build/server.d.ts +2 -0
- package/build/server.js +6 -0
- package/build/validation/types.d.ts +15 -0
- package/build/validation/types.js +2 -0
- package/build/validation/validation.test.d.ts +1 -0
- package/build/validation/validation.test.js +77 -0
- package/build/validation/withYup.d.ts +3 -0
- package/build/validation/withYup.js +38 -0
- package/package.json +10 -5
- package/.eslintrc.js +0 -46
- package/.github/workflows/test.yml +0 -35
- package/.husky/pre-commit +0 -4
- package/src/ValidatedForm.tsx +0 -130
- package/src/hooks.ts +0 -27
- package/src/internal/formContext.ts +0 -18
- package/src/internal/util.ts +0 -23
- package/src/server.ts +0 -5
- package/src/validation/types.ts +0 -12
- package/src/validation/validation.test.ts +0 -76
- package/src/validation/withYup.ts +0 -37
- package/test-app/README.md +0 -53
- package/test-app/app/components/Input.tsx +0 -24
- package/test-app/app/components/SubmitButton.tsx +0 -18
- package/test-app/app/entry.client.tsx +0 -4
- package/test-app/app/entry.server.tsx +0 -21
- package/test-app/app/root.tsx +0 -246
- package/test-app/app/routes/default-values.tsx +0 -34
- package/test-app/app/routes/index.tsx +0 -100
- package/test-app/app/routes/noscript.tsx +0 -10
- package/test-app/app/routes/submission.alt.tsx +0 -6
- package/test-app/app/routes/submission.fetcher.tsx +0 -6
- package/test-app/app/routes/submission.tsx +0 -47
- package/test-app/app/routes/validation.tsx +0 -40
- package/test-app/app/styles/dark.css +0 -7
- package/test-app/app/styles/demos/about.css +0 -26
- package/test-app/app/styles/demos/remix.css +0 -120
- package/test-app/app/styles/global.css +0 -98
- package/test-app/cypress/fixtures/example.json +0 -5
- package/test-app/cypress/integration/default-values.ts +0 -15
- package/test-app/cypress/integration/sanity.ts +0 -19
- package/test-app/cypress/integration/submission.ts +0 -26
- package/test-app/cypress/integration/validation.ts +0 -70
- package/test-app/cypress/plugins/config.ts +0 -38
- package/test-app/cypress/plugins/index.ts +0 -9
- package/test-app/cypress/support/commands/index.ts +0 -13
- package/test-app/cypress/support/commands/types.d.ts +0 -11
- package/test-app/cypress/support/index.ts +0 -20
- package/test-app/cypress/tsconfig.json +0 -11
- package/test-app/cypress.json +0 -3
- package/test-app/package-lock.json +0 -11675
- package/test-app/package.json +0 -40
- package/test-app/public/favicon.ico +0 -0
- package/test-app/remix.config.js +0 -10
- package/test-app/remix.env.d.ts +0 -2
- package/test-app/tsconfig.json +0 -18
- package/tsconfig.json +0 -15
package/.eslintcache
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
[{"/Users/aaronpettengill/dev/remix-validated-form/src/server.ts":"1","/Users/aaronpettengill/dev/remix-validated-form/src/validation/types.ts":"2","/Users/aaronpettengill/dev/remix-validated-form/src/validation/withYup.ts":"3","/Users/aaronpettengill/dev/remix-validated-form/test-app/app/routes/validation.tsx":"4","/Users/aaronpettengill/dev/remix-validated-form/test-app/app/routes/validation-fetcher.tsx":"5","/Users/aaronpettengill/dev/remix-validated-form/test-app/cypress/integration/validation-with-fetchers.ts":"6"},{"size":207,"mtime":1637876506168,"results":"7","hashOfConfig":"8"},{"size":438,"mtime":1637877226360,"results":"9","hashOfConfig":"8"},{"size":1085,"mtime":1637877476573,"results":"10","hashOfConfig":"8"},{"size":1293,"mtime":1637876566355,"results":"11","hashOfConfig":"8"},{"size":1330,"mtime":1637902697212,"results":"12","hashOfConfig":"8"},{"size":2220,"mtime":1637902736281,"results":"13","hashOfConfig":"8"},{"filePath":"14","messages":"15","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"bt07le",{"filePath":"16","messages":"17","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"18","messages":"19","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"20","messages":"21","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"22","messages":"23","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"24","messages":"25","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/aaronpettengill/dev/remix-validated-form/src/server.ts",[],"/Users/aaronpettengill/dev/remix-validated-form/src/validation/types.ts",[],"/Users/aaronpettengill/dev/remix-validated-form/src/validation/withYup.ts",["26"],"/Users/aaronpettengill/dev/remix-validated-form/test-app/app/routes/validation.tsx",[],"/Users/aaronpettengill/dev/remix-validated-form/test-app/app/routes/validation-fetcher.tsx",["27"],"/Users/aaronpettengill/dev/remix-validated-form/test-app/cypress/integration/validation-with-fetchers.ts",[],{"ruleId":"28","severity":1,"message":"29","line":2,"column":23,"nodeType":"30","messageId":"31","endLine":2,"endColumn":39},{"ruleId":"28","severity":1,"message":"32","line":1,"column":26,"nodeType":"30","messageId":"31","endLine":1,"endColumn":39},"@typescript-eslint/no-unused-vars","'ValidationResult' is defined but never used.","Identifier","unusedVar","'useActionData' is defined but never used."]
|
package/README.md
CHANGED
@@ -1,3 +1,182 @@
|
|
1
1
|
# Remix Validated Form
|
2
2
|
|
3
|
-
A form library built for [remix](https://remix.run).
|
3
|
+
A form library built for [remix](https://remix.run) to make validation easy.
|
4
|
+
|
5
|
+
- Client-side, field-by-field validation (e.g. validate on blur) and form-level validation
|
6
|
+
- Set default values for the entire form in one place
|
7
|
+
- Re-use validation on the server
|
8
|
+
- Show validation errors from the server even without JS
|
9
|
+
- Detect if the current form is submitting when there are multiple forms on the page
|
10
|
+
- Validation library agnostic
|
11
|
+
|
12
|
+
# Demo
|
13
|
+
|
14
|
+
https://user-images.githubusercontent.com/25882770/143505448-c4b7e660-7a73-4005-b2ca-17c65a15ef46.mov
|
15
|
+
|
16
|
+
# Getting started
|
17
|
+
|
18
|
+
## Install
|
19
|
+
|
20
|
+
```bash
|
21
|
+
npm install remix-validated-form
|
22
|
+
```
|
23
|
+
|
24
|
+
## Create an input component
|
25
|
+
|
26
|
+
In order to display field errors or do field-by-field validation,
|
27
|
+
it's recommended to incorporate this library into an input component using `useField`.
|
28
|
+
|
29
|
+
```tsx
|
30
|
+
import { useField } from "remix-validated-form";
|
31
|
+
|
32
|
+
type MyInputProps = {
|
33
|
+
name: string;
|
34
|
+
label: string;
|
35
|
+
};
|
36
|
+
|
37
|
+
export const MyInput = ({ name, label }: InputProps) => {
|
38
|
+
const { validate, clearError, defaultValue, error } = useField(name);
|
39
|
+
return (
|
40
|
+
<div>
|
41
|
+
<label htmlFor={name}>{label}</label>
|
42
|
+
<input
|
43
|
+
id={name}
|
44
|
+
name={name}
|
45
|
+
onBlur={validate}
|
46
|
+
onChange={clearError}
|
47
|
+
defaultValue={defaultValue}
|
48
|
+
/>
|
49
|
+
{error && <span className="my-error-class">{error}</span>}
|
50
|
+
</div>
|
51
|
+
);
|
52
|
+
};
|
53
|
+
```
|
54
|
+
|
55
|
+
## Create a submit button component
|
56
|
+
|
57
|
+
To best take advantage of the per-form submission detection, we can create a submit button component.
|
58
|
+
|
59
|
+
```tsx
|
60
|
+
import { useIsSubmitting } from "../../remix-validated-form";
|
61
|
+
|
62
|
+
export const MySubmitButton = () => {
|
63
|
+
const isSubmitting = useIsSubmitting();
|
64
|
+
return (
|
65
|
+
<button type="submit" disabled={isSubmitting}>
|
66
|
+
{isSubmitting ? "Submitting..." : "Submit"}
|
67
|
+
</button>
|
68
|
+
);
|
69
|
+
};
|
70
|
+
```
|
71
|
+
|
72
|
+
## Use the form!
|
73
|
+
|
74
|
+
Now that we have our components, making a form is easy!
|
75
|
+
|
76
|
+
```tsx
|
77
|
+
import { ActionFunction, LoaderFunction, redirect, useLoaderData } from "remix";
|
78
|
+
import * as yup from "yup";
|
79
|
+
import { validationError, ValidatedForm, withYup } from "remix-validated-form";
|
80
|
+
import { MyInput, MySubmitButton } from "~/components/Input";
|
81
|
+
|
82
|
+
// Using yup in this example, but you can use anything
|
83
|
+
const validator = withYup(
|
84
|
+
yup.object({
|
85
|
+
firstName: yup.string().label("First Name").required(),
|
86
|
+
lastName: yup.string().label("Last Name").required(),
|
87
|
+
email: yup.string().email().label("Email").required(),
|
88
|
+
})
|
89
|
+
);
|
90
|
+
|
91
|
+
export const action: ActionFunction = async ({ request }) => {
|
92
|
+
const fieldValues = validator.validate(
|
93
|
+
Object.fromEntries(await request.formData())
|
94
|
+
);
|
95
|
+
if (fieldValues.error) return validationError(fieldValues.error);
|
96
|
+
const { firstName, lastName, email } = fieldValues.data;
|
97
|
+
|
98
|
+
// Do something with correctly typed values;
|
99
|
+
|
100
|
+
return redirect("/");
|
101
|
+
};
|
102
|
+
|
103
|
+
export const loader: LoaderFunction = () => {
|
104
|
+
return {
|
105
|
+
defaultValues: {
|
106
|
+
firstName: "Jane",
|
107
|
+
lastName: "Doe",
|
108
|
+
email: "jane.doe@example.com",
|
109
|
+
},
|
110
|
+
};
|
111
|
+
};
|
112
|
+
|
113
|
+
export default function MyForm() {
|
114
|
+
const { defaultValues } = useLoaderData();
|
115
|
+
return (
|
116
|
+
<ValidatedForm
|
117
|
+
validator={validator}
|
118
|
+
method="post"
|
119
|
+
defaultValues={defaultValues}
|
120
|
+
>
|
121
|
+
<MyInput name="firstName" label="First Name" />
|
122
|
+
<MyInput name="lastName" label="Last Name" />
|
123
|
+
<MyInput name="email" label="Email" />
|
124
|
+
<MySubmitButton />
|
125
|
+
</ValidatedForm>
|
126
|
+
);
|
127
|
+
}
|
128
|
+
```
|
129
|
+
|
130
|
+
# Validation Library Support
|
131
|
+
|
132
|
+
This library currently includes an out-of-the-box adapter for `yup`,
|
133
|
+
but you can easily support whatever library you want by creating your own adapter.
|
134
|
+
|
135
|
+
And if you create an adapter for a library, feel free to make a PR on this library to add official support 😊
|
136
|
+
|
137
|
+
## Creating an adapter
|
138
|
+
|
139
|
+
Any object that conforms to the `Validator` type can be passed into the the `ValidatedForm`'s `validator` prop.
|
140
|
+
|
141
|
+
```ts
|
142
|
+
type FieldErrors = Record<string, string>;
|
143
|
+
|
144
|
+
type ValidationResult<DataType> =
|
145
|
+
| { data: DataType; error: undefined }
|
146
|
+
| { error: FieldErrors; data: undefined };
|
147
|
+
|
148
|
+
type ValidateFieldResult = { error?: string };
|
149
|
+
|
150
|
+
type Validator<DataType> = {
|
151
|
+
validate: (unvalidatedData: unknown) => ValidationResult<DataType>;
|
152
|
+
validateField: (
|
153
|
+
unvalidatedData: unknown,
|
154
|
+
field: string
|
155
|
+
) => ValidateFieldResult;
|
156
|
+
};
|
157
|
+
```
|
158
|
+
|
159
|
+
In order to make an adapter for your validation library of choice,
|
160
|
+
you can create a function that accepts a schema from the validation library and turns it into a validator.
|
161
|
+
|
162
|
+
The out-of-the-box support for `yup` in this library works like this:
|
163
|
+
|
164
|
+
```ts
|
165
|
+
export const withYup = <Schema extends AnyObjectSchema>(
|
166
|
+
validationSchema: Schema
|
167
|
+
// For best result with Typescript, we should type the `Validator` we return based on the provided schema
|
168
|
+
): Validator<InferType<Schema>> => ({
|
169
|
+
validate: (unvalidatedData) => {
|
170
|
+
// Validate with yup and return the validated & typed data or the error
|
171
|
+
|
172
|
+
if (isValid) return { data: { field1: "someValue" }, error: undefined };
|
173
|
+
else return { error: { field1: "Some error!" }, data: undefined };
|
174
|
+
},
|
175
|
+
validateField: (unvalidatedData, field) => {
|
176
|
+
// Validate the specific field with yup
|
177
|
+
|
178
|
+
if (isValid) return { error: undefined };
|
179
|
+
else return { error: "Some error" };
|
180
|
+
},
|
181
|
+
});
|
182
|
+
```
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import { Form as RemixForm, useFetcher } from "@remix-run/react";
|
2
|
+
import React, { ComponentProps } from "react";
|
3
|
+
import { Validator } from "./validation/types";
|
4
|
+
export declare type FormProps<DataType> = {
|
5
|
+
validator: Validator<DataType>;
|
6
|
+
onSubmit?: (data: DataType, event: React.FormEvent<HTMLFormElement>) => void;
|
7
|
+
fetcher?: ReturnType<typeof useFetcher>;
|
8
|
+
defaultValues?: Partial<DataType>;
|
9
|
+
formRef?: React.RefObject<HTMLFormElement>;
|
10
|
+
} & Omit<ComponentProps<typeof RemixForm>, "onSubmit">;
|
11
|
+
export declare function ValidatedForm<DataType>({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, ...rest }: FormProps<DataType>): JSX.Element;
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
2
|
+
import { Form as RemixForm, useActionData, useFormAction, useTransition, } from "@remix-run/react";
|
3
|
+
import { useEffect, useMemo, useRef, useState, } from "react";
|
4
|
+
import invariant from "tiny-invariant";
|
5
|
+
import { FormContext } from "./internal/formContext";
|
6
|
+
import { omit, mergeRefs } from "./internal/util";
|
7
|
+
function useFieldErrors(fetcher) {
|
8
|
+
const actionData = useActionData();
|
9
|
+
const dataToUse = fetcher ? fetcher.data : actionData;
|
10
|
+
const fieldErrorsFromAction = dataToUse === null || dataToUse === void 0 ? void 0 : dataToUse.fieldErrors;
|
11
|
+
const [fieldErrors, setFieldErrors] = useState(fieldErrorsFromAction !== null && fieldErrorsFromAction !== void 0 ? fieldErrorsFromAction : {});
|
12
|
+
useEffect(() => {
|
13
|
+
if (fieldErrorsFromAction)
|
14
|
+
setFieldErrors(fieldErrorsFromAction);
|
15
|
+
}, [fieldErrorsFromAction]);
|
16
|
+
return [fieldErrors, setFieldErrors];
|
17
|
+
}
|
18
|
+
const useIsSubmitting = (action, fetcher) => {
|
19
|
+
const actionForCurrentPage = useFormAction();
|
20
|
+
const pendingFormSubmit = useTransition().submission;
|
21
|
+
return fetcher
|
22
|
+
? fetcher.state === "submitting"
|
23
|
+
: pendingFormSubmit &&
|
24
|
+
pendingFormSubmit.action.endsWith(action !== null && action !== void 0 ? action : actionForCurrentPage);
|
25
|
+
};
|
26
|
+
const getDataFromForm = (el) => Object.fromEntries(new FormData(el));
|
27
|
+
export function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, ...rest }) {
|
28
|
+
var _a;
|
29
|
+
const [fieldErrors, setFieldErrors] = useFieldErrors(fetcher);
|
30
|
+
const isSubmitting = useIsSubmitting(action, fetcher);
|
31
|
+
const formRef = useRef(null);
|
32
|
+
const contextValue = useMemo(() => ({
|
33
|
+
fieldErrors,
|
34
|
+
action,
|
35
|
+
defaultValues,
|
36
|
+
isSubmitting: isSubmitting !== null && isSubmitting !== void 0 ? isSubmitting : false,
|
37
|
+
clearError: (fieldName) => {
|
38
|
+
setFieldErrors((prev) => omit(prev, fieldName));
|
39
|
+
},
|
40
|
+
validateField: (fieldName) => {
|
41
|
+
invariant(formRef.current, "Cannot find reference to form");
|
42
|
+
const { error } = validator.validateField(getDataFromForm(formRef.current), fieldName);
|
43
|
+
if (error) {
|
44
|
+
setFieldErrors((prev) => ({
|
45
|
+
...prev,
|
46
|
+
[fieldName]: error,
|
47
|
+
}));
|
48
|
+
}
|
49
|
+
},
|
50
|
+
}), [
|
51
|
+
fieldErrors,
|
52
|
+
action,
|
53
|
+
defaultValues,
|
54
|
+
isSubmitting,
|
55
|
+
setFieldErrors,
|
56
|
+
validator,
|
57
|
+
]);
|
58
|
+
const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : RemixForm;
|
59
|
+
return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, action: action, onSubmit: (event) => {
|
60
|
+
const result = validator.validate(getDataFromForm(event.currentTarget));
|
61
|
+
if (result.error) {
|
62
|
+
event.preventDefault();
|
63
|
+
setFieldErrors(result.error);
|
64
|
+
}
|
65
|
+
else {
|
66
|
+
onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(result.data, event);
|
67
|
+
}
|
68
|
+
}, children: _jsx(FormContext.Provider, { value: contextValue, children: children }, void 0) }, void 0));
|
69
|
+
}
|
@@ -0,0 +1,8 @@
|
|
1
|
+
export declare const useField: (name: string) => {
|
2
|
+
error: string;
|
3
|
+
clearError: () => void;
|
4
|
+
validate: () => void;
|
5
|
+
defaultValue: any;
|
6
|
+
};
|
7
|
+
export declare const useFormContext: () => import("./internal/formContext").FormContextValue;
|
8
|
+
export declare const useIsSubmitting: () => boolean;
|
package/browser/hooks.js
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
import { useContext, useMemo } from "react";
|
2
|
+
import { FormContext } from "./internal/formContext";
|
3
|
+
export const useField = (name) => {
|
4
|
+
const { fieldErrors, clearError, validateField, defaultValues } = useContext(FormContext);
|
5
|
+
const field = useMemo(() => ({
|
6
|
+
error: fieldErrors[name],
|
7
|
+
clearError: () => {
|
8
|
+
clearError(name);
|
9
|
+
},
|
10
|
+
validate: () => validateField(name),
|
11
|
+
defaultValue: defaultValues === null || defaultValues === void 0 ? void 0 : defaultValues[name],
|
12
|
+
}), [clearError, defaultValues, fieldErrors, name, validateField]);
|
13
|
+
return field;
|
14
|
+
};
|
15
|
+
// test commit
|
16
|
+
export const useFormContext = () => useContext(FormContext);
|
17
|
+
export const useIsSubmitting = () => useFormContext().isSubmitting;
|
File without changes
|
package/browser/index.js
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
/// <reference types="react" />
|
2
|
+
import { FieldErrors } from "../validation/types";
|
3
|
+
export declare type FormContextValue = {
|
4
|
+
fieldErrors: FieldErrors;
|
5
|
+
clearError: (...names: string[]) => void;
|
6
|
+
validateField: (fieldName: string) => void;
|
7
|
+
action?: string;
|
8
|
+
isSubmitting: boolean;
|
9
|
+
defaultValues?: {
|
10
|
+
[fieldName: string]: any;
|
11
|
+
};
|
12
|
+
};
|
13
|
+
export declare const FormContext: import("react").Context<FormContextValue>;
|
@@ -0,0 +1,19 @@
|
|
1
|
+
export const omit = (obj, ...keys) => {
|
2
|
+
const result = { ...obj };
|
3
|
+
for (const key of keys) {
|
4
|
+
delete result[key];
|
5
|
+
}
|
6
|
+
return result;
|
7
|
+
};
|
8
|
+
export const mergeRefs = (refs) => {
|
9
|
+
return (value) => {
|
10
|
+
refs.filter(Boolean).forEach((ref) => {
|
11
|
+
if (typeof ref === "function") {
|
12
|
+
ref(value);
|
13
|
+
}
|
14
|
+
else if (ref != null) {
|
15
|
+
ref.current = value;
|
16
|
+
}
|
17
|
+
});
|
18
|
+
};
|
19
|
+
};
|
@@ -0,0 +1,15 @@
|
|
1
|
+
export declare type FieldErrors = Record<string, string>;
|
2
|
+
export declare type ValidationResult<DataType> = {
|
3
|
+
data: DataType;
|
4
|
+
error: undefined;
|
5
|
+
} | {
|
6
|
+
error: FieldErrors;
|
7
|
+
data: undefined;
|
8
|
+
};
|
9
|
+
export declare type ValidateFieldResult = {
|
10
|
+
error?: string;
|
11
|
+
};
|
12
|
+
export declare type Validator<DataType> = {
|
13
|
+
validate: (unvalidatedData: unknown) => ValidationResult<DataType>;
|
14
|
+
validateField: (unvalidatedData: unknown, field: string) => ValidateFieldResult;
|
15
|
+
};
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1,56 @@
|
|
1
|
+
import * as yup from "yup";
|
2
|
+
import { withYup } from "..";
|
3
|
+
const validationTestCases = [
|
4
|
+
{
|
5
|
+
name: "yup",
|
6
|
+
validator: withYup(yup.object({
|
7
|
+
firstName: yup.string().required(),
|
8
|
+
lastName: yup.string().required(),
|
9
|
+
age: yup.number(),
|
10
|
+
})),
|
11
|
+
},
|
12
|
+
];
|
13
|
+
// Not going to enforce exact error strings here
|
14
|
+
const anyString = expect.any(String);
|
15
|
+
describe("Validation", () => {
|
16
|
+
describe.each(validationTestCases)("Adapter for $name", ({ validator }) => {
|
17
|
+
describe("validate", () => {
|
18
|
+
it("should return the data when valid", () => {
|
19
|
+
const obj = {
|
20
|
+
firstName: "John",
|
21
|
+
lastName: "Doe",
|
22
|
+
age: 30,
|
23
|
+
};
|
24
|
+
expect(validator.validate(obj)).toEqual({
|
25
|
+
data: obj,
|
26
|
+
error: undefined,
|
27
|
+
});
|
28
|
+
});
|
29
|
+
it("should return field errors when invalid", () => {
|
30
|
+
const obj = { age: "hi!" };
|
31
|
+
expect(validator.validate(obj)).toEqual({
|
32
|
+
data: undefined,
|
33
|
+
error: {
|
34
|
+
firstName: anyString,
|
35
|
+
lastName: anyString,
|
36
|
+
age: anyString,
|
37
|
+
},
|
38
|
+
});
|
39
|
+
});
|
40
|
+
});
|
41
|
+
describe("validateField", () => {
|
42
|
+
it("should not return an error if field is valid", () => {
|
43
|
+
const obj = { firstName: "John", lastName: 123 };
|
44
|
+
expect(validator.validateField(obj, "firstName")).toEqual({
|
45
|
+
error: undefined,
|
46
|
+
});
|
47
|
+
});
|
48
|
+
it("should return an error if field is invalid", () => {
|
49
|
+
const obj = { firstName: "John", lastName: {} };
|
50
|
+
expect(validator.validateField(obj, "lastName")).toEqual({
|
51
|
+
error: anyString,
|
52
|
+
});
|
53
|
+
});
|
54
|
+
});
|
55
|
+
});
|
56
|
+
});
|
@@ -0,0 +1,34 @@
|
|
1
|
+
const validationErrorToFieldErrors = (error) => {
|
2
|
+
const fieldErrors = {};
|
3
|
+
error.inner.forEach((innerError) => {
|
4
|
+
if (!innerError.path)
|
5
|
+
return;
|
6
|
+
fieldErrors[innerError.path] = innerError.message;
|
7
|
+
});
|
8
|
+
return fieldErrors;
|
9
|
+
};
|
10
|
+
export const withYup = (validationSchema) => ({
|
11
|
+
validate: (data) => {
|
12
|
+
try {
|
13
|
+
const validated = validationSchema.validateSync(data, {
|
14
|
+
abortEarly: false,
|
15
|
+
});
|
16
|
+
return { data: validated, error: undefined };
|
17
|
+
}
|
18
|
+
catch (err) {
|
19
|
+
return {
|
20
|
+
error: validationErrorToFieldErrors(err),
|
21
|
+
data: undefined,
|
22
|
+
};
|
23
|
+
}
|
24
|
+
},
|
25
|
+
validateField: (data, field) => {
|
26
|
+
try {
|
27
|
+
validationSchema.validateSyncAt(field, data);
|
28
|
+
return {};
|
29
|
+
}
|
30
|
+
catch (err) {
|
31
|
+
return { error: err.message };
|
32
|
+
}
|
33
|
+
},
|
34
|
+
});
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import { Form as RemixForm, useFetcher } from "@remix-run/react";
|
2
|
+
import React, { ComponentProps } from "react";
|
3
|
+
import { Validator } from "./validation/types";
|
4
|
+
export declare type FormProps<DataType> = {
|
5
|
+
validator: Validator<DataType>;
|
6
|
+
onSubmit?: (data: DataType, event: React.FormEvent<HTMLFormElement>) => void;
|
7
|
+
fetcher?: ReturnType<typeof useFetcher>;
|
8
|
+
defaultValues?: Partial<DataType>;
|
9
|
+
formRef?: React.RefObject<HTMLFormElement>;
|
10
|
+
} & Omit<ComponentProps<typeof RemixForm>, "onSubmit">;
|
11
|
+
export declare function ValidatedForm<DataType>({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, ...rest }: FormProps<DataType>): JSX.Element;
|
@@ -0,0 +1,76 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.ValidatedForm = void 0;
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
8
|
+
const react_1 = require("@remix-run/react");
|
9
|
+
const react_2 = require("react");
|
10
|
+
const tiny_invariant_1 = __importDefault(require("tiny-invariant"));
|
11
|
+
const formContext_1 = require("./internal/formContext");
|
12
|
+
const util_1 = require("./internal/util");
|
13
|
+
function useFieldErrors(fetcher) {
|
14
|
+
const actionData = (0, react_1.useActionData)();
|
15
|
+
const dataToUse = fetcher ? fetcher.data : actionData;
|
16
|
+
const fieldErrorsFromAction = dataToUse === null || dataToUse === void 0 ? void 0 : dataToUse.fieldErrors;
|
17
|
+
const [fieldErrors, setFieldErrors] = (0, react_2.useState)(fieldErrorsFromAction !== null && fieldErrorsFromAction !== void 0 ? fieldErrorsFromAction : {});
|
18
|
+
(0, react_2.useEffect)(() => {
|
19
|
+
if (fieldErrorsFromAction)
|
20
|
+
setFieldErrors(fieldErrorsFromAction);
|
21
|
+
}, [fieldErrorsFromAction]);
|
22
|
+
return [fieldErrors, setFieldErrors];
|
23
|
+
}
|
24
|
+
const useIsSubmitting = (action, fetcher) => {
|
25
|
+
const actionForCurrentPage = (0, react_1.useFormAction)();
|
26
|
+
const pendingFormSubmit = (0, react_1.useTransition)().submission;
|
27
|
+
return fetcher
|
28
|
+
? fetcher.state === "submitting"
|
29
|
+
: pendingFormSubmit &&
|
30
|
+
pendingFormSubmit.action.endsWith(action !== null && action !== void 0 ? action : actionForCurrentPage);
|
31
|
+
};
|
32
|
+
const getDataFromForm = (el) => Object.fromEntries(new FormData(el));
|
33
|
+
function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues, formRef: formRefProp, ...rest }) {
|
34
|
+
var _a;
|
35
|
+
const [fieldErrors, setFieldErrors] = useFieldErrors(fetcher);
|
36
|
+
const isSubmitting = useIsSubmitting(action, fetcher);
|
37
|
+
const formRef = (0, react_2.useRef)(null);
|
38
|
+
const contextValue = (0, react_2.useMemo)(() => ({
|
39
|
+
fieldErrors,
|
40
|
+
action,
|
41
|
+
defaultValues,
|
42
|
+
isSubmitting: isSubmitting !== null && isSubmitting !== void 0 ? isSubmitting : false,
|
43
|
+
clearError: (fieldName) => {
|
44
|
+
setFieldErrors((prev) => (0, util_1.omit)(prev, fieldName));
|
45
|
+
},
|
46
|
+
validateField: (fieldName) => {
|
47
|
+
(0, tiny_invariant_1.default)(formRef.current, "Cannot find reference to form");
|
48
|
+
const { error } = validator.validateField(getDataFromForm(formRef.current), fieldName);
|
49
|
+
if (error) {
|
50
|
+
setFieldErrors((prev) => ({
|
51
|
+
...prev,
|
52
|
+
[fieldName]: error,
|
53
|
+
}));
|
54
|
+
}
|
55
|
+
},
|
56
|
+
}), [
|
57
|
+
fieldErrors,
|
58
|
+
action,
|
59
|
+
defaultValues,
|
60
|
+
isSubmitting,
|
61
|
+
setFieldErrors,
|
62
|
+
validator,
|
63
|
+
]);
|
64
|
+
const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : react_1.Form;
|
65
|
+
return ((0, jsx_runtime_1.jsx)(Form, { ref: (0, util_1.mergeRefs)([formRef, formRefProp]), ...rest, action: action, onSubmit: (event) => {
|
66
|
+
const result = validator.validate(getDataFromForm(event.currentTarget));
|
67
|
+
if (result.error) {
|
68
|
+
event.preventDefault();
|
69
|
+
setFieldErrors(result.error);
|
70
|
+
}
|
71
|
+
else {
|
72
|
+
onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(result.data, event);
|
73
|
+
}
|
74
|
+
}, children: (0, jsx_runtime_1.jsx)(formContext_1.FormContext.Provider, { value: contextValue, children: children }, void 0) }, void 0));
|
75
|
+
}
|
76
|
+
exports.ValidatedForm = ValidatedForm;
|
package/build/hooks.d.ts
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
export declare const useField: (name: string) => {
|
2
|
+
error: string;
|
3
|
+
clearError: () => void;
|
4
|
+
validate: () => void;
|
5
|
+
defaultValue: any;
|
6
|
+
};
|
7
|
+
export declare const useFormContext: () => import("./internal/formContext").FormContextValue;
|
8
|
+
export declare const useIsSubmitting: () => boolean;
|
package/build/hooks.js
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.useIsSubmitting = exports.useFormContext = exports.useField = void 0;
|
4
|
+
const react_1 = require("react");
|
5
|
+
const formContext_1 = require("./internal/formContext");
|
6
|
+
const useField = (name) => {
|
7
|
+
const { fieldErrors, clearError, validateField, defaultValues } = (0, react_1.useContext)(formContext_1.FormContext);
|
8
|
+
const field = (0, react_1.useMemo)(() => ({
|
9
|
+
error: fieldErrors[name],
|
10
|
+
clearError: () => {
|
11
|
+
clearError(name);
|
12
|
+
},
|
13
|
+
validate: () => validateField(name),
|
14
|
+
defaultValue: defaultValues === null || defaultValues === void 0 ? void 0 : defaultValues[name],
|
15
|
+
}), [clearError, defaultValues, fieldErrors, name, validateField]);
|
16
|
+
return field;
|
17
|
+
};
|
18
|
+
exports.useField = useField;
|
19
|
+
// test commit
|
20
|
+
const useFormContext = () => (0, react_1.useContext)(formContext_1.FormContext);
|
21
|
+
exports.useFormContext = useFormContext;
|
22
|
+
const useIsSubmitting = () => (0, exports.useFormContext)().isSubmitting;
|
23
|
+
exports.useIsSubmitting = useIsSubmitting;
|
package/build/index.d.ts
ADDED
package/build/index.js
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
5
|
+
}) : (function(o, m, k, k2) {
|
6
|
+
if (k2 === undefined) k2 = k;
|
7
|
+
o[k2] = m[k];
|
8
|
+
}));
|
9
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
10
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
11
|
+
};
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
13
|
+
__exportStar(require("./hooks"), exports);
|
14
|
+
__exportStar(require("./server"), exports);
|
15
|
+
__exportStar(require("./ValidatedForm"), exports);
|
16
|
+
__exportStar(require("./validation/types"), exports);
|
17
|
+
__exportStar(require("./validation/withYup"), exports);
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/// <reference types="react" />
|
2
|
+
import { FieldErrors } from "../validation/types";
|
3
|
+
export declare type FormContextValue = {
|
4
|
+
fieldErrors: FieldErrors;
|
5
|
+
clearError: (...names: string[]) => void;
|
6
|
+
validateField: (fieldName: string) => void;
|
7
|
+
action?: string;
|
8
|
+
isSubmitting: boolean;
|
9
|
+
defaultValues?: {
|
10
|
+
[fieldName: string]: any;
|
11
|
+
};
|
12
|
+
};
|
13
|
+
export declare const FormContext: import("react").Context<FormContextValue>;
|