Skip to content

Commit

Permalink
File array support and bug fix
Browse files Browse the repository at this point in the history
  • Loading branch information
AlemTuzlak committed Oct 15, 2024
1 parent be02359 commit 5b13c0f
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 38 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "remix-hook-form",
"version": "5.1.0",
"version": "5.1.1",
"description": "Utility wrapper around react-hook-form for use with Remix.run",
"type": "module",
"main": "./dist/index.cjs",
Expand Down
44 changes: 44 additions & 0 deletions src/hook/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,9 @@ describe("useRemixForm", () => {
let streetFieldProps = result.current.register("address.street");

expect(nameFieldProps.defaultValue).toBe("Default name");
expect(nameFieldProps.defaultChecked).toBe(undefined);
expect(streetFieldProps.defaultValue).toBe("Default street");
expect(streetFieldProps.defaultChecked).toBe(undefined);

useActionDataMock.mockReturnValue({
defaultValues: {
Expand All @@ -298,7 +300,49 @@ describe("useRemixForm", () => {
streetFieldProps = result.current.register("address.street");

expect(nameFieldProps.defaultValue).toBe("Updated name");
expect(nameFieldProps.defaultChecked).toBe(undefined);
expect(streetFieldProps.defaultValue).toBe("Updated street");
expect(streetFieldProps.defaultChecked).toBe(undefined);
});

it("should return defaultChecked from the register function when a boolean", async () => {
const { result, rerender } = renderHook(() =>
useRemixForm({
resolver: () => ({
values: { name: "", address: { street: "" }, boolean: true },
errors: {},
}),
defaultValues: {
name: "Default name",
boolean: true,
address: {
street: "Default street",
},
},
}),
);

let booleanFieldProps = result.current.register("boolean");

expect(booleanFieldProps.defaultChecked).toBe(true);
expect(booleanFieldProps.defaultValue).toBe(undefined);

useActionDataMock.mockReturnValue({
defaultValues: {
name: "Updated name",
address: {
street: "Updated street",
},
boolean: false,
},
errors: { name: "Enter another name" },
});

rerender();

booleanFieldProps = result.current.register("boolean");
expect(booleanFieldProps.defaultChecked).toBe(false);
expect(booleanFieldProps.defaultValue).toBe(undefined);
});
});

Expand Down
59 changes: 31 additions & 28 deletions src/hook/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import {
type FetcherWithComponents,
type FormEncType,
type FormMethod,
type SubmitFunction,
useActionData,
useNavigation,
useSubmit,
} from "@remix-run/react";
import React, {
type FormEvent,
type ReactNode,
Expand All @@ -6,30 +15,21 @@ import React, {
useState,
} from "react";
import {
type FetcherWithComponents,
type SubmitFunction,
useActionData,
useSubmit,
useNavigation,
type FormEncType,
type FormMethod,
} from "@remix-run/react";
import {
type SubmitErrorHandler,
type SubmitHandler,
useFormContext,
useForm,
FormProvider,
get,
type DefaultValues,
type FieldValues,
FormProvider,
type FormState,
type KeepStateOptions,
type Path,
type RegisterOptions,
type SubmitErrorHandler,
type SubmitHandler,
type UseFormHandleSubmit,
type UseFormProps,
type UseFormReturn,
get,
useForm,
useFormContext,
} from "react-hook-form";

import { createFormData } from "../utilities";
Expand Down Expand Up @@ -71,9 +71,7 @@ export const useRemixForm = <T extends FieldValues>({
() =>
Boolean(
(navigation.state !== "idle" && navigation.formData !== undefined) ||
(fetcher &&
fetcher.state !== "idle" &&
fetcher.formData !== undefined),
(fetcher?.state !== "idle" && fetcher?.formData !== undefined),
),
[navigation.state, navigation.formData, fetcher?.state, fetcher?.formData],
);
Expand Down Expand Up @@ -180,16 +178,21 @@ export const useRemixForm = <T extends FieldValues>({
options?: RegisterOptions<T> & {
disableProgressiveEnhancement?: boolean;
},
) => ({
...methods.register(name, options),
...(!options?.disableProgressiveEnhancement && {
defaultValue:
get(data?.defaultValues, name) ??
get(methods.formState.defaultValues, name) ??
"",
}),
}),
[methods.register, data?.defaultValues, methods.formState],
) => {
const defaultValue =
get(data?.defaultValues, name) ??
get(methods.formState.defaultValues, name);
return {
...methods.register(name, options),
...(!options?.disableProgressiveEnhancement && {
defaultValue:
typeof defaultValue === "string" ? defaultValue : undefined,
defaultChecked:
typeof defaultValue === "boolean" ? defaultValue : undefined,
}),
};
},
[methods.register, data?.defaultValues, methods.formState.defaultValues],
);

const handleSubmit = useMemo(
Expand Down
23 changes: 16 additions & 7 deletions src/testing-app/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { zodResolver } from "@hookform/resolvers/zod";
import {
json,
type ActionFunctionArgs,
unstable_parseMultipartFormData,
type LoaderFunctionArgs,
type UploadHandler,
json,
unstable_parseMultipartFormData,
} from "@remix-run/node";
import { Form, useFetcher } from "@remix-run/react";
import { useEffect } from "react";
import {
RemixFormProvider,
getFormDataFromSearchParams,
getValidatedFormData,
useRemixForm,
parseFormData,
getFormDataFromSearchParams,
useRemixForm,
validateFormData,
RemixFormProvider,
} from "remix-hook-form";
import { z } from "zod";

Expand Down Expand Up @@ -84,12 +84,21 @@ export default function Index() {
});
const { register, handleSubmit, formState, watch, setError } = methods;

console.log(formState.errors);
const checkbox = watch("boolean");
console.log(checkbox, typeof checkbox);
return (
<RemixFormProvider {...methods}>
<p>Add a thing...</p>

<Form method="post" encType="multipart/form-data" onSubmit={handleSubmit}>
<label>
Boolean
<input type="checkbox" {...register("boolean")} />
</label>
<label>
number
<input type="number" {...register("number")} />
</label>
<div></div>
<div>
<button type="submit" className="button">
Add
Expand Down
4 changes: 2 additions & 2 deletions src/testing-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"isbot": "^3.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remix-hook-form": "^5.0.0",
"remix-hook-form": "*",
"react-hook-form": "7.48.2"
},
"devDependencies": {
Expand All @@ -31,4 +31,4 @@
"engines": {
"node": ">=14.0.0"
}
}
}
39 changes: 39 additions & 0 deletions src/utilities/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,45 @@ describe("validateFormData", () => {
});

describe("createFormData", () => {
it("accepts a file array and maps it properly", async () => {
const formData = createFormData({
files: [new File(["test"], "test.txt"), new File(["test2"], "test2.txt")],
});
const files = formData.getAll("files");
expect(files).toHaveLength(2);
expect(files[0]).toBeInstanceOf(File);
expect(files[1]).toBeInstanceOf(File);
});

it("accepts a blob array and maps it properly", async () => {
const formData = createFormData({
files: [
new Blob(["test"], { type: "text/plain" }),
new Blob(["test2"], { type: "text/plain" }),
],
});
const files = formData.getAll("files");
expect(files).toHaveLength(2);
expect(files[0]).toBeInstanceOf(Blob);
expect(files[1]).toBeInstanceOf(Blob);
});

it("accepts a file and adds it properly to formData", async () => {
const formData = createFormData({
file: new File(["test"], "test.txt"),
});
const file = formData.get("file");
expect(file).toBeInstanceOf(File);
});

it("accepts a blob and adds it properly to formData", async () => {
const formData = createFormData({
file: new Blob(["test"], { type: "text/plain" }),
});
const file = formData.get("file");
expect(file).toBeInstanceOf(Blob);
});

it("doesn't mess up types when passed from frontend to backend", async () => {
const formData = createFormData({
name: "123",
Expand Down
10 changes: 10 additions & 0 deletions src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ export const createFormData = <T extends FieldValues>(
}
continue;
}
if (
value instanceof Array &&
value.length > 0 &&
(value[0] instanceof File || value[0] instanceof Blob)
) {
for (let i = 0; i < value.length; i++) {
formData.append(key, value[i]);
}
continue;
}
if (value instanceof File || value instanceof Blob) {
formData.append(key, value);
continue;
Expand Down

0 comments on commit 5b13c0f

Please sign in to comment.