Skip to content

Commit 30e0787

Browse files
authored
Merge pull request #112 from forge42dev/undefined-handling
fix for undefined handling
2 parents 6d1296b + 61951f0 commit 30e0787

File tree

6 files changed

+105
-55
lines changed

6 files changed

+105
-55
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,4 @@ dist
104104
.tern-port
105105

106106
/build
107+
.history

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "remix-hook-form",
3-
"version": "5.0.4",
3+
"version": "5.1.0",
44
"description": "Utility wrapper around react-hook-form for use with Remix.run",
55
"type": "module",
66
"main": "./dist/index.cjs",

src/testing-app/app/root.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ function App() {
3333
</html>
3434
);
3535
}
36-
export default withDevTools(App);
36+
export default App;

src/testing-app/app/routes/_index.tsx

+17-38
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
json,
44
type ActionFunctionArgs,
55
unstable_parseMultipartFormData,
6-
LoaderFunctionArgs,
6+
type LoaderFunctionArgs,
77
type UploadHandler,
88
} from "@remix-run/node";
99
import { Form, useFetcher } from "@remix-run/react";
@@ -17,22 +17,7 @@ import {
1717
RemixFormProvider,
1818
} from "remix-hook-form";
1919
import { z } from "zod";
20-
import {
21-
generateObjectSchema,
22-
stringOptional,
23-
stringRequired,
24-
dateOfBirthRequired,
25-
emailOptional,
26-
booleanOptional,
27-
booleanRequired,
28-
} from "~/zod";
29-
const MAX_FILE_SIZE = 500000;
30-
const ACCEPTED_IMAGE_TYPES = [
31-
"image/jpeg",
32-
"image/jpg",
33-
"image/png",
34-
"image/webp",
35-
];
20+
3621
export const fileUploadHandler =
3722
(): UploadHandler =>
3823
async ({ data, filename }) => {
@@ -51,30 +36,26 @@ export const fileUploadHandler =
5136
return new File([buffer], filename, { type: "image/jpeg" });
5237
};
5338

54-
export const patientBaseSchema = generateObjectSchema({
55-
file: z.any().optional(),
56-
});
5739
const FormDataZodSchema = z.object({
5840
email: z.string().trim().nonempty("validation.required"),
5941
password: z.string().trim().nonempty("validation.required"),
6042
number: z.number({ coerce: true }).int().positive(),
6143
redirectTo: z.string().optional(),
44+
boolean: z.boolean().optional(),
45+
date: z.date().or(z.string()),
46+
null: z.null(),
6247
});
6348

64-
type FormData = z.infer<typeof patientBaseSchema>;
49+
type SchemaFormData = z.infer<typeof FormDataZodSchema>;
6550

