Forms
Builder
A list whose every item picks its own block type and schema — where a repeater repeats one shape, a builder chooses among several.
On this page
Block builder for heterogeneous content: a list of items where each item picks its own block type and is edited with that block's schema. Where a Repeater repeats one schema, a Builder chooses among several — the shape behind a page builder or a rich content field.
Basic Usage
use NyonCode\WireForms\Components\Block;use NyonCode\WireForms\Components\Builder; Builder::make('content') ->blocks([ Block::make('heading')->icon('star')->schema([ TextInput::make('text')->rules(['required']), ]), Block::make('paragraph')->schema([ Textarea::make('body'), ]), Block::make('image')->schema([ FileUpload::make('file'), TextInput::make('alt'), ]), ]) ->reorderable()
The "add" trigger opens a picker listing every declared block; choosing one appends an item of that type.
Stored Shape
Each item is stored as its type plus its data:
[ ['type' => 'heading', 'data' => ['text' => 'Hello']], ['type' => 'paragraph', 'data' => ['body' => 'World']],]
Fields bind under <statePath>.<index>.data, so a block's schema needs no
knowledge of its position — and a field named type inside a block cannot
collide with the item's own discriminator. Cast the attribute to array (or
json) on the model.
It Is a Repeater
Builder extends Repeater, so it shares add/remove/reorder, per-item
reactivity, item limits and the form runtime's treatment of a repeated subtree:
Builder::make('content') ->blocks([...]) ->minItems(1) ->maxItems(20) ->reorderable() ->cloneable() ->collapsible() ->expandLast() ->itemLabel(fn (array $state) => $state['text'] ?? null) ->addButtonLabel('Add block')
Everything a repeater row can do, a block can: it is dragged with the same
controller, moved with the same keyboard buttons, duplicated by the same
endpoint, and folded under the same expansion policy. Two details are the
Builder's own — itemLabel() is handed the block's data envelope rather than
the item, so the closure sees the block's own fields; and the name renders
beside the block's label rather than instead of it, because which block a row
is remains the first thing a reader needs.
Two things do not apply. relationship(): mixed block types have no single
related model, so a builder is stored as an array rather than saved through a
relation — and with no relation there is no key to strip, so a duplicated block
is a plain copy. And table(), which throws: a table lays one schema out as
columns, and a builder's items each carry a different one.
Validation
Block field rules mount under the item's data envelope, at
<path>.*.data.<field>. Because the resolver validates by wildcard path, blocks
sharing a field name share its rules — rules are only as strict as the loosest
block declaring that name. Name fields distinctly where blocks must validate
differently.
Blocks That No Longer Exist
Stored content outlives the code that declared it. An item whose stored type names no declared block renders its type as the header and no fields, rather than making the whole form unrenderable — so the content can still be recognised, reordered, or removed.
Declaring a Block
Block::make(string $name)->label(string|Closure $label) // header label (auto-generated from name)->icon(string|Icon $icon) // shown in the picker and the item header->schema(array $components) // the fields this block is edited with
A Block is a definition, not a rendered surface: placing one directly in a form
schema throws a FormConfigurationException.
Builder API
->blocks(array $blocks) // the block types this builder can place->getBlocks(): array->getBlock(string $name): ?Block->getItemType(mixed $item): ?string->table(bool $condition = true) // throws: see below// plus the whole Repeater API: addable, deletable, reorderable,// collapsible, collapsed, minItems, maxItems, addButtonLabel
table() is the one part of the Repeater API that does not carry over. The
table layout lays a single schema out as columns, and a builder's items each
carry a different block's schema, so there is no shared set of columns to head.
Calling it throws a FormConfigurationException rather than accepting the flag
and rendering the ordinary builder regardless.
Related Docs
- Repeater — repeat one schema instead of choosing among several
- Validation