Repeater
A short list of small records saved as JSON in one column, e.g. phone
numbers or line items. With lookup() the rows are records chosen by a
search instead, e.g. objects of a CMDB.
Use a relation instead when the entries need their own permissions, history or queries.
use Anvil\Schema\Fields\NumberInput;use Anvil\Schema\Fields\Repeater;use Anvil\Schema\Fields\Select;use Anvil\Schema\Fields\TextInput;
Repeater::make('phones')->schema([ Select::make('type')->options(['work' => 'Work', 'mobile' => 'Mobile'])->required(), TextInput::make('number')->tel()->required(),])->maxItems(5),
Repeater::make('line_items')->label('Line items')->schema([ TextInput::make('product')->required()->maxLength(100), NumberInput::make('quantity')->integer()->min(1)->default(1), NumberInput::make('price')->label('Unit price')->currency('EUR')->min(0),])->minItems(1)->maxItems(10)->addLabel('Add line item'),Options
Section titled “Options”| Option | Effect | Default |
|---|---|---|
schema([…]) |
the fields of a row | none |
minItems(n) |
fewest rows; more than 0 makes the field required | 0 |
maxItems(n) |
most rows; the add button is disabled when reached | 20 |
addLabel('…') |
label of the add button | “Add” |
lookup(RowSource, 2, 20) |
rows are records chosen by search, see below | |
fixedRows([uuids], '…') |
records shown read-only above the chosen ones (lookup only) | none |
Plus the options of every field.
Fields in a row
Section titled “Fields in a row”Rows accept simple fields: TextInput, Textarea, MarkdownEditor,
NumberInput, Select, Radio, Toggle, DateInput.
schema() throws an InvalidArgumentException for Repeater,
TagsInput, CheckboxList, BelongsTo/UserSelect, virtual fields
(including FileUpload) and a field named uuid. Defaults of the sub
fields apply to new rows.
Editing
Section titled “Editing”Add appends an empty row, the trash button removes one. On phones the fields of a row are stacked.
Saved value
Section titled “Saved value”A JSON list of objects with the converted values of the sub fields (use a
JSON or TEXT column):
[{"type":"work","number":"+49 30 1234"},{"type":"mobile","number":"+49 170 5678"}]Rows in which every field is empty are dropped; switches do not count, they
always have a value. No rows: null.
Validation
Section titled “Validation”For the field is_array, max_count[n] and min_count[n] (and required
with minItems()); for every row the rules of the sub fields, e.g.
line_items.*.product. A required sub field is only required when the
repeater has rows. Errors are shown at the sub field of the row, labelled
“Line items: Product”.
Read-only
Section titled “Read-only”A table with a column per sub field; values are shown with their
displayText() (option labels, formatted numbers and dates).
An array of objects with a property per sub field (maxItems,
minItems); every sub field converts its value as on its own.
Rows chosen by search
Section titled “Rows chosen by search”With lookup() the rows are records of a RowSource
(Anvil\Schema\RowSource) found by a search: typing (from two characters)
searches, choosing a hit adds a row rendered by the server. Each row shows
the columns of the source, an optional link to the record and the sub
fields of the schema (e.g. a note per item).
Repeater::make('items')->label('Items')->required()->maxItems(30) ->lookup(new CatalogSource(), minLength: 2, limit: 20) ->schema([TextInput::make('note')->maxLength(200)]) ->fixedRows($alreadyAssigned, 'already assigned'),A source implements three methods:
use Anvil\Schema\RowSource;use Anvil\Schema\RowSourceException;
final class CatalogSource implements RowSource{ public function columns(): array { return ['title' => 'Item', 'serial' => 'Serial number']; }
// uuid => label of the matching records public function search(string $term, int $limit): array { // throw new RowSourceException('The catalog cannot be reached.') when it fails return service('catalog')->search($term, $limit); }
// uuid => ['cells' => [column => text], 'url' => link or null], unknown UUIDs left out public function rows(array $uuids): array { return service('catalog')->rows($uuids); }}- Fixed rows (
fixedRows()) are shown above the chosen ones with the label as badge: read only, not removable, not part of the input. The search does not offer them. Typical: the items a person already has. - Only UUIDs are trusted. The browser submits the UUID and the sub
field values of each row. Every UUID must be a record of the source
(otherwise 422), duplicates and fixed rows are dropped, and the shown
values are read from the source again whenever they are needed – they
are never taken from the browser. Read the values for a document or a
write-back with
rows()of the source in the handler. - Errors of the source: a
RowSourceExceptionis shown in the result list; its message must not contain details. - Choosing a record twice highlights the existing row instead of adding
another one. Links open in a new tab (
noopener noreferrer). - The search and the added rows are an action of the form’s component
(
lookup-<action>) with the permission of the form, like a searchable BelongsTo. - Rows with columns are shown as cards on phones, each value with its column label.
Saved value: a JSON list [{"uuid": "…", "note": "…"}]; the API adds the
uuid property to the row objects. The
i-doit package (AnvilIdoit\Ui\ObjectLookup) provides a
source for CMDB objects.