diff --git a/package-lock.json b/package-lock.json index 96418cf..00c6822 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "remix-hook-form", - "version": "7.0.1", + "version": "7.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "remix-hook-form", - "version": "7.0.1", + "version": "7.1.0", "license": "MIT", "workspaces": [ ".", diff --git a/src/utilities/index.test.ts b/src/utilities/index.test.ts index 1f7e205..083122f 100644 --- a/src/utilities/index.test.ts +++ b/src/utilities/index.test.ts @@ -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"); diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 486f71c..b2f830b 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -25,8 +25,17 @@ export const generateFormData = ( // biome-ignore lint/suspicious/noExplicitAny: const outputObject: Record = {}; + // See if a key is repeated, and then handle that in a special case + const keyCounts: Record = {}; + 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. @@ -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; + } } } } diff --git a/test-apps/react-router/app/routes/home.tsx b/test-apps/react-router/app/routes/home.tsx index 68e8bb6..d278a7d 100644 --- a/test-apps/react-router/app/routes/home.tsx +++ b/test-apps/react-router/app/routes/home.tsx @@ -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"), @@ -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); @@ -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" }; }; @@ -54,6 +75,9 @@ export default function Index() { date: new Date(), boolean: true, null: null, + files: undefined, + options: undefined, + checkboxes: undefined, }, submitData: { test: "test" }, @@ -73,6 +97,52 @@ export default function Index() { + + +