Skip to content

Commit

Permalink
Merge pull request #41 from Code-Forge-Net/32-formstateissubmitsucces…
Browse files Browse the repository at this point in the history
…sful-does-not-behave-like-react-hook-form

version 3.0.0
  • Loading branch information
AlemTuzlak authored Oct 26, 2023
2 parents 4db1c48 + 113c991 commit 511c0bb
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 113 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"plugins": ["@typescript-eslint", "react", "prettier"],
"rules": {
"@typescript-eslint/no-unnecessary-type-constraint": "off",
"@typescript-eslint/no-explicit-any": "off"
"@typescript-eslint/no-explicit-any": "off",
"no-console": "error"
}
}
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export const action = async ({ request }: ActionArgs) => {

### createFormData

createFormData is a utility function that can be used to create a FormData object from the data returned by the handleSubmit function from `react-hook-form`. It takes two arguments, first one is the `data` from the `handleSubmit` function and the second one is the key that the data will be stored in the FormData object. (default is `formData`). It returns a FormData object.
createFormData is a utility function that can be used to create a FormData object from the data returned by the handleSubmit function from `react-hook-form`. It takes one argument, the `data` from the `handleSubmit` function and it converts everything it can to strings and appends files as well. It returns a FormData object.

```jsx
/** all the same code from above */
Expand All @@ -168,10 +168,8 @@ export default function MyForm() {
...,
submitHandlers: {
onValid: data => {
// This will create a FormData instance ready to be sent to the server, by default all your data is stored inside a key called `formData` but this behavior can be changed by passing a second argument to the function
const formData = createFormData(data);
// Example with a custom key
const formDataCustom = createFormData(data, "yourkeyhere");
// This will create a FormData instance ready to be sent to the server, by default all your data is converted to a string before sent
const formData = createFormData(data);
// Do something with the formData
}
}
Expand All @@ -186,7 +184,7 @@ export default function MyForm() {

### parseFormData

parseFormData is a utility function that can be used to parse the data submitted to the action by the handleSubmit function from `react-hook-form`. It takes two arguments, first one is the `request` submitted from the frontend and the second one is the key that the data will be stored in the FormData object. (default is `formData`). It returns an object that contains unvalidated `data` submitted from the frontend.
parseFormData is a utility function that can be used to parse the data submitted to the action by the handleSubmit function from `react-hook-form`. It takes two arguments, first one is the `request` submitted from the frontend and the second one is `preserveStringified`, the form data you submit will be cast to strings because that is how form data works, when retrieving it you can either keep everything as strings or let the helper try to parse it back to original types (eg number to string), default is `false`. It returns an object that contains unvalidated `data` submitted from the frontend.


```jsx
Expand All @@ -195,8 +193,9 @@ parseFormData is a utility function that can be used to parse the data submitted
export const action = async ({ request }: ActionArgs) => {
// Allows you to get the data from the request object without having to validate it
const formData = await parseFormData(request);
// If you used a custom key (eg. `yourkeyhere` from above you can extract like this)
const formDataCustom = await parseFormData(request, "yourkeyhere");
// formData.age will be a number
const formDataStringified = await parseFormData(request, true);
// formDataStringified.age will be a string
// Do something with the data
};

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "remix-hook-form",
"version": "2.0.3",
"version": "3.0.0",
"description": "Utility wrapper around react-hook-form for use with Remix.run",
"type": "module",
"main": "./dist/index.cjs",
Expand Down Expand Up @@ -32,7 +32,7 @@
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
"remix-dev": "npm run dev -w src/testing-app",
"build:dev": "npm run build",
"build:dev": "tsup src/index.ts --format cjs,esm --dts",
"build:dev:watch": "npm run build -- --watch",
"dev": "npm-run-all -s build:dev -p remix-dev build:dev:watch",
"vite": "npm run build --watch -m development",
Expand Down Expand Up @@ -97,4 +97,4 @@
"vitest": "^0.30.1",
"zod": "^3.21.4"
}
}
}
19 changes: 19 additions & 0 deletions src/hook/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,25 @@ describe("useRemixForm", () => {
});
});

it("should reset isSubmitSuccessful after submission if reset is called", async () => {
const { result } = renderHook(() =>
useRemixForm({
resolver: () => ({ values: {}, errors: {} }),
}),
);

act(() => {
result.current.handleSubmit();
});
await waitFor(() => {
expect(result.current.formState.isSubmitSuccessful).toBe(true);
});
act(() => {
result.current.reset();
});
expect(result.current.formState.isSubmitSuccessful).toBe(false);
});

