Skip to content

Builder

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

Builder::make('items', 'Line items')
->blocks([
Block::make('text')->label('Text')->schema([
Textarea::make('content', 'Content')->required(),
]),
Block::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')

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

Block::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', 'content' => 'Thanks for your order.'],
['type' => 'product', 'product' => 'Widget', 'qty' => '3', 'price' => '9.99'],
]]

Each builder row is inserted through an add menu listing every block by its label. Picking a block 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')
->blocks([Block::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 block each row is.

Builder::make('items', 'Line items')
->blocks([Block::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')
->blocks([Block::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 block’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 block type spans the full width below the header rather than fitting the columns.

Builder::make('items', 'Line items')
->blocks([
Block::make('product')->label('Product line')->schema([
TextInput::make('product', 'Product'),
TextInput::make('qty', 'Qty')->rules(['numeric']),
]),
Block::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()/->destructive(). See Repeater → Row actions for the full rundown.

Each row is validated against the schema of its own block: a child marked ->required() is required only in rows of that block 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 blocks 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.

These are follow-ups, not part of the first release:

  • Built-in image and subtotal blocks.
  • Nested builders (a builder inside a builder row).
  • Customer-aware price prefill — tracked as separate upcoming work.

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.