<HeadlessRepeatable> component
The <HeadlessRepeatable> component provides a way to handle repeatable form fields (arrays) with no built-in UI. It manages the state and logic for array operations while allowing complete control over the presentation.
📔 For a fully working example check out the Headless components example
The component must be used within an `EncolaForm` or `HeadlessForm` component
Basic Usage
Here's a simple example of using <HeadlessRepeatable> to manage a list of skills:
<HeadlessRepeatable name="skills" :min="0" :max="5">
<template #default="{ value, add, remove, canAdd, canRemove, moveUp, moveDown, count }">
<div v-for="(skill, index) in value" :key="index">
<HeadlessField :name="`skills.${index}.name`">
<template #default="{ attrs, events, id }">
<input
:id="id"
v-bind="attrs"
v-on="events"
/>
</template>
</HeadlessField>
<button v-if="canRemove" @click="remove(index)">Remove</button>
<button v-if="index > 0" @click="moveUp(index)">Move Up</button>
<button v-if="index < count - 1" @click="moveDown(index)">Move Down</button>
</div>
<button v-if="canAdd" @click="add()">Add Skill</button>
</template>
</HeadlessRepeatable>Table Layout Example
For less complex data structures, you might want to use a table layout:
<HeadlessRepeatable name="skills" :min="0" :max="5">
<template #default="{ value, add, remove, canAdd, moveUp, moveDown, count }">
<table>
<thead>
<tr>
<th>Name</th>
<th>Level</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(skill, index) in value" :key="index">
<td>
<HeadlessField :name="`skills.${index}.name`">
<template #default="{ attrs, events, id }">
<input :id="id" v-bind="attrs" v-on="events" />
</template>
</HeadlessField>
</td>
<td>
<HeadlessField :name="`skills.${index}.level`">
<template #default="{ attrs, events, id }">
<select :id="id" v-bind="attrs" v-on="events">
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</select>
</template>
</HeadlessField>
</td>
<td>
<button v-if="canRemove" @click="remove(index)">Remove</button>
<button v-if="index > 0" @click="moveUp(index)">↑</button>
<button v-if="index < count - 1" @click="moveDown(index)">↓</button>
</td>
</tr>
</tbody>
</table>
<button v-if="canAdd" @click="add()">Add Skill</button>
</template>
</HeadlessRepeatable>Validation
The component supports validation through the parent form. You can validate on add or remove operations.
The validation refers to the array, not individual fields/items. For example if you want to have between 3 and 5 skills and you set up validation rules for this, you can use this props to validate the skills on add/remove/both/none
<HeadlessRepeatable
name="skills"
:min="1"
:max="5"
:validate-on-add="true"
:validate-on-remove="true"
>
<!-- ... template content ... -->
</HeadlessRepeatable>Default Values
You can provide default values for new items:
<HeadlessRepeatable
name="skills"
:default-value="{ name: '', level: 'beginner' }"
>
<!-- ... template content ... -->
</HeadlessRepeatable>Best Practices
- Always provide a
:keybinding when iterating over items usingv-for - Use appropriate min/max values to prevent invalid states
- Consider using validation to ensure data integrity
- Implement proper error handling and user feedback
- Use semantic HTML elements for better accessibility
- Consider mobile responsiveness when designing the layout
Common Patterns
Grid Layout
For complex forms with multiple fields per item:
<HeadlessRepeatable name="experience" :min="0" :max="10">
<template #default="{ value, add, remove, canAdd, moveUp, moveDown, count }">
<div v-for="(exp, index) in value" :key="index" class="grid grid-cols-2 gap-4">
<HeadlessField :name="`experience.${index}.company`" label="Company">
<template #default="{ attrs, events, id }">
<input :id="id" v-bind="attrs" v-on="events" />
</template>
</HeadlessField>
<HeadlessField :name="`experience.${index}.position`" label="Position">
<template #default="{ attrs, events, id }">
<input :id="id" v-bind="attrs" v-on="events" />
</template>
</HeadlessField>
<!-- Action buttons -->
<div class="col-span-2">
<button v-if="canRemove" @click="remove(index)">Remove</button>
<button v-if="index > 0" @click="moveUp(index)">Move Up</button>
<button v-if="index < count - 1" @click="moveDown(index)">Move Down</button>
</div>
</div>
<button v-if="canAdd" @click="add()">Add Experience</button>
</template>
</HeadlessRepeatable>Nested Repeatables
For complex nested data structures:
<HeadlessRepeatable name="sections" :min="1">
<template #default="{ value, add, remove, canAdd, moveUp, moveDown, count }">
<div v-for="(section, index) in value" :key="index">
<HeadlessField :name="`sections.${index}.title`">
<template #default="{ attrs, events, id }">
<input :id="id" v-bind="attrs" v-on="events" />
</template>
</HeadlessField>
<HeadlessRepeatable :name="`sections.${index}.items`" :min="1">
<template #default="{ value: items, add: addItem, remove: removeItem }">
<div v-for="(item, itemIndex) in items" :key="itemIndex">
<HeadlessField :name="`sections.${index}.items.${itemIndex}.content`">
<template #default="{ attrs, events, id }">
<input :id="id" v-bind="attrs" v-on="events" />
</template>
</HeadlessField>
<button @click="removeItem(itemIndex)">Remove Item</button>
</div>
<button @click="addItem()">Add Item</button>
</template>
</HeadlessRepeatable>
<button v-if="canRemove" @click="remove(index)">Remove Section</button>
</div>
<button v-if="canAdd" @click="add()">Add Section</button>
</template>
</HeadlessRepeatable>