Interactive demo of a repeater field — add and remove team members
/**
* Repeater Field Demo - Dynamic list of sub-fields with add/remove
*/
import { Form, FormBuilder } from '@saastro/forms';
import { FormProvider } from '@/components/FormProvider';
import { TooltipProvider } from '@/components/ui/tooltip';
const config = FormBuilder.create('repeater-demo')
.addField('teamName', (f) =>
f
.type('text')
.label('Team Name')
.placeholder('Enter a team name')
.required('Team name is required'),
)
.addField('members', (f) =>
f
.type('repeater')
.label('Team Members')
.helperText('Add up to 4 team members')
.itemFields({
name: {
type: 'text',
label: 'Name',
placeholder: 'Member name',
},
email: {
type: 'email',
label: 'Email',
placeholder: '[email protected]',
},
role: {
type: 'select',
label: 'Role',
placeholder: 'Select a role...',
options: [
{ label: 'Developer', value: 'developer' },
{ label: 'Designer', value: 'designer' },
{ label: 'Product Manager', value: 'pm' },
],
},
})
.minItems(1)
.maxItems(4)
.addLabel('Add member')
.removeLabel('Remove')
.value([{ name: '', email: '', role: '' }])
.optional(),
)
.addStep('main', ['teamName', 'members'])
.build();
export default function RepeaterDemo() {
const handleSubmit = (data: Record<string, unknown>) => {
console.log('Form submitted:', data);
alert('Form submitted! Check console for data.');
};
return (
<TooltipProvider>
<FormProvider>
<Form config={config} onSubmit={handleSubmit} className="space-y-4" />
</FormProvider>
</TooltipProvider>
);
} Overview
The repeater renders a dynamic list of items. Each item is a bordered card containing the sub-fields you declare in itemFields; users append new items with the add button and delete existing ones with the remove button. Internally the field is powered by react-hook-form’s useFieldArray, and its value is an array of objects — one object per item.
Typical uses: contact lists, order line items, multiple addresses, team members, “add another” sections of any kind.
Usage
FormBuilder API
import { FormBuilder } from '@saastro/forms';
const config = FormBuilder.create('order')
.addField('items', (f) =>
f
.type('repeater')
.label('Order Items')
.itemFields({
product: { type: 'text', label: 'Product', placeholder: 'Product name' },
quantity: { type: 'number', label: 'Quantity', placeholder: '1' },
})
.minItems(1)
.maxItems(10)
.addLabel('Add item')
.removeLabel('Remove')
.value([{ product: '', quantity: '' }]),
)
.addStep('main', ['items'])
.build();
JSON Configuration
{
"type": "repeater",
"label": "Order Items",
"itemFields": {
"product": { "type": "text", "label": "Product", "placeholder": "Product name" },
"quantity": { "type": "number", "label": "Quantity", "placeholder": "1" }
},
"minItems": 1,
"maxItems": 10,
"addLabel": "Add item",
"removeLabel": "Remove"
}
Value Shape
A repeater named items submits an array of objects, one per item, keyed by the itemFields names:
// Submitted data:
{
items: [
{ product: 'Keyboard', quantity: '2' },
{ product: 'Mouse', quantity: '1' },
]
}
numbersub-fields hold string values at runtime, like the standalone number input. Convert in your submit handler if you need numbers.
Sub-field naming
Each rendered input is registered with react-hook-form under the path {name}.{index}.{subName} — the standard useFieldArray convention:
items.0.product
items.0.quantity
items.1.product
items.1.quantity
You’ll see these paths when inspecting form state or reading values through react-hook-form APIs.
Initial and default values
- The repeater’s default value is
[]— the list starts empty unless you setvalueto an array of item objects (as in the examples above). - When the user clicks the add button, the new item initializes each sub-field to that sub-field’s own
valuefromitemFields, or''when unset.
Item Limits
minItems(default0) — the remove button is hidden whenever removing would drop the list below this count. It does not pre-create items and is not validated; to actually start with items, set the field’svalue.maxItems(default: unlimited) — the add button is hidden once the list reaches this count.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | 'repeater' | - | Field type (required) |
itemFields | Record<string, FieldConfig> | - | Sub-field definitions for each item (required) |
minItems | number | 0 | Hide the remove button at or below this item count |
maxItems | number | - (unlimited) | Hide the add button at this item count |
addLabel | string | 'Add item' | Label of the add button |
removeLabel | string | 'Remove' | Label of the remove button |
label | string | - | Label shown above the list |
hideLabel | boolean | false | Hide the label visually |
helperText | string | - | Help text shown below the list |
value | Array<Record<string, unknown>> | [] | Initial items |
hidden | boolean | function | ConditionGroup | Responsive | false | Hide the field |
columns | Partial<Record<Breakpoint, number>> | - | Grid columns by breakpoint |
Validation
The repeater bypasses schema validation entirely — the form engine always validates its value as z.any(). This means:
- A
schema(orrequired) set on the repeater field itself is ignored. schemaandrequiredsettings on the sub-fields insideitemFieldsare not enforced either — the form-level validation schema only covers top-level fields, and the whole array is accepted as-is.
If you need to enforce constraints (e.g. at least one item, valid emails inside items), check the array in your onSubmit handler or validate server-side.
How It Works
- The repeater mounts a
useFieldArrayon the field’s name — it relies on thereact-hook-formcontext that<Form>creates, so it only works inside a rendered form (it can’t be used standalone). - Each item renders its sub-fields recursively through the same field renderer used for top-level fields, registered under
{name}.{index}.{subName}. - The add button appends a default item built from the
itemFieldsdefinitions; the remove button deletes the item at its index. - Add/remove buttons hide themselves according to
maxItems/minItems.
Required Components
The repeater itself needs Button (for the add/remove buttons) plus the field wrapper components (FormField, Field, FieldDescription, FieldError). Each sub-field additionally requires its own components — e.g. Input for text sub-fields, the Select cluster for select sub-fields. See the Components guide for how injection works.
Related
- Components & dependency injection — How UI components are provided to fields
- Validation guide — Validation for regular fields (the repeater is exempt, see above)
- Submit & Actions — Where the submitted array ends up