-
I'm trying to build a form with many rows each corresponding to an 'object'. In the olden days of PHP I would have done something like |
Beta Was this translation helpful? Give feedback.
Replies: 7 comments 6 replies
-
Calling this easy might be a stretch, but you can try something along these lines: const toFormDataArray = <T extends Record<string, unknown>[]>(
formData: FormData,
entityName: string
) =>
[...formData.entries()] // TypeScript is unhappy with this, not sure why. It should work 🤷♂️
.filter(([key]) => key.startsWith(entityName))
.reduce((acc: T, [key, value]) => {
const [prefix, name] = key.split(".");
const id = Number(prefix.charAt(prefix.lastIndexOf("[") + 1));
acc[id] = {
...acc[id],
[name]: value
};
return acc;
}, []);
type Person = Record<string, unknown>;
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const formDataArray = toFormDataArray<Person[]>(formData, "people");
}; |
Beta Was this translation helpful? Give feedback.
-
Simple casesIn simple cases, you can build a form with multiple inputs with same Then what you probably want to do is transposing the fields array into array of object with fields. Here's a example of a complete route which does that: import {Form, useActionData} from "@remix-run/react";
import {ActionFunction} from "remix";
export const action: ActionFunction = async ({request}) => {
const formData = await request.formData();
// 2. Here comes 2 arrays. Transpose them to be array of objects
const firstNames = formData.getAll("firstname");
const lastNames = formData.getAll("lastname");
const persons = [];
for (let i = 0; i < firstNames.length; i++) {
const firstName = firstNames[i];
const lastName = lastNames[i];
persons.push({
firstName,
lastName,
})
}
return persons;
};
export default function () {
const actionData = useActionData();
return (
<>
<Form method="post">
{/* 1. repeat the same input names multiple times */}
<p><input name="firstname"/><input name="lastname"/></p>
<p><input name="firstname"/><input name="lastname"/></p>
<p><input name="firstname"/><input name="lastname"/></p>
<p><input name="firstname"/><input name="lastname"/></p>
<button>Submit</button>
</Form>
<pre>
{JSON.stringify(actionData || {}, null, 2)}
</pre>
</>
);
} Complex casesIn complex cases where the arrays/objects are nested, you can try the strategy utilize import {Form, useActionData} from "@remix-run/react";
import {ActionFunction} from "remix";
import qs from "qs";
export const action: ActionFunction = async ({request}) => {
const text = await request.text();
return qs.parse(text);
};
export default function () {
const actionData = useActionData();
return (
<>
<Form method="post">
{/* use array-annotation instead of dot-annotation because qs supports that */}
<p><input name="person[0][firstname]"/><input name="person[0][lastname]"/><input name="person[0][kids][0][name]"/><input name="person[0][kids][1][name]"/></p>
<p><input name="person[1][firstname]"/><input name="person[1][lastname]"/></p>
<p><input name="person[2][firstname]"/><input name="person[2][lastname]"/><input name="person[2][kids][0][name]"/></p>
<p><input name="person[3][firstname]"/><input name="person[3][lastname]"/></p>
<button>Submit</button>
</Form>
<pre>
{JSON.stringify(actionData || {}, null, 2)}
</pre>
</>
);
} which prints out something like this: {
"person": [
{
"firstname": "Mona",
"lastname": "Simpson",
"kids": [
{
"name": "Herb"
},
{
"name": "Homer"
}
]
},
{"...abbreviated...": "...below..."}
]
} DemoHere's the working demo: https://stackblitz.com/edit/node-ktypys?file=app%2Froutes%2Findex.tsx |
Beta Was this translation helpful? Give feedback.
-
Extending the previous answer of mine (#1541 (comment)), I made a helper library
|
Beta Was this translation helpful? Give feedback.
-
Instead of putting square brackets and index numbers in field names, another option is to simply name nested fields using the format const formDataToPlainObject = (formData) => {
let obj = {};
for (const key of formData.keys()) {
if (key.includes(".")) {
const [prefix, suffix] = key.split(".");
obj[prefix] ??= [];
for (const [index, element] of formData.getAll(key).entries()) {
obj[prefix][index] ??= {};
obj[prefix][index][suffix] = element;
}
} else {
obj[key] = formData.get(key);
}
}
return obj;
}; |
Beta Was this translation helpful? Give feedback.
-
How do you replicate this using the useSubmit() hook if my frontend already has an existing array? |
Beta Was this translation helpful? Give feedback.
-
Hello everybody, This week I got a case similar to yours, but I used the useFieldArray pattern as in the example below that is also in the documentation: function FieldArray() {
const { control, register } = useForm();
const { fields, append, prepend, remove, swap, move, insert } = useFieldArray({
control, // control props comes from useForm (optional: if you are using FormContext)
name: "test", // unique name for your Field Array
});
return (
{fields.map((field, index) => (
<input
key={field.id} // important to include key with field's id
{...register(`test.${index}.value`)}
/>
))}
);
} In the example above it returned an object like this: const object = {
test.0.value: valueInput
} And as in my problem I had an array with several objects, the action grouped everything in the formData returning me a large object. To solve the problem, with the help of my coworker @owilliamgoncalves, I intercepted the data and transformed it with JSON.stringify and sent the data through the @remix-run/react fetcher. const onSubmit = (data) => {
fetcher.submit({ documents: JSON.stringify(data) }, { method: "post" });
}; And just call it in the form as follows, and in the submit I used handleSubmit from useForm of react-hook-form <Form method="post" onSubmit={handleSubmit(onSubmit)}>
{/* code form here */}
</Form> Then just get the data through the formData, do the JSON.parse, and you will have the entire object in the same way you sent it through the form Hope my case can help someone |
Beta Was this translation helpful? Give feedback.
-
A really clean way, IMO, is with Arraysconst inputs = (
<>
<input name="todos[0]" />
<input name="todos[1]" />
</>
);
const result = {
todos: ["Take out the trash", "Buy some milk"];
}; Objectsconst inputs = (
<>
<input name="todo.title" />
<input name="todo.description" />
</>
);
const result = {
todo: {
title: "Take out the trash",
description: "I should really do this",
},
}; The way it works is that you define a "validator" that gets passed to their const validator = withZod(
z.object({
todos: z.array(z.string().min(1))
})
)
export default function Route() {
return (
<ValidatedForm
validator={validator}
method="post"
>
<input name="todos[0]" />
<input name="todos[1]" />
<button type="submit">Submit</button>
</ValidatedForm>
)
} And then you can use that same validator in your action: export async function action({request}: ActionArgs) {
const validation = await validator.validate(await request.formData());
if (validation.error) {
return validationError(validation.error);
}
const { todos } = validation.data; // todos is an array of strings
return null;
}
``` |
Beta Was this translation helpful? Give feedback.
Simple cases
In simple cases, you can build a form with multiple inputs with same
name
attributes, and then browser/Remix will serialize and allow us to read it as array fromformData.getAll("fieldName")
. (They are the web standards, #useThePlatform!)Then what you probably want to do is transposing the fields array into array of object with fields. Here's a example of a complete route which does that: