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
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FormContext = void 0;
4
+ const react_1 = require("react");
5
+ exports.FormContext = (0, react_1.createContext)({
6
+ fieldErrors: {},
7
+ clearError: () => { },
8
+ validateField: () => { },
9
+ isSubmitting: false,
10
+ });
@@ -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,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeRefs = exports.omit = void 0;
4
+ const omit = (obj, ...keys) => {
5
+ const result = { ...obj };
6
+ for (const key of keys) {
7
+ delete result[key];
8
+ }
9
+ return result;
10
+ };
11
+ exports.omit = omit;
12
+ const mergeRefs = (refs) => {
13
+ return (value) => {
14
+ refs.filter(Boolean).forEach((ref) => {
15
+ if (typeof ref === "function") {
16
+ ref(value);
17
+ }
18
+ else if (ref != null) {
19
+ ref.current = value;
20
+ }
21
+ });
22
+ };
23
+ };
24
+ exports.mergeRefs = mergeRefs;
@@ -0,0 +1,2 @@
1
+ import { FieldErrors } from "./validation/types";
2
+ export declare const validationError: (errors: FieldErrors) => Response;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validationError = void 0;
4
+ const server_runtime_1 = require("@remix-run/server-runtime");
5
+ const validationError = (errors) => (0, server_runtime_1.json)({ fieldErrors: errors }, { status: 422 });
6
+ exports.validationError = validationError;
@@ -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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
11
+ }) : function(o, v) {
12
+ o["default"] = v;
13
+ });
14
+ var __importStar = (this && this.__importStar) || function (mod) {
15
+ if (mod && mod.__esModule) return mod;
16
+ var result = {};
17
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
+ __setModuleDefault(result, mod);
19
+ return result;
20
+ };
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ const yup = __importStar(require("yup"));
23
+ const __1 = require("..");
24
+ const validationTestCases = [
25
+ {
26
+ name: "yup",
27
+ validator: (0, __1.withYup)(yup.object({
28
+ firstName: yup.string().required(),
29
+ lastName: yup.string().required(),
30
+ age: yup.number(),
31
+ })),
32
+ },
33
+ ];
34
+ // Not going to enforce exact error strings here
35
+ const anyString = expect.any(String);
36
+ describe("Validation", () => {
37
+ describe.each(validationTestCases)("Adapter for $name", ({ validator }) => {
38
+ describe("validate", () => {
39
+ it("should return the data when valid", () => {
40
+ const obj = {
41
+ firstName: "John",
42
+ lastName: "Doe",
43
+ age: 30,
44
+ };
45
+ expect(validator.validate(obj)).toEqual({
46
+ data: obj,
47
+ error: undefined,
48
+ });
49
+ });
50
+ it("should return field errors when invalid", () => {
51
+ const obj = { age: "hi!" };
52
+ expect(validator.validate(obj)).toEqual({
53
+ data: undefined,
54
+ error: {
55
+ firstName: anyString,
56
+ lastName: anyString,
57
+ age: anyString,
58
+ },
59
+ });
60
+ });
61
+ });
62
+ describe("validateField", () => {
63
+ it("should not return an error if field is valid", () => {
64
+ const obj = { firstName: "John", lastName: 123 };
65
+ expect(validator.validateField(obj, "firstName")).toEqual({
66
+ error: undefined,
67
+ });
68
+ });
69
+ it("should return an error if field is invalid", () => {
70
+ const obj = { firstName: "John", lastName: {} };
71
+ expect(validator.validateField(obj, "lastName")).toEqual({
72
+ error: anyString,
73
+ });
74
+ });
75
+ });
76
+ });
77
+ });
@@ -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,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withYup = void 0;
4
+ const validationErrorToFieldErrors = (error) => {
5
+ const fieldErrors = {};
6
+ error.inner.forEach((innerError) => {
7
+ if (!innerError.path)
8
+ return;
9
+ fieldErrors[innerError.path] = innerError.message;
10
+ });
11
+ return fieldErrors;
12
+ };
13
+ const withYup = (validationSchema) => ({
14
+ validate: (data) => {
15
+ try {
16
+ const validated = validationSchema.validateSync(data, {
17
+ abortEarly: false,
18
+ });
19
+ return { data: validated, error: undefined };
20
+ }
21
+ catch (err) {
22
+ return {
23
+ error: validationErrorToFieldErrors(err),
24
+ data: undefined,
25
+ };
26
+ }
27
+ },
28
+ validateField: (data, field) => {
29
+ try {
30
+ validationSchema.validateSyncAt(field, data);
31
+ return {};
32
+ }
33
+ catch (err) {
34
+ return { error: err.message };
35
+ }
36
+ },
37
+ });
38
+ exports.withYup = withYup;
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "remix-validated-form",
3
- "version": "0.0.1",
3
+ "version": "1.0.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",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/airjp73/remix-validated-form"
10
+ },
7
11
  "sideEffects": false,
