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
@@ -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,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;
|
package/build/server.js
ADDED
@@ -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 @@
|
|
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,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
|
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,
|
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
package/src/ValidatedForm.tsx
DELETED
@@ -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
|
-
});
|
package/src/internal/util.ts
DELETED
@@ -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
package/src/validation/types.ts
DELETED
@@ -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
|
-
});
|