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.
Files changed (84) hide show
  1. package/browser/ValidatedForm.d.ts +11 -0
  2. package/browser/ValidatedForm.js +69 -0
  3. package/browser/hooks.d.ts +8 -0
  4. package/browser/hooks.js +17 -0
  5. package/{src/index.ts → browser/index.d.ts} +0 -0
  6. package/browser/index.js +5 -0
  7. package/browser/internal/formContext.d.ts +13 -0
  8. package/browser/internal/formContext.js +7 -0
  9. package/browser/internal/util.d.ts +3 -0
  10. package/browser/internal/util.js +19 -0
  11. package/browser/server.d.ts +2 -0
  12. package/browser/server.js +2 -0
  13. package/browser/validation/types.d.ts +15 -0
  14. package/browser/validation/types.js +1 -0
  15. package/browser/validation/validation.test.d.ts +1 -0
  16. package/browser/validation/validation.test.js +56 -0
  17. package/browser/validation/withYup.d.ts +3 -0
  18. package/browser/validation/withYup.js +34 -0
  19. package/build/ValidatedForm.d.ts +11 -0
  20. package/build/ValidatedForm.js +76 -0
  21. package/build/hooks.d.ts +8 -0
  22. package/build/hooks.js +23 -0
  23. package/build/index.d.ts +5 -0
  24. package/build/index.js +17 -0
  25. package/build/internal/formContext.d.ts +13 -0
  26. package/build/internal/formContext.js +10 -0
  27. package/build/internal/util.d.ts +3 -0
  28. package/build/internal/util.js +24 -0
  29. package/build/server.d.ts +2 -0
  30. package/build/server.js +6 -0
  31. package/build/validation/types.d.ts +15 -0
  32. package/build/validation/types.js +2 -0
  33. package/build/validation/validation.test.d.ts +1 -0
  34. package/build/validation/validation.test.js +77 -0
  35. package/build/validation/withYup.d.ts +3 -0
  36. package/build/validation/withYup.js +38 -0
  37. package/package.json +5 -1
  38. package/.eslintrc.js +0 -46
  39. package/.github/workflows/test.yml +0 -35
  40. package/.husky/pre-commit +0 -4
  41. package/src/ValidatedForm.tsx +0 -130
  42. package/src/hooks.ts +0 -27
  43. package/src/internal/formContext.ts +0 -18
  44. package/src/internal/util.ts +0 -23
  45. package/src/server.ts +0 -5
  46. package/src/validation/types.ts +0 -12
  47. package/src/validation/validation.test.ts +0 -76
  48. package/src/validation/withYup.ts +0 -37
  49. package/test-app/README.md +0 -53
  50. package/test-app/app/components/Input.tsx +0 -24
  51. package/test-app/app/components/SubmitButton.tsx +0 -18
  52. package/test-app/app/entry.client.tsx +0 -4
  53. package/test-app/app/entry.server.tsx +0 -21
  54. package/test-app/app/root.tsx +0 -246
  55. package/test-app/app/routes/default-values.tsx +0 -34
  56. package/test-app/app/routes/index.tsx +0 -100
  57. package/test-app/app/routes/noscript.tsx +0 -10
  58. package/test-app/app/routes/submission.alt.tsx +0 -6
  59. package/test-app/app/routes/submission.fetcher.tsx +0 -6
  60. package/test-app/app/routes/submission.tsx +0 -47
  61. package/test-app/app/routes/validation.tsx +0 -40
  62. package/test-app/app/styles/dark.css +0 -7
  63. package/test-app/app/styles/demos/about.css +0 -26
  64. package/test-app/app/styles/demos/remix.css +0 -120
  65. package/test-app/app/styles/global.css +0 -98
  66. package/test-app/cypress/fixtures/example.json +0 -5
  67. package/test-app/cypress/integration/default-values.ts +0 -15
  68. package/test-app/cypress/integration/sanity.ts +0 -19
  69. package/test-app/cypress/integration/submission.ts +0 -26
  70. package/test-app/cypress/integration/validation.ts +0 -70
  71. package/test-app/cypress/plugins/config.ts +0 -38
  72. package/test-app/cypress/plugins/index.ts +0 -9
  73. package/test-app/cypress/support/commands/index.ts +0 -13
  74. package/test-app/cypress/support/commands/types.d.ts +0 -11
  75. package/test-app/cypress/support/index.ts +0 -20
  76. package/test-app/cypress/tsconfig.json +0 -11
  77. package/test-app/cypress.json +0 -3
  78. package/test-app/package-lock.json +0 -11675
  79. package/test-app/package.json +0 -40
  80. package/test-app/public/favicon.ico +0 -0
  81. package/test-app/remix.config.js +0 -10
  82. package/test-app/remix.env.d.ts +0 -2
  83. package/test-app/tsconfig.json +0 -18
  84. 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.2",
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
@@ -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
- });
@@ -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,4 +0,0 @@
1
- import { hydrate } from "react-dom";
2
- import { RemixBrowser } from "remix";
3
-
4
- hydrate(<RemixBrowser />, document);
@@ -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
- }