8
12
  "scripts": {
9
13
  "build": "npm run build:browser && npm run build:main && npm run build:tests",
@@ -13,7 +17,8 @@
13
17
  "test": "jest src",
14
18
  "lint": "eslint .",
15
19
  "prettier": "prettier . --write",
16
- "prepare": "husky install"
20
+ "prepare": "husky install",
21
+ "prepublishOnly": "npm run build:browser && npm run build:main"
17
22
  },
18
23
  "author": {
19
24
  "name": "Aaron Pettengill",
@@ -45,6 +50,7 @@
45
50
  "eslint": "^7.32.0",
46
51
  "eslint-config-react-app": "^6.0.0",
47
52
  "eslint-plugin-cypress": "^2.12.1",
53
+ "eslint-plugin-flowtype": "^5.10.0",
48
54
  "eslint-plugin-import": "^2.25.3",
49
55
  "eslint-plugin-jsx-a11y": "^6.5.1",
50
56
  "eslint-plugin-prettier": "^4.0.0",
@@ -54,18 +60,17 @@
54
60
  "husky": "^7.0.4",
55
61
  "jest": "^27.3.1",
56
62
  "lint-staged": "^12.1.2",
63
+ "prettier": "^2.5.0",
57
64
  "react": "^17.0.2",
58
65
  "ts-jest": "^27.0.7",
59
66
  "typescript": "^4.5.2",
60
67
  "yup": "^0.32.11"
61
68
  },
62
69
  "dependencies": {
63
- "eslint-plugin-flowtype": "^5.10.0",
64
- "prettier": "^2.5.0",
65
70
  "tiny-invariant": "^1.2.0"
66
71
  },
