Skip to content
Fields

Repeater ​

Erag\InertiaForms\Fields\Repeater repeats one group of fields as a list of items. Users add items, fill them in, reorder, collapse and delete them. It works like Blocks with a single block type, but stores plain rows instead of { type, data } items. Like every other field, it is built into the package with Tailwind CSS and needs no extra library.

When to use: social links, contact people, FAQ entries, order lines, speakers, or any list where every entry has the same few fields. When entries can be of different kinds (a heading, a quote, an image), use Blocks.

Try it on the Landing page and All fields forms in the Live Demo.

Examples ​

Each example is a live form built from the PHP below it. Fill it in and press submit to see the data your controller would receive.

Basic ​

A list of links. Each item has the same two fields, and the value is a plain array of rows.

php
use Erag\InertiaForms\Fields\Repeater;
use Erag\InertiaForms\Fields\TextInput;

Repeater::make('links')
    ->itemLabel('Link')
    ->fields([
        TextInput::make('label')->required(),
        TextInput::make('url')->label('URL')->url()->required(),
    ]);

Item titles ​

titleFrom('question') shows the question in the item header once it is typed. maxItems(5) disables the add button after five questions.

php
use Erag\InertiaForms\Fields\Repeater;
use Erag\InertiaForms\Fields\Textarea;
use Erag\InertiaForms\Fields\TextInput;

Repeater::make('faq')
    ->label('FAQ')
    ->itemLabel('Question')
    ->titleFrom('question')
    ->maxItems(5)
    ->fields([
        TextInput::make('question')->required(),
        Textarea::make('answer')->rows(2)->required(),
    ]);

Advanced: grid, limits and defaults ​

Two contacts are filled in with default() and start collapsed. Each item uses a two-column grid, and minItems(1) keeps at least one contact in the list.

php
use Erag\InertiaForms\Fields\Combobox;
use Erag\InertiaForms\Fields\Repeater;
use Erag\InertiaForms\Fields\TextInput;
use Erag\InertiaForms\Fields\Toggle;

Repeater::make('contacts')
    ->label('Project contacts')
    ->itemLabel('Contact')
    ->addActionLabel('Add a contact')
    ->titleFrom('name')
    ->columns(2)
    ->minItems(1)
    ->maxItems(4)
    ->collapsed()
    ->default([
        ['name' => 'Aisha Khan', 'email' => 'aisha@example.com', 'role' => 'owner', 'notify' => true],
        ['name' => 'Ravi Mehta', 'email' => 'ravi@example.com', 'role' => 'reviewer'],
    ])
    ->fields([
        TextInput::make('name')->required(),
        TextInput::make('email')->email()->required(),
        Combobox::make('role')->options([
            'owner' => 'Owner',
            'reviewer' => 'Reviewer',
            'viewer' => 'Viewer',
        ])->required(),
        Toggle::make('notify')->label('Email updates'),
    ]);

How it works ​

  • The add button (Add question in the example) appends a new item straight away. There is no menu, because there is only one kind of item. The new item opens and its first field gets focus.
  • Each item is a card. Its header shows the item title: the value of the titleFrom() field once it is filled in, otherwise the item label and position, like "Question 2".
  • Click the header to collapse or expand an item. Collapse all / Expand all toggles every item.
  • Drag the ⋮⋮ handle to move an item, or use the up and down buttons. The trash button deletes it.
  • Fields inside an item work like fields anywhere else: every built-in field, custom fields, visibleWhen() (checked against the item's own values), columnSpan() and help text.
  • When the server returns errors for an item, that item opens and gets a red border.

Methods ​

All common field methods are available, plus:

fields(array $fields) ​

The fields of each item. Their names are relative to the item, so question is stored as faq.0.question.

itemLabel(string $label) ​

The name of one item, used in item titles ("Question 2"), the default add button text and validation messages. Defaults to "Item".

titleFrom(?string $field) ​

Use a field's value as the item title once it is filled in.

columns(int $columns) ​

Lay each item's fields out in a grid of 1 to 6 columns. Fields use columnSpan() as usual.

php
Repeater::make('contacts')
    ->itemLabel('Contact')
    ->columns(2)
    ->fields([
        TextInput::make('name')->required(),
        TextInput::make('email')->email(),
        TextInput::make('role')->columnSpan(2),
    ]);

addActionLabel(?string $label) ​

Text of the add button. Defaults to "Add" plus the item label in lower case, like "Add contact".

minItems(?int $count) / maxItems(?int $count) ​

How many items are allowed. Delete buttons are disabled at the minimum and the add button at the maximum; the server checks both too.

reorderable(), addable(), deletable(), collapsible() ​

Turn off the drag handle and move buttons, the add button, the delete buttons, or collapsing. All are on by default.

collapsed(bool $collapsed = true) ​

Start existing items collapsed.

Validation rules ​

AttributeRules
faqnullable (or required), array, min:<minItems>, max:<maxItems>
faq.*array
faq.N.<field>the rules of that field, for every submitted item
  • Each item is validated with its fields' rules. Fields hidden by visibleWhen() inside an item are skipped.
  • Messages name the item: "The Label (Link 2) field is required."
  • With required() or minItems(1), an empty list fails.

Value ​

The value is a list of rows, one array per item:

json
[
    { "question": "Can I cancel any time?", "answer": "Yes, from the billing page." },
    { "question": "Is there a free plan?", "answer": "Yes, for up to three users." }
]

$form->validated() returns the same shape, but each row only keeps the item's own visible fields. Unknown keys are dropped:

php
// submitted: [['label' => 'Docs', 'url' => 'https://erag.in', 'extra' => 'x']]
$form->validated('links');
// [['label' => 'Docs', 'url' => 'https://erag.in']]

That is ready to store in a json / array cast column. Defaults, bound models and JSON strings are filled in the same way, and missing fields get their starting value: default([['label' => 'Docs']]) becomes [['label' => 'Docs', 'url' => '']].

Empty value: [] (no items).

Nesting

A Repeater or Blocks field inside a Repeater item is not supported yet.

Standalone use ​

vue
<Repeater v-model="faq" :field="faqField" id="faq" :disabled="false" />
tsx
<Repeater field={faqField} id="faq" value={faq} disabled={false} onChange={setFaq} />
svelte
<Repeater field={faqField} id="faq" bind:value={faq} disabled={false} />

Repeater is the Blocks component in repeater mode (it switches when field.component is 'Repeater'). Start with a list of row objects (or []) as the value. See Standalone Components.