Dynamic Form Fields
Problem
Users can add or remove repeating entries — such as multiple phone numbers or project links. The number of fields is not known at form creation time, so fields must be added and removed dynamically.
Solution
Form with dynamically added/removed fields.
typescript
import { createForm } from '@vielzeug/formit';
type TeamMember = {
name: string;
email: string;
role: string;
};
const dynamicForm = createForm({
defaultValues: {
teamName: '',
members: [] as TeamMember[],
},
validators: {
teamName: (v) => (!v ? 'Team name is required' : undefined),
},
});
// Add a team member
function addMember() {
const members = dynamicForm.get<TeamMember[]>('members') ?? [];
dynamicForm.set('members', [...members, { name: '', email: '', role: 'member' }]);
}
// Remove a team member
function removeMember(index: number) {
const members = dynamicForm.get<TeamMember[]>('members') ?? [];
dynamicForm.set(
'members',
members.filter((_, i) => i !== index),
);
}
// Update a team member
function updateMember(index: number, field: keyof TeamMember, value: string) {
const members = dynamicForm.get<TeamMember[]>('members') ?? [];
const updated = [...members];
updated[index] = { ...updated[index], [field]: value };
dynamicForm.set('members', updated);
}
// Submit
async function submitTeam() {
const result = await dynamicForm.submit(async (values) => {
const response = await fetch('/api/teams', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
});
return response.json();
});
if (!result.ok) {
return;
}
}Pitfalls
- Calling
removeField(name)before reading the field value discards its data. Read the current value first if you need to process it before removal. - Generating field names from array indices (
phone_0,phone_1) causes stale validation state when items are reordered. Use stable IDs (e.g., UUIDs) as field keys instead. - After
addField(), the new field'stouchedstate isfalse. Validators still run on submit for all fields, including ones the user has never touched — this is intentional but may show errors on fields the user has not seen yet.