remix-validated-form 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- package/.eslintignore +1 -0
- package/.eslintrc.js +46 -0
- package/.github/workflows/test.yml +35 -0
- package/.husky/pre-commit +4 -0
- package/.prettierignore +8 -0
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/jest.config.js +5 -0
- package/package.json +73 -0
- package/src/ValidatedForm.tsx +130 -0
- package/src/hooks.ts +27 -0
- package/src/index.ts +5 -0
- package/src/internal/formContext.ts +18 -0
- package/src/internal/util.ts +23 -0
- package/src/server.ts +5 -0
- package/src/validation/types.ts +12 -0
- package/src/validation/validation.test.ts +76 -0
- package/src/validation/withYup.ts +37 -0
- package/test-app/README.md +53 -0
- package/test-app/app/components/Input.tsx +24 -0
- package/test-app/app/components/SubmitButton.tsx +18 -0
- package/test-app/app/entry.client.tsx +4 -0
- package/test-app/app/entry.server.tsx +21 -0
- package/test-app/app/root.tsx +246 -0
- package/test-app/app/routes/default-values.tsx +34 -0
- package/test-app/app/routes/index.tsx +100 -0
- package/test-app/app/routes/noscript.tsx +10 -0
- package/test-app/app/routes/submission.alt.tsx +6 -0
- package/test-app/app/routes/submission.fetcher.tsx +6 -0
- package/test-app/app/routes/submission.tsx +47 -0
- package/test-app/app/routes/validation.tsx +40 -0
- package/test-app/app/styles/dark.css +7 -0
- package/test-app/app/styles/demos/about.css +26 -0
- package/test-app/app/styles/demos/remix.css +120 -0
- package/test-app/app/styles/global.css +98 -0
- package/test-app/cypress/fixtures/example.json +5 -0
- package/test-app/cypress/integration/default-values.ts +15 -0
- package/test-app/cypress/integration/sanity.ts +19 -0
- package/test-app/cypress/integration/submission.ts +26 -0
- package/test-app/cypress/integration/validation.ts +70 -0
- package/test-app/cypress/plugins/config.ts +38 -0
- package/test-app/cypress/plugins/index.ts +9 -0
- package/test-app/cypress/support/commands/index.ts +13 -0
- package/test-app/cypress/support/commands/types.d.ts +11 -0
- package/test-app/cypress/support/index.ts +20 -0
- package/test-app/cypress/tsconfig.json +11 -0
- package/test-app/cypress.json +3 -0
- package/test-app/package-lock.json +11675 -0
- package/test-app/package.json +40 -0
- package/test-app/public/favicon.ico +0 -0
- package/test-app/remix.config.js +10 -0
- package/test-app/remix.env.d.ts +2 -0
- package/test-app/tsconfig.json +18 -0
- package/tsconfig.json +15 -0
package/.eslintignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
./package-lock.json
|
package/.eslintrc.js
ADDED
@@ -0,0 +1,46 @@
|
|
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
|
+
};
|
@@ -0,0 +1,35 @@
|
|
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/.prettierignore
ADDED
package/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 Aaron Pettengill
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/jest.config.js
ADDED
package/package.json
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
{
|
2
|
+
"name": "remix-validated-form",
|
3
|
+
"version": "0.0.1",
|
4
|
+
"description": "Form component and utils for easy form validation in remix",
|
5
|
+
"browser": "./browser/index.js",
|
6
|
+
"main": "./build/index.js",
|
7
|
+
"sideEffects": false,
|
8
|
+
"scripts": {
|
9
|
+
"build": "npm run build:browser && npm run build:main && npm run build:tests",
|
10
|
+
"build:browser": "tsc --project tsconfig.json --module ESNext --outDir ./browser",
|
11
|
+
"build:main": "tsc --project tsconfig.json --module CommonJS --outDir ./build",
|
12
|
+
"build:tests": "tsc --project tsconfig.json --module CommonJS --outDir ./test-app/remix-validated-form",
|
13
|
+
"test": "jest src",
|
14
|
+
"lint": "eslint .",
|
15
|
+
"prettier": "prettier . --write",
|
16
|
+
"prepare": "husky install"
|
17
|
+
},
|
18
|
+
"author": {
|
19
|
+
"name": "Aaron Pettengill",
|
20
|
+
"email": "pettengill.aaron@gmail.com",
|
21
|
+
"url": "https://github.com/airjp73"
|
22
|
+
},
|
23
|
+
"license": "MIT",
|
24
|
+
"keywords": [
|
25
|
+
"remix",
|
26
|
+
"remix.run",
|
27
|
+
"react",
|
28
|
+
"form",
|
29
|
+
"yup",
|
30
|
+
"validation"
|
31
|
+
],
|
32
|
+
"peerDependencies": {
|
33
|
+
"@remix-run/react": "^1.0.0",
|
34
|
+
"@remix-run/server-runtime": "^1.0.0",
|
35
|
+
"react": "^17.0.2"
|
36
|
+
},
|
37
|
+
"devDependencies": {
|
38
|
+
"@remix-run/react": "^1.0.0",
|
39
|
+
"@remix-run/server-runtime": "^1.0.0",
|
40
|
+
"@types/jest": "^27.0.3",
|
41
|
+
"@types/react": "^17.0.36",
|
42
|
+
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
43
|
+
"@typescript-eslint/parser": "^4.33.0",
|
44
|
+
"babel-eslint": "^10.1.0",
|
45
|
+
"eslint": "^7.32.0",
|
46
|
+
"eslint-config-react-app": "^6.0.0",
|
47
|
+
"eslint-plugin-cypress": "^2.12.1",
|
48
|
+
"eslint-plugin-import": "^2.25.3",
|
49
|
+
"eslint-plugin-jsx-a11y": "^6.5.1",
|
50
|
+
"eslint-plugin-prettier": "^4.0.0",
|
51
|
+
"eslint-plugin-react": "^7.27.1",
|
52
|
+
"eslint-plugin-react-hooks": "^4.3.0",
|
53
|
+
"fetch-blob": "^3.1.3",
|
54
|
+
"husky": "^7.0.4",
|
55
|
+
"jest": "^27.3.1",
|
56
|
+
"lint-staged": "^12.1.2",
|
57
|
+
"react": "^17.0.2",
|
58
|
+
"ts-jest": "^27.0.7",
|
59
|
+
"typescript": "^4.5.2",
|
60
|
+
"yup": "^0.32.11"
|
61
|
+
},
|
62
|
+
"dependencies": {
|
63
|
+
"eslint-plugin-flowtype": "^5.10.0",
|
64
|
+
"prettier": "^2.5.0",
|
65
|
+
"tiny-invariant": "^1.2.0"
|
66
|
+
},
|
67
|
+
"lint-staged": {
|
68
|
+
"**/*.{ts,tsx,json,js,jsx}": [
|
69
|
+
"prettier --write",
|
70
|
+
"eslint --cache --fix"
|
71
|
+
]
|
72
|
+
}
|
73
|
+
}
|
@@ -0,0 +1,130 @@
|
|
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
ADDED
@@ -0,0 +1,27 @@
|
|
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;
|
package/src/index.ts
ADDED
@@ -0,0 +1,18 @@
|
|
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
|
+
});
|
@@ -0,0 +1,23 @@
|
|
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
ADDED
@@ -0,0 +1,12 @@
|
|
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
|
+
};
|
@@ -0,0 +1,76 @@
|
|
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
|
+
});
|
@@ -0,0 +1,37 @@
|
|
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
|
+
});
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# Welcome to Remix!
|
2
|
+
|
3
|
+
- [Remix Docs](https://remix.run/docs)
|
4
|
+
|
5
|
+
## Development
|
6
|
+
|
7
|
+
From your terminal:
|
8
|
+
|
9
|
+
```sh
|
10
|
+
npm run dev
|
11
|
+
```
|
12
|
+
|
13
|
+
This starts your app in development mode, rebuilding assets on file changes.
|
14
|
+
|
15
|
+
## Deployment
|
16
|
+
|
17
|
+
First, build your app for production:
|
18
|
+
|
19
|
+
```sh
|
20
|
+
npm run build
|
21
|
+
```
|
22
|
+
|
23
|
+
Then run the app in production mode:
|
24
|
+
|
25
|
+
```sh
|
26
|
+
npm start
|
27
|
+
```
|
28
|
+
|
29
|
+
Now you'll need to pick a host to deploy it to.
|
30
|
+
|
31
|
+
### DIY
|
32
|
+
|
33
|
+
If you're familiar with deploying node applications, the built-in Remix app server is production-ready.
|
34
|
+
|
35
|
+
Make sure to deploy the output of `remix build`
|
36
|
+
|
37
|
+
- `build/`
|
38
|
+
- `public/build/`
|
39
|
+
|
40
|
+
### Using a Template
|
41
|
+
|
42
|
+
When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server.
|
43
|
+
|
44
|
+
```sh
|
45
|
+
cd ..
|
46
|
+
# create a new project, and pick a pre-configured host
|
47
|
+
npx create-remix@latest
|
48
|
+
cd my-new-remix-app
|
49
|
+
# remove the new project's app (not the old one!)
|
50
|
+
rm -rf app
|
51
|
+
# copy your app over
|
52
|
+
cp -R ../my-old-remix-app/app app
|
53
|
+
```
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import { useField } from "../../remix-validated-form";
|
2
|
+
|
3
|
+
type InputProps = {
|
4
|
+
name: string;
|
5
|
+
label: string;
|
6
|
+
validateOnBlur?: boolean;
|
7
|
+
};
|
8
|
+
|
9
|
+
export const Input = ({ name, label, validateOnBlur }: InputProps) => {
|
10
|
+
const { validate, clearError, defaultValue, error } = useField(name);
|
11
|
+
return (
|
12
|
+
<div>
|
13
|
+
<label htmlFor={name}>{label}</label>
|
14
|
+
<input
|
15
|
+
id={name}
|
16
|
+
name={name}
|
17
|
+
onBlur={validateOnBlur ? validate : undefined}
|
18
|
+
onChange={clearError}
|
19
|
+
defaultValue={defaultValue}
|
20
|
+
/>
|
21
|
+
{error && <span style={{ color: "red" }}>{error}</span>}
|
22
|
+
</div>
|
23
|
+
);
|
24
|
+
};
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { useIsSubmitting } from "../../remix-validated-form";
|
2
|
+
|
3
|
+
type Props = {
|
4
|
+
label?: string;
|
5
|
+
submittingLabel?: string;
|
6
|
+
};
|
7
|
+
|
8
|
+
export const SubmitButton = ({
|
9
|
+
label = "Submit",
|
10
|
+
submittingLabel = "Submitting...",
|
11
|
+
}: Props) => {
|
12
|
+
const isSubmitting = useIsSubmitting();
|
13
|
+
return (
|
14
|
+
<button type="submit" disabled={isSubmitting}>
|
15
|
+
{isSubmitting ? submittingLabel : label}
|
16
|
+
</button>
|
17
|
+
);
|
18
|
+
};
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { renderToString } from "react-dom/server";
|
2
|
+
import { RemixServer } from "remix";
|
3
|
+
import type { EntryContext } from "remix";
|
4
|
+
|
5
|
+
export default function handleRequest(
|
6
|
+
request: Request,
|
7
|
+
responseStatusCode: number,
|
8
|
+
responseHeaders: Headers,
|
9
|
+
remixContext: EntryContext
|
10
|
+
) {
|
11
|
+
let markup = renderToString(
|
12
|
+
<RemixServer context={remixContext} url={request.url} />
|
13
|
+
);
|
14
|
+
|
15
|
+
responseHeaders.set("Content-Type", "text/html");
|
16
|
+
|
17
|
+
return new Response("<!DOCTYPE html>" + markup, {
|
18
|
+
status: responseStatusCode,
|
19
|
+
headers: responseHeaders,
|
20
|
+
});
|
21
|
+
}
|