Fields
Repeater
Repeater holds rows of fields, as cards, a grid or a table, saved to JSON or a HasMany relationship.
use Xelqun\Forms\Components\Repeater;
use Xelqun\Forms\Components\TextInput;
Repeater::make('members')
->schema([
TextInput::make('name')->required(),
TextInput::make('role')->required(),
])
->columns(2)
->defaultItems(1)
->addActionLabel('Add member');
Rows have stable keys, so errors land on the right row even after a drag. The state is a list, so a model needs an array cast, unless you use relationship().
Limits
Repeater::make('members')->schema([...])->minItems(1)->maxItems(10);
What people can do
| Method | What it does |
|---|---|
addable(false) |
Stops adding rows. A row posted anyway is dropped on the server. |
deletable(false) |
Stops deleting rows. A row removed anyway comes back on the server. |
reorderable(false) |
Stops reordering. |
reorderableWithDragAndDrop(false) |
Turns dragging off. The move buttons stay, for keyboard and touch users. |
cloneable() |
Adds a button that copies a row. File fields start empty in the copy. |
collapsible(), collapsed() |
Lets rows collapse, or starts them collapsed. A row with an error always opens. |
requiresDeleteConfirmation() |
Asks before deleting a row. |
itemLabel() |
The singular name of one row, used in headings and error messages. |
itemNumbers() |
Shows each row's number. |
extraItemActions() |
Adds buttons to each row's header. $get, $set and $record work on that row. |
A list of one field
simple() makes rows of one field, with a plain list of values as the state.
Repeater::make('emails')->simple(TextInput::make('email')->email()->required());
A grid of cards
Repeater::make('members')->schema([...])->grid(2);
A table
table() shows the rows as a table, with one column per field, in order.
use Xelqun\Forms\Components\Repeater\TableColumn;
Repeater::make('items')
->table([
TableColumn::make('Product'),
TableColumn::make('Qty')->width('100px'),
])
->schema([
TextInput::make('product')->required(),
TextInput::make('qty')->integer()->required(),
]);
hiddenHeaderLabel() hides a heading on screen, and markAsRequired() shows an asterisk.
Calculated rows
computed() works inside a row. A value in a row is read by its name, and the total reads every row.
Repeater::make('items')->schema([
TextInput::make('qty')->numeric(),
TextInput::make('price')->numeric(),
TextInput::make('subtotal')->computed('qty * price')->disabled(),
]);
TextInput::make('total')->computed('sum(items.*.subtotal)')->disabled();
Saving to a relationship
relationship() saves rows to a HasMany. The name defaults to the field's.
Repeater::make('items')
->relationship(orderColumn: 'sort')
->schema([...]);
These change a related row's data on its way through:
| Method | When it runs |
|---|---|
mutateRelationshipDataBeforeFillUsing() |
Before the row fills the form. |
mutateRelationshipDataBeforeCreateUsing() |
Before a new row is created. Return null to skip it. |
mutateRelationshipDataBeforeSaveUsing() |
Before an existing row is saved. Return null to skip it. |
afterCreate(), afterUpdate(), afterDelete() |
After each change, with $data and $record. |
No duplicates
distinct() on a field inside a repeater makes sure no two rows share a value.
Select::make('product_id')->options($products)->distinct();