Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions src/utilities/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ describe("parseFormData", () => {
});
});

it("should handle multiple file input", async () => {
const request = new Request("http://localhost:3000");
const requestFormDataSpy = vi.spyOn(request, "formData");
const blob = new Blob(["Hello, world!"], { type: "text/plain" });
const mockFormData = new FormData();
mockFormData.append("files", blob);
mockFormData.append("files", blob);
requestFormDataSpy.mockResolvedValueOnce(mockFormData);
const data = await parseFormData<{ files: Blob[] }>(request);
expect(data.files).toBeTypeOf("object");
expect(Array.isArray(data.files)).toBe(true);
expect(data.files).toHaveLength(2);
expect(data.files[0]).toBeInstanceOf(File);
expect(data.files[1]).toBeInstanceOf(File);
});

it("should not throw an error when a file is passed in", async () => {
const request = new Request("http://localhost:3000");
const requestFormDataSpy = vi.spyOn(request, "formData");
Expand Down
21 changes: 18 additions & 3 deletions src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,17 @@ export const generateFormData = (
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const outputObject: Record<any, any> = {};

// See if a key is repeated, and then handle that in a special case
const keyCounts: Record<string, number> = {};
for (const key of formData.keys()) {
keyCounts[key] = (keyCounts[key] ?? 0) + 1;
}

// Iterate through each key-value pair in the form data.
for (const [key, value] of formData.entries()) {
// Get the current key's count
const keyCount = keyCounts[key];

// Try to convert data to the original type, otherwise return the original value
const data = preserveStringified ? value : tryParseJSON(value);
// Split the key into an array of parts.
Expand Down Expand Up @@ -60,16 +69,22 @@ export const generateFormData = (

currentObject[key].push(data);
}

// Handles array.foo.0 cases
if (!lastKeyPartIsArray) {
else {
// If the last key part is a valid integer index, push the value to the current array.
if (/^\d+$/.test(lastKeyPart)) {
currentObject.push(data);
}
// Otherwise, set a property on the current object with the last key part and the corresponding value.
else {
currentObject[lastKeyPart] = data;
if (keyCount > 1) {
if (!currentObject[key]) {
currentObject[key] = [];
}
currentObject[key].push(data);
} else {
currentObject[lastKeyPart] = data;
}
}
}
}
Expand Down
70 changes: 70 additions & 0 deletions test-apps/react-router/app/routes/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ import {
import { getFormData, getValidatedFormData } from "remix-hook-form/middleware";
import { z } from "zod";

const fileListSchema = z.any().refine((files) => {
return (
typeof files === "object" &&
Symbol.iterator in files &&
!Array.isArray(files) &&
Array.from(files).every((file: any) => file instanceof File)
);
});

const FormDataZodSchema = z.object({
email: z.string().trim().nonempty("validation.required"),
password: z.string().trim().nonempty("validation.required"),
Expand All @@ -21,6 +30,9 @@ const FormDataZodSchema = z.object({
boolean: z.boolean().optional(),
date: z.date().or(z.string()),
null: z.null(),
files: fileListSchema.optional(),
options: z.array(z.string()).optional(),
checkboxes: z.array(z.string()).optional(),
});

const resolver = zodResolver(FormDataZodSchema);
Expand All @@ -38,6 +50,15 @@ export const action = async ({ context }: ActionFunctionArgs) => {
if (errors) {
return { errors, receivedValues };
}

console.log(
"File names:",
// since files is of type "any", we need to assert its type here
data.files?.map((file: File) => file.name).join(", "),
);

console.log("Selected options:", data.options);

return { result: "success" };
};

Expand All @@ -54,6 +75,9 @@ export default function Index() {
date: new Date(),
boolean: true,
null: null,
files: undefined,
options: undefined,
checkboxes: undefined,
},

submitData: { test: "test" },
Expand All @@ -73,6 +97,52 @@ export default function Index() {
<label>
number
<input type="number" {...register("number")} />
{formState.errors.number?.message}
</label>
<label>
Multiple Files
<input type="file" {...register("files")} multiple />
{formState.errors.files?.message}
</label>
<label>
Selected Options
<select {...register("options")} multiple>
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
<option value="option3">Option 3</option>
</select>
{formState.errors.options?.message}
</label>
<label>
Checkboxes
<fieldset>
<legend>Select your preferences:</legend>
<label>
<input
type="checkbox"
value="preference1"
{...register("checkboxes")}
/>
Preference 1
</label>
<label>
<input
type="checkbox"
value="preference2"
{...register("checkboxes")}
/>
Preference 2
</label>
<label>
<input
type="checkbox"
value="preference3"
{...register("checkboxes")}
/>{" "}
Preference 3
</label>
</fieldset>
{formState.errors.checkboxes?.message}
</label>

<div>
Expand Down
Loading