Skip to content

<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:

html
<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:

html
<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

html
<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:

html
<HeadlessRepeatable 
  name="skills" 
  :default-value="{ name: '', level: 'beginner' }"
>
  <!-- ... template content ... -->
</HeadlessRepeatable>

Best Practices

  1. Always provide a :key binding when iterating over items using v-for
  2. Use appropriate min/max values to prevent invalid states
  3. Consider using validation to ensure data integrity
  4. Implement proper error handling and user feedback
  5. Use semantic HTML elements for better accessibility
  6. Consider mobile responsiveness when designing the layout

Common Patterns

Grid Layout

For complex forms with multiple fields per item:

html
<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:

html
<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>

Released under the MIT License