Skip to content

Builder

The builder renders a repeatable list of rows where each row can be a different type. You define the available row templates up front, and every template carries its own schema of fields. A row records which template it uses through an automatic hidden type discriminator, so the field submits an array of row objects — each tagged with its type and keyed by that template’s field names. Create one with Builder::make() and list the templates with ->templates().

Builder::make('items', 'Line items')
->templates([
RowTemplate::make('text')->label('Text')->schema([
Textarea::make('content', 'Content')->required(),
]),
RowTemplate::make('product')->label('Product line')->schema([
TextInput::make('product', 'Product')->required(),
TextInput::make('qty', 'Qty')->rules(['numeric']),
TextInput::make('price', 'Price')->rules(['numeric']),
]),
])
->minItems(1)
->addLabel('Add block')

->templates() takes the list of row types the builder offers. Each is a RowTemplate::make($type) where $type is the discriminator stored on every row of that kind. Give it a human label with ->label() and its row schema with ->schema(); the schema is a normal array of fields, so each child keeps its own label, placeholder, default value, and rules.

RowTemplate::make('product')
->label('Product line')
->schema([
TextInput::make('product', 'Product')->required(),
TextInput::make('qty', 'Qty')->rules(['numeric']),
]);

The example above submits as an array of typed rows:

['items' => [
['type' => 'text', 'rowId' => '9f3c…', 'content' => 'Thanks for your order.'],
['type' => 'product', 'rowId' => 'a2b1…', 'product' => 'Widget', 'qty' => '3', 'price' => '9.99'],
]]

Every row carries a stable UUID under the reserved rowId key: rows filled from stored data keep theirs, new rows get one when they are added, and it survives validation into your handle() data — so rows stay identifiable across saves (collect($data['items'])->keyBy('rowId') gives a map view). Row schemas must not declare their own rowId field.

Each builder row is inserted through an add menu listing every template by its label. Picking one appends an empty row of that type. ->addLabel() sets the text on the button that opens the menu (default “Add”).

Builder::make('items', 'Line items')
->templates([RowTemplate::make('text')->schema([Textarea::make('content')])])
->addLabel('Add block');

->minItems() and ->maxItems() bound how many rows the form accepts; both are enforced as array-level validation rules independent of which template each row uses.

Builder::make('items', 'Line items')
->templates([RowTemplate::make('text')->schema([Textarea::make('content')])])
->minItems(1)
->maxItems(20);

Rows are reorderable by default through up/down controls. Pass ->reorderable(false) to fix the order.

Builder::make('items', 'Line items')
->templates([RowTemplate::make('text')->schema([Textarea::make('content')])])
->reorderable(false);

Rows stack by default — each row is a full block of stacked fields. Call ->table() to lay rows out as a table instead. The columns come from the first template’s schema, a shared header of those field labels sits above the rows, and each cell renders the input alone (the field’s own label moves up into the header). A row of any other type spans the full width below the header rather than fitting the columns.

Builder::make('items', 'Line items')
->templates([
RowTemplate::make('product')->label('Product line')->schema([
TextInput::make('product', 'Product'),
TextInput::make('qty', 'Qty')->rules(['numeric']),
]),
RowTemplate::make('text')->label('Text')->schema([
Textarea::make('content', 'Content'),
]),
])
->table();

Reorder controls (↑↓) sit on the left of each row and the row actions on the right; once a row carries more than one action they collapse into a ⋯ menu. Reordering slides each row to its new position, and the table scrolls horizontally on narrow screens. The slide animation is skipped when the viewer prefers reduced motion.

Like the repeater, the builder takes ->rowActions([RowAction::duplicate(), RowAction::remove()]) to declare the per-row menu — the built-in Duplicate and Remove actions, customisable with ->label()/->icon()/->danger(). See Repeater → Row actions for the full rundown.

Each row is validated against the schema of its own template: a child marked ->required() is required only in rows of that type, and an error points at the offending row. The hidden type discriminator is validated too — a row whose type is not one of the declared templates is rejected. The array-level minItems/maxItems rules validate the number of rows independently of the per-row rules. See Validation for how field rules are resolved.

Builder shares label, required, disabled, read-only, and visibility options with every field — see Fields. For validation and conditional behavior, see Validation and Conditional fields.