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.
Files changed (86) hide show
  1. package/.eslintcache +1 -0
  2. package/README.md +180 -1
  3. package/browser/ValidatedForm.d.ts +11 -0
  4. package/browser/ValidatedForm.js +69 -0
  5. package/browser/hooks.d.ts +8 -0
  6. package/browser/hooks.js +17 -0
  7. package/{src/index.ts → browser/index.d.ts} +0 -0
  8. package/browser/index.js +5 -0
  9. package/browser/internal/formContext.d.ts +13 -0
  10. package/browser/internal/formContext.js +7 -0
  11. package/browser/internal/util.d.ts +3 -0
  12. package/browser/internal/util.js +19 -0
  13. package/browser/server.d.ts +2 -0
  14. package/browser/server.js +2 -0
  15. package/browser/validation/types.d.ts +15 -0
  16. package/browser/validation/types.js +1 -0
  17. package/browser/validation/validation.test.d.ts +1 -0
  18. package/browser/validation/validation.test.js +56 -0
  19. package/browser/validation/withYup.d.ts +3 -0
  20. package/browser/validation/withYup.js +34 -0
  21. package/build/ValidatedForm.d.ts +11 -0
  22. package/build/ValidatedForm.js +76 -0
  23. package/build/hooks.d.ts +8 -0
  24. package/build/hooks.js +23 -0
  25. package/build/index.d.ts +5 -0
  26. package/build/index.js +17 -0
  27. package/build/internal/formContext.d.ts +13 -0
  28. package/build/internal/formContext.js +10 -0
  29. package/build/internal/util.d.ts +3 -0
  30. package/build/internal/util.js +24 -0
  31. package/build/server.d.ts +2 -0
  32. package/build/server.js +6 -0
  33. package/build/validation/types.d.ts +15 -0
  34. package/build/validation/types.js +2 -0
  35. package/build/validation/validation.test.d.ts +1 -0
  36. package/build/validation/validation.test.js +77 -0
  37. package/build/validation/withYup.d.ts +3 -0
  38. package/build/validation/withYup.js +38 -0
  39. package/package.json +10 -5
  40. package/.eslintrc.js +0 -46
  41. package/.github/workflows/test.yml +0 -35
  42. package/.husky/pre-commit +0 -4
  43. package/src/ValidatedForm.tsx +0 -130
  44. package/src/hooks.ts +0 -27
  45. package/src/internal/formContext.ts +0 -18
  46. package/src/internal/util.ts +0 -23
  47. package/src/server.ts +0 -5
  48. package/src/validation/types.ts +0 -12
  49. package/src/validation/validation.test.ts +0 -76
  50. package/src/validation/withYup.ts +0 -37
  51. package/test-app/README.md +0 -53
  52. package/test-app/app/components/Input.tsx +0 -24
  53. package/test-app/app/components/SubmitButton.tsx +0 -18
  54. package/test-app/app/entry.client.tsx +0 -4
  55. package/test-app/app/entry.server.tsx +0 -21
  56. package/test-app/app/root.tsx +0 -246
  57. package/test-app/app/routes/default-values.tsx +0 -34
  58. package/test-app/app/routes/index.tsx +0 -100
  59. package/test-app/app/routes/noscript.tsx +0 -10
  60. package/test-app/app/routes/submission.alt.tsx +0 -6
  61. package/test-app/app/routes/submission.fetcher.tsx +0 -6
  62. package/test-app/app/routes/submission.tsx +0 -47
  63. package/test-app/app/routes/validation.tsx +0 -40
  64. package/test-app/app/styles/dark.css +0 -7
  65. package/test-app/app/styles/demos/about.css +0 -26
  66. package/test-app/app/styles/demos/remix.css +0 -120
  67. package/test-app/app/styles/global.css +0 -98
  68. package/test-app/cypress/fixtures/example.json +0 -5
  69. package/test-app/cypress/integration/default-values.ts +0 -15
  70. package/test-app/cypress/integration/sanity.ts +0 -19
  71. package/test-app/cypress/integration/submission.ts +0 -26
  72. package/test-app/cypress/integration/validation.ts +0 -70
  73. package/test-app/cypress/plugins/config.ts +0 -38
  74. package/test-app/cypress/plugins/index.ts +0 -9
  75. package/test-app/cypress/support/commands/index.ts +0 -13
  76. package/test-app/cypress/support/commands/types.d.ts +0 -11
  77. package/test-app/cypress/support/index.ts +0 -20
  78. package/test-app/cypress/tsconfig.json +0 -11
  79. package/test-app/cypress.json +0 -3
  80. package/test-app/package-lock.json +0 -11675
  81. package/test-app/package.json +0 -40
  82. package/test-app/public/favicon.ico +0 -0
  83. package/test-app/remix.config.js +0 -10
  84. package/test-app/remix.env.d.ts +0 -2
  85. package/test-app/tsconfig.json +0 -18
  86. 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;
@@ -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
@@ -0,0 +1,5 @@
1
+ export * from "./hooks";
2
+ export * from "./server";
3
+ export * from "./ValidatedForm";
4
+ export * from "./validation/types";
5
+ export * from "./validation/withYup";
@@ -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,7 @@
1
+ import { createContext } from "react";
2
+ export const FormContext = createContext({
3
+ fieldErrors: {},
4
+ clearError: () => { },
5
+ validateField: () => { },
6
+ isSubmitting: false,
7
+ });
@@ -0,0 +1,3 @@
1
+ import type React from "react";
2
+ export declare const omit: (obj: any, ...keys: string[]) => any;
3
+ export declare const mergeRefs: <T = any>(refs: (React.MutableRefObject<T> | React.LegacyRef<T> | undefined)[]) => (instance: T | null) => void;
@@ -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,2 @@
1
+ import { FieldErrors } from "./validation/types";
2
+ export declare const validationError: (errors: FieldErrors) => Response;
@@ -0,0 +1,2 @@
1
+ import { json } from "@remix-run/server-runtime";
2
+ export const validationError = (errors) => json({ fieldErrors: errors }, { status: 422 });
@@ -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,3 @@
1
+ import type { AnyObjectSchema, InferType } from "yup";
2
+ import { Validator } from "./types";
3
+ export declare const withYup: <Schema extends AnyObjectSchema>(validationSchema: Schema) => Validator<InferType<Schema>>;
@@ -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;
@@ -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;
@@ -0,0 +1,5 @@
1
+ export * from "./hooks";
2
+ export * from "./server";
3
+ export * from "./ValidatedForm";
4
+ export * from "./validation/types";
5
+ export * from "./validation/withYup";
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>;