it("should submit the form data to the server when the form is valid", async () => {
const { result } = renderHook(() =>
useRemixForm({
Expand Down
13 changes: 11 additions & 2 deletions src/hook/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "react-hook-form";
import { useForm, FormProvider } from "react-hook-form";
import type {
DeepPartial,
FieldValues,
Path,
RegisterOptions,
Expand Down Expand Up @@ -42,6 +43,8 @@ export const useRemixForm = <T extends FieldValues>({
fetcher,
...formProps
}: UseRemixFormOptions<T>) => {
const [isSubmittedSuccessfully, setIsSubmittedSuccessfully] =
React.useState(false);
const actionSubmit = useSubmit();
const actionData = useActionData();
const submit = fetcher?.submit ?? actionSubmit;
Expand All @@ -54,7 +57,9 @@ export const useRemixForm = <T extends FieldValues>({

// Submits the data to the server when form is valid
const onSubmit = (data: T) => {
submit(createFormData({ ...data, ...submitData }), {
setIsSubmittedSuccessfully(true);
const formData = createFormData({ ...data, ...submitData });
submit(formData, {
method: "post",
...submitConfig,
});
Expand Down Expand Up @@ -92,6 +97,10 @@ export const useRemixForm = <T extends FieldValues>({
submitHandlers?.onValid ?? onSubmit,
submitHandlers?.onInvalid ?? onInvalid,
),
reset: (values?: T | DeepPartial<T> | undefined) => {
setIsSubmittedSuccessfully(false);
methods.reset(values);
},
register: (
name: Path<T>,
options?: RegisterOptions<T> & {
Expand All @@ -106,7 +115,7 @@ export const useRemixForm = <T extends FieldValues>({
formState: {
dirtyFields,
isDirty,
isSubmitSuccessful,
isSubmitSuccessful: isSubmittedSuccessfully || isSubmitSuccessful,
isSubmitted,
isSubmitting: isSubmittingForm || isSubmitting,
isValid,
Expand Down
99 changes: 72 additions & 27 deletions src/testing-app/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,55 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { json, type ActionFunctionArgs } from "@remix-run/node";
import { useFetcher } from "@remix-run/react";
import { getValidatedFormData, useRemixForm } from "remix-hook-form";
import {
json,
type ActionFunctionArgs,
unstable_parseMultipartFormData,
LoaderFunctionArgs,
} from "@remix-run/node";
import { Form, useFetcher } from "@remix-run/react";
import {
getValidatedFormData,
useRemixForm,
parseFormData,
getFormDataFromSearchParams,
} from "remix-hook-form";
import { z } from "zod";

const MAX_FILE_SIZE = 500000;
const ACCEPTED_IMAGE_TYPES = [
"image/jpeg",
"image/jpg",
"image/png",
"image/webp",
];
const schema = z.object({
content: z.string().nonempty("content is required"),
array: z.array(z.string()).nonempty(),
user: z.object({
name: z.string().nonempty(),
email: z.string().email(),
}),
boolean: z.boolean(),
number: z.number().min(5),

// file: z
// .any()
// .refine((files) => files?.length == 1, "Image is required.")
// .refine(
// (files) => files?.[0]?.size <= MAX_FILE_SIZE,
// `Max file size is 5MB.`,
// )
// .refine(
// (files) => ACCEPTED_IMAGE_TYPES.includes(files?.[0]?.type),
// ".jpg, .jpeg, .png and .webp files are accepted.",
// )
// .nullish(),
});

type FormData = z.infer<typeof schema>;

const resolver = zodResolver(schema);

export const action = async ({ request }: ActionFunctionArgs) => {
const { data } = await getValidatedFormData<FormData>(request, resolver);
const data = await getValidatedFormData<FormData>(request, resolver);
console.log("action", data);
await new Promise((resolve) => setTimeout(resolve, 2000));
return null;
// Make DB call or similar here.
Expand All @@ -24,41 +60,50 @@ export const action = async ({ request }: ActionFunctionArgs) => {
};
};

export const loader = () => {
export const loader = ({ request }: LoaderFunctionArgs) => {
const data = getFormDataFromSearchParams(request);
console.log("loader", data);
return json({ result: "success" });
};

export default function Index() {
const fetcher = useFetcher();
const { register, handleSubmit, formState } = useRemixForm<FormData>({
resolver,
fetcher,
submitConfig: {
method: "GET",
},
});
const { register, handleSubmit, formState, watch, reset } =
useRemixForm<FormData>({
resolver,
defaultValues: {
array: ["a", "b"],
boolean: true,
number: 6,
user: {
name: "John Doe",
email: "[email protected]",
},
},
submitConfig: {
method: "POST",
},
});

return (
<div>
<p>Add a thing...</p>
<p>Current Errors: {JSON.stringify(formState.errors)}</p>
<fetcher.Form method="post" onSubmit={handleSubmit}>
<div>
<label>
Content:{" "}
<input
type="text"
{...register("content", { disableProgressiveEnhancement: true })}
/>
Error: {formState.errors.content?.message}
</label>

<Form method="post" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<input type="number" {...register("number")} />
<input type="boolean" {...register("boolean")} />

<input type="string" {...register("user.name")} />
<input type="string" {...register("user.email")} />
<input type="string" {...register("array.0")} />
<input type="string" {...register("array.1")} />
</div>
<div>
<button type="submit" className="button">
Add
</button>
</div>
</fetcher.Form>
</Form>
</div>
);
}
1 change: 1 addition & 0 deletions src/testing-app/remix.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ module.exports = {
// publicPath: "/build/",
serverModuleFormat: "cjs",
watchPaths: ["../../dist"],
tailwind: true,
};
Loading

0 comments on commit 511c0bb

Please sign in to comment.