Skip to content

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'),
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.

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.

Add appends an empty row, the trash button removes one. On phones the fields of a row are stacked.

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.

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”.

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.

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 RowSourceException is 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.