67
72
  "lint-staged": {
68
- "**/*.{ts,tsx,json,js,jsx}": [
73
+ "**/*.{ts,tsx,js,jsx}": [
69
74
  "prettier --write",
70
75
  "eslint --cache --fix"
71
76
  ]
package/.eslintrc.js DELETED
@@ -1,46 +0,0 @@
1
- /**
2
- * @type {import('eslint').Linter.Config}
3
- */
4
- module.exports = {
5
- ignorePatterns: [
6
- "node_modules/",
7
- ".cache/",
8
- "browser/",
9
- "build/",
10
- "test-app/build",
11
- "test-app/remix-validated-form",
12
- ],
13
- extends: ["react-app"],
14
- plugins: ["prettier"],
15
- rules: {
16
- "import/no-anonymous-default-export": "off",
17
- "import/order": [
18
- "warn",
19
- {
20
- groups: [
21
- "builtin",
22
- "external",
23
- "internal",
24
- "unknown",
25
- "parent",
26
- "sibling",
27
- "index",
28
- ],
29
- alphabetize: {
30
- order: "asc",
31
- caseInsensitive: true,
32
- },
33
- "newlines-between": "never",
34
- },
35
- ],
36
- "prettier/prettier": "error",
37
- },
38
- overrides: [
39
- {
40
- files: ["./cypress/**"],
41
- rules: {
42
- "@typescript-eslint/no-unused-expressions": "off",
43
- },
44
- },
45
- ],
46
- };
@@ -1,35 +0,0 @@
1
- name: Test and build
2
-
3
- on:
4
- push:
5
- branches:
6
- - "main"
7
- pull_request:
8
- branches:
9
- - "main"
10
-
11
- jobs:
12
- build:
13
- runs-on: ubuntu-latest
14
-
15
- steps:
16
- - uses: actions/checkout@v2
17
-
18
- - name: Install Deps
19
- run: npm install
20
-
21
- - name: Lint
22
- run: npm run lint
23
-
24
- - name: Jest Tests
25
- run: npm run test
26
-
27
- - name: Build
28
- run: npm run build
29
-
30
- - name: Cypress Tests
31
- uses: cypress-io/github-action@v2
32
- with:
33
- working-directory: ./test-app
34
- start: npm run dev
35
- wait-on: http://localhost:3000
package/.husky/pre-commit DELETED
@@ -1,4 +0,0 @@
1
- #!/bin/sh
2
- . "$(dirname "$0")/_/husky.sh"
3
-
4
- npx lint-staged
@@ -1,130 +0,0 @@
1
- import {
2
- Form as RemixForm,
3
- useActionData,
4
- useFetcher,
5
- useFormAction,
6
- useTransition,
7
- } from "@remix-run/react";
8
- import React, {
9
- ComponentProps,
10
- useEffect,
11
- useMemo,
12
- useRef,
13
- useState,
14
- } from "react";
15
- import invariant from "tiny-invariant";
16
- import { FormContext, FormContextValue } from "./internal/formContext";
17
- import { omit, mergeRefs } from "./internal/util";
18
- import { FieldErrors, Validator } from "./validation/types";
19
-
20
- export type FormProps<DataType> = {
21
- validator: Validator<DataType>;
22
- onSubmit?: (data: DataType, event: React.FormEvent<HTMLFormElement>) => void;
23
- fetcher?: ReturnType<typeof useFetcher>;
24
- defaultValues?: Partial<DataType>;
25
- formRef?: React.RefObject<HTMLFormElement>;
26
- } & Omit<ComponentProps<typeof RemixForm>, "onSubmit">;
27
-
28
- function useFieldErrors(
29
- fetcher?: ReturnType<typeof useFetcher>
30
- ): [FieldErrors, React.Dispatch<React.SetStateAction<FieldErrors>>] {
31
- const actionData = useActionData<any>();
32
- const dataToUse = fetcher ? fetcher.data : actionData;
33
- const fieldErrorsFromAction = dataToUse?.fieldErrors;
34
-
35
- const [fieldErrors, setFieldErrors] = useState<FieldErrors>(
36
- fieldErrorsFromAction ?? {}
37
- );
38
- useEffect(() => {
39
- if (fieldErrorsFromAction) setFieldErrors(fieldErrorsFromAction);
40
- }, [fieldErrorsFromAction]);
41
-
42
- return [fieldErrors, setFieldErrors];
43
- }
44
-
45
- const useIsSubmitting = (
46
- action?: string,
47
- fetcher?: ReturnType<typeof useFetcher>
48
- ) => {
49
- const actionForCurrentPage = useFormAction();
50
- const pendingFormSubmit = useTransition().submission;
51
- return fetcher
52
- ? fetcher.state === "submitting"
53
- : pendingFormSubmit &&
54
- pendingFormSubmit.action.endsWith(action ?? actionForCurrentPage);
55
- };
56
-
57
- const getDataFromForm = (el: HTMLFormElement) =>
58
- Object.fromEntries(new FormData(el));
59
-
60
- export function ValidatedForm<DataType>({
61
- validator,
62
- onSubmit,
63
- children,
64
- fetcher,
65
- action,
66
- defaultValues,
67
- formRef: formRefProp,
68
- ...rest
69
- }: FormProps<DataType>) {
70
- const [fieldErrors, setFieldErrors] = useFieldErrors(fetcher);
71
- const isSubmitting = useIsSubmitting(action, fetcher);
72
-
73
- const formRef = useRef<HTMLFormElement>(null);
74
-
75
- const contextValue = useMemo<FormContextValue>(
76
- () => ({
77
- fieldErrors,
78
- action,
79
- defaultValues,
80
- isSubmitting: isSubmitting ?? false,
81
- clearError: (fieldName) => {
82
- setFieldErrors((prev) => omit(prev, fieldName));
83
- },
84
- validateField: (fieldName) => {
85
- invariant(formRef.current, "Cannot find reference to form");
86
- const { error } = validator.validateField(
87
- getDataFromForm(formRef.current),
88
- fieldName as any
89
- );
90
- if (error) {
91
- setFieldErrors((prev) => ({
92
- ...prev,
93
- [fieldName]: error,
94
- }));
95
- }
96
- },
97
- }),
98
- [
99
- fieldErrors,
100
- action,
101
- defaultValues,
102
- isSubmitting,
103
- setFieldErrors,
104
- validator,
105
- ]
106
- );
107
-
108
- const Form = fetcher?.Form ?? RemixForm;
109
-
110
- return (
111
- <Form
112
- ref={mergeRefs([formRef, formRefProp])}
113
- {...rest}
114
- action={action}
115
- onSubmit={(event) => {
116
- const result = validator.validate(getDataFromForm(event.currentTarget));
117
- if (result.error) {
118
- event.preventDefault();
119
- setFieldErrors(result.error);
120
- } else {
121
- onSubmit?.(result.data, event);
122
- }
123
- }}
124
- >
125
- <FormContext.Provider value={contextValue}>
126
- {children}
127
- </FormContext.Provider>
128
- </Form>
129
- );
130
- }
package/src/hooks.ts DELETED
@@ -1,27 +0,0 @@
1
- import { useContext, useMemo } from "react";
2
- import { FormContext } from "./internal/formContext";
3
-
4
- export const useField = (name: string) => {
5
- const { fieldErrors, clearError, validateField, defaultValues } =
6
- useContext(FormContext);
7
-
8
- const field = useMemo(
9
- () => ({
10
- error: fieldErrors[name],
11
- clearError: () => {
12
- clearError(name);
13
- },
14
- validate: () => validateField(name),
15
- defaultValue: defaultValues?.[name],
16
- }),
17
- [clearError, defaultValues, fieldErrors, name, validateField]
18
- );
19
-
20
- return field;
21
- };
22
-
23
- // test commit
24
-
25
- export const useFormContext = () => useContext(FormContext);
26
-
27
- export const useIsSubmitting = () => useFormContext().isSubmitting;
@@ -1,18 +0,0 @@
1
- import { createContext } from "react";
2
- import { FieldErrors } from "../validation/types";
3
-
4
- export type FormContextValue = {
5
- fieldErrors: FieldErrors;
6
- clearError: (...names: string[]) => void;
7
- validateField: (fieldName: string) => void;
8
- action?: string;
9
- isSubmitting: boolean;
10
- defaultValues?: { [fieldName: string]: any };
11
- };
12
-
13
- export const FormContext = createContext<FormContextValue>({
14
- fieldErrors: {},
15
- clearError: () => {},
16
- validateField: () => {},
17
- isSubmitting: false,
18
- });
@@ -1,23 +0,0 @@
1
- import type React from "react";
2
-
3
- export const omit = (obj: any, ...keys: string[]) => {
4
- const result = { ...obj };
5
- for (const key of keys) {
6
- delete result[key];
7
- }
8
- return result;
9
- };
10
-
11
- export const mergeRefs = <T = any>(
12
- refs: Array<React.MutableRefObject<T> | React.LegacyRef<T> | undefined>
13
- ): React.RefCallback<T> => {
14
- return (value: T) => {
15
- refs.filter(Boolean).forEach((ref) => {
16
- if (typeof ref === "function") {
17
- ref(value);
18
- } else if (ref != null) {
19
- (ref as React.MutableRefObject<T | null>).current = value;
20
- }
21
- });
22
- };
23
- };
package/src/server.ts DELETED
@@ -1,5 +0,0 @@
1
- import { json } from "@remix-run/server-runtime";
2
- import { FieldErrors } from "./validation/types";
3
-
4
- export const fieldErrors = (errors: FieldErrors) =>
5
- json({ fieldErrors: errors }, { status: 422 });
@@ -1,12 +0,0 @@
1
- export type FieldErrors = Record<string, string>;
2
-
3
- export type ValidationResult<DataType> =
4
- | { data: DataType; error: undefined }
5
- | { error: FieldErrors; data: undefined };
6
-
7
- export type ValidateFieldResult = { error?: string };
8
-
9
- export type Validator<DataType> = {
10
- validate: (formData: unknown) => ValidationResult<DataType>;
11
- validateField: (formData: unknown, field: string) => ValidateFieldResult;
12
- };
@@ -1,76 +0,0 @@
1
- import * as yup from "yup";
2
- import { Validator, withYup } from "..";
3
-
4
- // If adding an adapter, write a validator that validates this shape
5
- type Shape = {
6
- firstName: string;
7
- lastName: string;
8
- age?: number;
9
- };
10
-
11
- type ValidationTestCase = {
12
- name: string;
13
- validator: Validator<Shape>;
14
- };
15
-
16
- const validationTestCases: ValidationTestCase[] = [
17
- {
18
- name: "yup",
19
- validator: withYup(
20
- yup.object({
21
- firstName: yup.string().required(),
22
- lastName: yup.string().required(),
23
- age: yup.number(),
24
- })
25
- ),
26
- },
27
- ];
28
-
29
- // Not going to enforce exact error strings here
30
- const anyString = expect.any(String);
31
-
32
- describe("Validation", () => {
33
- describe.each(validationTestCases)("Adapter for $name", ({ validator }) => {
34
- describe("validate", () => {
35
- it("should return the data when valid", () => {
36
- const obj: Shape = {
37
- firstName: "John",
38
- lastName: "Doe",
39
- age: 30,
40
- };
41
- expect(validator.validate(obj)).toEqual({
42
- data: obj,
43
- error: undefined,
44
- });
45
- });
46
-
47
- it("should return field errors when invalid", () => {
48
- const obj = { age: "hi!" };
49
- expect(validator.validate(obj)).toEqual({
50
- data: undefined,
51
- error: {
52
- firstName: anyString,
53
- lastName: anyString,
54
- age: anyString,
55
- },
56
- });
57
- });
58
- });
59
-
60
- describe("validateField", () => {
61
- it("should not return an error if field is valid", () => {
62
- const obj = { firstName: "John", lastName: 123 };
63
- expect(validator.validateField(obj, "firstName")).toEqual({
64
- error: undefined,
65
- });
66
- });
67
-
68
- it("should return an error if field is invalid", () => {
69
- const obj = { firstName: "John", lastName: {} };
70
- expect(validator.validateField(obj, "lastName")).toEqual({
71
- error: anyString,
72
- });
73
- });
74
- });
75
- });
76
- });
@@ -1,37 +0,0 @@
1
- import type { AnyObjectSchema, InferType, ValidationError } from "yup";
2
- import { FieldErrors, ValidationResult, Validator } from "./types";
3
-
4
- const validationErrorToFieldErrors = (error: ValidationError): FieldErrors => {
5
- const fieldErrors: FieldErrors = {};
6
- error.inner.forEach((innerError) => {
7
- if (!innerError.path) return;
8
- fieldErrors[innerError.path] = innerError.message;
9
- });
10
- return fieldErrors;
11
- };
12
-
13
- export const withYup = <Schema extends AnyObjectSchema>(
14
- validationSchema: Schema
15
- ): Validator<InferType<Schema>> => ({
16
- validate: (data): ValidationResult<InferType<Schema>> => {
17
- try {
18
- const validated = validationSchema.validateSync(data, {
19
- abortEarly: false,
20
- });
21
- return { data: validated, error: undefined };
22
- } catch (err) {
23
- return {
24
- error: validationErrorToFieldErrors(err as ValidationError),
25
- data: undefined,
26
- };
27
- }
28
- },
29
- validateField: (data, field) => {
30
- try {
31
- validationSchema.validateSyncAt(field, data);
32
- return {};
33
- } catch (err) {
34
- return { error: (err as ValidationError).message };
35
- }
36
- },
37
- });