I am using react to create a form with multiple dynamic inputs.
This is my form.tsx
:
interface Property {
name: string;
values: string;
}
function CreateForm({ categories }: { categories: CategoryField[] }) {
const [properties, setProperties] = useState<Property[]>([]);
const initialState = {
message: null,
errors: {},
};
function addProperty() {
setProperties((prev) => {
return [...prev, { name: '', values: '' }];
});
}
function handlePropertyNameChange(
index: number,
property: Property,
newName: string
) {
setProperties((prev) => {
const properties = [...prev];
properties[index].name = newName;
return properties;
});
}
function handlePropertyValuesChange(
index: number,
property: Property,
newValues: string
) {
setProperties((prev) => {
const properties = [...prev];
properties[index].values = newValues;
return properties;
});
}
function removeProperty(indexToRemove: number) {
setProperties((prev) => {
return [...prev].filter((p, pIndex) => {
return pIndex !== indexToRemove;
});
});
}
const [state, dispatch] = useFormState(saveCategory, initialState);
console.log('properties: ', properties);
return (
<div>
<form
action={(form) => {
const p = form.get('name');
// const parent = form.get('')
const k = new FormData();
const k2 = new Array().concat(properties);
console.log('k2 before split: ', k2, ' properties: ', properties);
k2.forEach((p: any) => {
p.values = p.values.split(',');
});
console.log('k2 after forEach: ', k2);
k.append('properties', JSON.stringify(k2));
if (p) {
k.append('name', p);
}
dispatch(k);
}}
>
/* more static inputs here */
{/* properties */}
<div className='mb-2'>
<label className='block' htmlFor='properties'>
Properties
</label>
<button
onClick={addProperty}
type='button'
className='btn-default text-sm mb-2'
>
Add new property
</button>
{properties.length > 0 &&
properties.map((property, index) => (
<div key={index} className='flex gap-1 mb-2'>
<input
type='text'
className='mb-0'
onChange={(ev) =>
handlePropertyNameChange(index, property, ev.target.value)
}
value={property.name}
placeholder='property name (example: color)'
/>
<input
type='text'
className='mb-0'
onChange={(ev) =>
handlePropertyValuesChange(
index,
property,
ev.target.value
)
}
value={property.values}
placeholder='values, comma separated'
/>
<button
onClick={() => removeProperty(index)}
type='button'
className='btn-red'
>
Remove
</button>
</div>
))}
</div>
</div>
<Button type='submit'> Add category </Button>
</form>
</div>
And this is my action.ts
:
const FormSchema = z.object({
id: z.string(),
name: z.string(),
parentCategory: z
.string({
invalid_type_error: 'Please select a category',
})
.nullable()
.optional(),
properties: z.string(),
});
export type CategoryState = {
errors?: {
name?: string[];
parentCategory?: string[];
properties?: string[];
};
message?: string | null;
};
const CreateCategory = FormSchema.omit({ id: true });
export async function saveCategory(
prevState: CategoryState,
formData: FormData
) {
console.log('FormData:::: ', formData, prevState);
const validatedFields = CreateCategory.safeParse({
parentCategory: formData.get('parentCategory'),
properties: formData.get('properties'),
name: formData.get('name'),
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Create Invoice.',
};
}
// Prepare data for insertion into the database
const { parentCategory, properties, name } = validatedFields.data;
console.log(
'This is the parsed data::::: ',
parentCategory,
properties,
name
);
const parsed = JSON.parse(properties);
console.log('parsed with json: ', JSON.parse(properties));
..........
So the whole idea is to add as many properties
inputs as the user wants, where each property is an array of strings, separated by ,
.
I want to parse my formdata to achieve this format:
{
name: 'whatevername'
properties: {
property1name: ["array" , "of", "string values"],
property2name: ["another property"]
}
}
Right now I'm managing this using a useState hook to count and remember how many properties
inputs are, and then pass them to the dispatch action. However, this doesnt feel right, even though it works for now. It doesn't really feel "elegant"
For Forms in React you can use the react-hook-form
library. It's pretty simple to learn and very useful.
Here is the documentation. It has some advantages over classic controlled forms. I Strongly advice you to use it, It makes it much easier to manage nested form value objects.