remix-validated-form 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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 +5 -1
- 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,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": "0.0.3",
|
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",
|
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
|
-
});
|
package/test-app/README.md
DELETED
@@ -1,53 +0,0 @@
|
|
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
|
-
```
|
@@ -1,24 +0,0 @@
|
|
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
|
-
};
|
@@ -1,18 +0,0 @@
|
|
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
|
-
};
|
@@ -1,21 +0,0 @@
|
|
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
|
-
}
|