66-
const resolver = zodResolver(patientBaseSchema);
51+
const resolver = zodResolver(FormDataZodSchema);
6752
export const loader = ({ request }: LoaderFunctionArgs) => {
6853
const data = getFormDataFromSearchParams(request);
6954
return json({ result: "success" });
7055
};
7156
export const action = async ({ request }: ActionFunctionArgs) => {
72-
const formData = await unstable_parseMultipartFormData(
73-
request,
74-
fileUploadHandler(),
75-
);
76-
console.log(formData.get("file"));
77-
const { errors, data } = await getValidatedFormData(formData, resolver);
57+
const { errors, data } = await getValidatedFormData(request, resolver);
58+
console.log(data, errors);
7859
if (errors) {
7960
return json(errors, {
8061
status: 422,
@@ -89,28 +70,26 @@ export default function Index() {
8970
resolver,
9071
fetcher,
9172
defaultValues: {
92-
file: undefined,
73+
redirectTo: undefined,
74+
number: 4,
75+
76+
password: "test",
77+
date: new Date(),
78+
boolean: true,
79+
null: null,
9380
},
9481
submitData: {
9582
test: "test",
9683
},
9784
});
9885
const { register, handleSubmit, formState, watch, setError } = methods;
99-
useEffect(() => {
100-
setError("root.file", {
101-
message: "File is required",
102-
});
103-
}, []);
86+
10487
console.log(formState.errors);
10588
return (
10689
<RemixFormProvider {...methods}>
10790
<p>Add a thing...</p>
10891

10992
<Form method="post" encType="multipart/form-data" onSubmit={handleSubmit}>
110-
<input type="file" {...register("file")} />
111-
{formState.errors.file && (
112-
<p className="error">{formState.errors.file.message}</p>
113-
)}
11493
<div>
11594
<button type="submit" className="button">
11695
Add

src/utilities/index.test.ts

+58
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,30 @@ describe("createFormData", () => {
3232
expect(formData.get("bool")).toEqual(data.bool.toString());
3333
});
3434

35+
it("should create a FormData object with the provided data and remove undefined values", () => {
36+
const data = {
37+
name: "John Doe",
38+
age: 30,
39+
bool: true,
40+
b: undefined,
41+
a: null,
42+
object: {
43+
test: "1",
44+
number: 2,
45+
},
46+
array: [1, 2, 3],
47+
};
48+
const formData = createFormData(data);
49+
50+
expect(formData.get("name")).toEqual(JSON.stringify(data.name));
51+
expect(formData.get("age")).toEqual(data.age.toString());
52+
expect(formData.get("object")).toEqual(JSON.stringify(data.object));
53+
expect(formData.get("array")).toEqual(JSON.stringify(data.array));
54+
expect(formData.get("bool")).toEqual(data.bool.toString());
55+
expect(formData.get("b")).toEqual(null);
56+
expect(formData.get("a")).toEqual("null");
57+
});
58+
3559
it("should handle null data", () => {
3660
const formData = createFormData(null as any);
3761
expect(formData).toBeTruthy();
@@ -356,6 +380,40 @@ describe("createFormData", () => {
356380
},
357381
});
358382
});
383+
384+
it("doesn't send undefined values to the backend but sends null values", async () => {
385+
const formData = createFormData({
386+
name: "123",
387+
age: 30,
388+
hobbies: ["Reading", "Writing", "Coding"],
389+
boolean: true,
390+
a: null,
391+
b: undefined,
392+
numbers: [1, 2, 3],
393+
other: {
394+
skills: ["testing", "testing"],
395+
something: "else",
396+
},
397+
});
398+
const request = new Request("http://localhost:3000", { method: "POST" });
399+
const requestFormDataSpy = vi.spyOn(request, "formData");
400+
401+
requestFormDataSpy.mockResolvedValueOnce(formData);
402+
const parsed = await parseFormData<typeof formData>(request);
403+
404+
expect(parsed).toStrictEqual({
405+
name: "123",
406+
age: 30,
407+
hobbies: ["Reading", "Writing", "Coding"],
408+
boolean: true,
409+
a: null,
410+
numbers: [1, 2, 3],
411+
other: {
412+
skills: ["testing", "testing"],
413+
something: "else",
414+
},
415+
});
416+
});
359417
});
360418

361419
describe("getValidatedFormData", () => {

src/utilities/index.ts

+27-15
Original file line numberDiff line numberDiff line change
@@ -153,29 +153,41 @@ export const createFormData = <T extends FieldValues>(
153153
if (!data) {
154154
return formData;
155155
}
156-
Object.entries(data).map(([key, value]) => {
156+
for (const [key, value] of Object.entries(data)) {
157+
// Skip undefined values
158+
if (value === undefined) {
159+
continue;
160+
}
161+
// Handle FileList
157162
if (value instanceof FileList) {
158163
for (let i = 0; i < value.length; i++) {
159164
formData.append(key, value[i]);
160165
}
161-
return;
166+
continue;
162167
}
163168
if (value instanceof File || value instanceof Blob) {
164169
formData.append(key, value);
165-
} else {
166-
if (stringifyAll) {
167-
formData.append(key, JSON.stringify(value));
168-
} else {
169-
if (typeof value === "string") {
170-
formData.append(key, value);
171-
} else if (value instanceof Date) {
172-
formData.append(key, value.toISOString());
173-
} else {
174-
formData.append(key, JSON.stringify(value));
175-
}
176-
}
170+
continue;
171+
}
172+
// Stringify all values if set
173+
if (stringifyAll) {
174+
formData.append(key, JSON.stringify(value));
175+
continue;
176+
}
177+
// Handle strings
178+
if (typeof value === "string") {
179+
formData.append(key, value);
180+
continue;
181+
}
182+
// Handle dates
183+
if (value instanceof Date) {
184+
formData.append(key, value.toISOString());
185+
continue;
177186
}
178-
});
187+
// Handle all the other values
188+
189+
formData.append(key, JSON.stringify(value));
190+
}
179191

180192
return formData;
181193
};

0 commit comments

Comments
 (0)