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')- addLabel:"Add block"
- columnWidth:"md"
- conditions:null
- defaultItems:0
- dependsOnAny:false
- dependsOnKeys:null
- disabled:false
- editablePrefill:false
- helperText:null
- label:"Line items"
- labelAction:null
- layout:"stack"
- maxItems:null
- minItems:1
- name:"items"
- nestedRowsKey:null
- prefillRefreshOn:null
- prefillResetOn:null
- readOnly:false
- reorderable:true
- required:false
- resizableColumns:false
- resizeIndicator:false
- rowActions:null
- tooltip:null
- value:null
- autoFocus:false
- columnWidth:"md"
- conditions:null
- dependsOnAny:false
- dependsOnKeys:null
- disabled:false
- editablePrefill:false
- helperText:null
- label:"Content"
- labelAction:null
- name:"content"
- placeholder:null
- prefillRefreshOn:null
- prefillResetOn:null
- readOnly:false
- required:true
- rows:null
- tabIndex:null
- tooltip:null
- value:null
- autoComplete:null
- autoFocus:false
- columnWidth:"md"
- conditions:null
- copyable:false
- dependsOnAny:false
- dependsOnKeys:null
- disabled:false
- editablePrefill:false
- helperText:null
- label:"Product"
- labelAction:null
- name:"product"
- placeholder:null
- prefillRefreshOn:null
- prefillResetOn:null
- prefix:null
- prefixFieldName:null
- readOnly:false
- required:true
- suffix:null
- suffixFieldName:null
- tabIndex:null
- tooltip:null
- type:null
- value:null
- autoComplete:null
- autoFocus:false
- columnWidth:"md"
- conditions:null
- copyable:false
- dependsOnAny:false
- dependsOnKeys:null
- disabled:false
- editablePrefill:false
- helperText:null
- label:"Qty"
- labelAction:null
- name:"qty"
- placeholder:null
- prefillRefreshOn:null
- prefillResetOn:null
- prefix:null
- prefixFieldName:null
- readOnly:false
- required:false
- suffix:null
- suffixFieldName:null
- tabIndex:null
- tooltip:null
- type:null
- value:null
- autoComplete:null
- autoFocus:false
- columnWidth:"md"
- conditions:null
- copyable:false
- dependsOnAny:false
- dependsOnKeys:null
- disabled:false
- editablePrefill:false
- helperText:null
- label:"Price"
- labelAction:null
- name:"price"
- placeholder:null
- prefillRefreshOn:null
- prefillResetOn:null
- prefix:null
- prefixFieldName:null
- readOnly:false
- required:false
- suffix:null
- suffixFieldName:null
- tabIndex:null
- tooltip:null
- type:null
- value:null
Row templates
Section titled “Row templates”->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.
Adding rows
Section titled “Adding rows”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');Row counts
Section titled “Row counts”->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);Reordering
Section titled “Reordering”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);Table layout
Section titled “Table layout”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.
Row actions
Section titled “Row actions”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.
Validation
Section titled “Validation”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.
Common options
Section titled “Common options”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.