Skip to content

Commit d718447

Browse files
authored
Merge pull request #174 from phyziyx/patch-1
Fix: Multiple File Input
2 parents 7f6931d + bab6fc2 commit d718447

File tree

4 files changed

+106
-5
lines changed

4 files changed

+106
-5
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/utilities/index.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,22 @@ describe("parseFormData", () => {
132132
});
133133
});
134134

135+
it("should handle multiple file input", async () => {
136+
const request = new Request("http://localhost:3000");
137+
const requestFormDataSpy = vi.spyOn(request, "formData");
138+
const blob = new Blob(["Hello, world!"], { type: "text/plain" });
139+
const mockFormData = new FormData();
140+
mockFormData.append("files", blob);
141+
mockFormData.append("files", blob);
142+
requestFormDataSpy.mockResolvedValueOnce(mockFormData);
143+
const data = await parseFormData<{ files: Blob[] }>(request);
144+
expect(data.files).toBeTypeOf("object");
145+
expect(Array.isArray(data.files)).toBe(true);
146+
expect(data.files).toHaveLength(2);
147+
expect(data.files[0]).toBeInstanceOf(File);
148+
expect(data.files[1]).toBeInstanceOf(File);
149+
});
150+
135151
it("should not throw an error when a file is passed in", async () => {
136152
const request = new Request("http://localhost:3000");
137153
const requestFormDataSpy = vi.spyOn(request, "formData");

src/utilities/index.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,17 @@ export const generateFormData = (
2525
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
2626
const outputObject: Record<any, any> = {};
2727

28+
// See if a key is repeated, and then handle that in a special case
29+
const keyCounts: Record<string, number> = {};
30+
for (const key of formData.keys()) {
31+
keyCounts[key] = (keyCounts[key] ?? 0) + 1;
32+
}
33+
2834
// Iterate through each key-value pair in the form data.
2935
for (const [key, value] of formData.entries()) {
36+
// Get the current key's count
37+
const keyCount = keyCounts[key];
38+
3039
// Try to convert data to the original type, otherwise return the original value
3140
const data = preserveStringified ? value : tryParseJSON(value);
3241
// Split the key into an array of parts.
@@ -60,16 +69,22 @@ export const generateFormData = (
6069

6170
currentObject[key].push(data);
6271
}
63-
6472
// Handles array.foo.0 cases
65-
if (!lastKeyPartIsArray) {
73+
else {
6674
// If the last key part is a valid integer index, push the value to the current array.
6775
if (/^\d+$/.test(lastKeyPart)) {
6876
currentObject.push(data);
6977
}
7078
// Otherwise, set a property on the current object with the last key part and the corresponding value.
7179
else {
72-
currentObject[lastKeyPart] = data;
80+
if (keyCount > 1) {
81+
if (!currentObject[key]) {
82+
currentObject[key] = [];
83+
}
84+
currentObject[key].push(data);
85+
} else {
86+
currentObject[lastKeyPart] = data;
87+
}
7388
}
7489
}
7590
}

test-apps/react-router/app/routes/home.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ import {
1313
import { getFormData, getValidatedFormData } from "remix-hook-form/middleware";
1414
import { z } from "zod";
1515

16+
const fileListSchema = z.any().refine((files) => {
17+
return (
18+
typeof files === "object" &&
19+
Symbol.iterator in files &&
20+
!Array.isArray(files) &&
21+
Array.from(files).every((file: any) => file instanceof File)
22+
);
23+
});
24+
1625
const FormDataZodSchema = z.object({
1726
email: z.string().trim().nonempty("validation.required"),
1827
password: z.string().trim().nonempty("validation.required"),
@@ -21,6 +30,9 @@ const FormDataZodSchema = z.object({
2130
boolean: z.boolean().optional(),
2231
date: z.date().or(z.string()),
2332
null: z.null(),
33+
files: fileListSchema.optional(),
34+
options: z.array(z.string()).optional(),
35+
checkboxes: z.array(z.string()).optional(),
2436
});
2537

2638
const resolver = zodResolver(FormDataZodSchema);
@@ -38,6 +50,15 @@ export const action = async ({ context }: ActionFunctionArgs) => {
3850
if (errors) {
3951
return { errors, receivedValues };
4052
}
53+
54+
console.log(
55+
"File names:",
56+
// since files is of type "any", we need to assert its type here
57+
data.files?.map((file: File) => file.name).join(", "),
58+
);
59+
60+
console.log("Selected options:", data.options);
61+
4162
return { result: "success" };
4263
};
4364

@@ -54,6 +75,9 @@ export default function Index() {
5475
date: new Date(),
5576
boolean: true,
5677
null: null,
78+
files: undefined,
79+
options: undefined,
80+
checkboxes: undefined,
5781
},
5882

5983
submitData: { test: "test" },
@@ -73,6 +97,52 @@ export default function Index() {
7397
<label>
7498
number
7599
<input type="number" {...register("number")} />
100+
{formState.errors.number?.message}
101+
</label>
102+
<label>
103+
Multiple Files
104+
<input type="file" {...register("files")} multiple />
105+
{formState.errors.files?.message}
106+
</label>
107+
<label>
108+
Selected Options
109+
<select {...register("options")} multiple>
110+
<option value="option1">Option 1</option>
111+
<option value="option2">Option 2</option>
112+
<option value="option3">Option 3</option>
113+
</select>
114+
{formState.errors.options?.message}
115+
</label>
116+
<label>
117+
Checkboxes
118+
<fieldset>
119+
<legend>Select your preferences:</legend>
120+
<label>
121+
<input
122+
type="checkbox"
123+
value="preference1"
124+
{...register("checkboxes")}
125+
/>
126+
Preference 1
127+
</label>
128+
<label>
129+
<input
130+
type="checkbox"
131+
value="preference2"
132+
{...register("checkboxes")}
133+
/>
134+
Preference 2
135+
</label>
136+
<label>
137+
<input
138+
type="checkbox"
139+
value="preference3"
140+
{...register("checkboxes")}
141+
/>{" "}
142+
Preference 3
143+
</label>
144+
</fieldset>
145+
{formState.errors.checkboxes?.message}
76146
</label>
77147

78148
<div>

0 commit comments

Comments
 (0)