Skip to main content

Advanced Fields

Repeater

The Repeater field makes it possible to repeat fields in a Form and allows the user to add and delete rows.

A basic example:

Repeater::make('My repeated', 'my_column')
->fields(new FieldCollection(
Text::make('Hello repeater!', 'repeat')
->setRepeatedWidth(200) // The width of this field
->default('Hello world')
->rules('required', 'min:3'),
Checkbox::make('Check!', 'check')
))

Storing data

By default, the value (array) is stored as json. In this case, the data will be stored in the my_column column. This means that you need a cast on your model:

    protected $casts = [
'my_column' => 'array'
];

Often though, you will probably not want to save the value. In that case you can add fillLazyUsing on the field:

Repeater::make('My repeated', 'my_column')
->fillLazyUsing(function(Model $model, array $data) {
// dd($data)
})

The Repeater field holds an array as its value inside a form. If you would like to set the state of this field, you will have to pass an array that also includes a uuid. In the following example a Button field will set the state for the Repeater field:

Repeater::make('My repeated', 'my_column')
->fields(new FieldCollection(
Text::make('Hello repeater!', 'repeat'),

Button::make('Button support!', 'button')
->hideOnShow()
->onClick(function(ButtonClickEvent $event) {
if ($event->getForm()) {
$rowIndex = $event->getMetadata('row_index');
$dialogState = $event->getDialogFormState();

$currentState = $event->getForm()->getState('my_column');
$currentState[$rowIndex]['repeat'] = $dialogState['hello'];

$event->getForm()->setState('my_column', $currentState);
}
})
->withFields(function(ButtonFieldsResponse $response) {
$response->setFields(new FieldCollection(
Text::make('Hello', 'hello')
));
})
)),

Configuration

The Repeater is quite large by default, you can set it to dense. You can also set the layout to vertical.

Repeater::make('My repeated', 'my_column')
->dense()
->vertical() // 1 field per row

When the field is cleared, and you want Qore to add at least one row, you can add the following:

->notEmptyOnNull()

You can set the maximum amount of rows:

->max(5)

You can disallow deleting rows:

->deletable(false)

Metadata

You can set the field meta for a repeater field using dot notation. Keep in mind that this meta will be the same for each row.

Repeater::make('My repeater', 'my_repeater')
->fields(new FieldCollection(
Text::make('Text 1', 'text_1'),
BetterTime::make('Time', 'time')
->withMinuteInterval(30)
->withoutDropdown()
)),

Text::make('Name', 'name')
->onUpdate(function(ManagesForm $form) {
$form->setFieldMeta('my_repeater', 'fields.time.minuteInterval', 5);
})

Sort order

You can allow the user to sort the rows in the repeater field by calling the draggable method:

Repeater::make('My repeater', 'my_repeater')->draggable()

Quirks and limitations

Please note that some functionality is limited or disabled for repeatable fields. If you need more advanced functionality, you can use the AdvancedRepeater field instead.

caution

Only a handful of fields are now repeatable ( like Text, Textarea, Select, Button, Checkbox, BelongsTo, BelongsToMany, Date, DateTime), however most fields can be made repeatable easily. For fields like FilePicker, you can use the AdvancedRepeater field.

The ->onUpdate event is not available on individual fields in the repeater. You can use the ->onUpdate event on the repeater field instead:

Repeater::make('My repeated', 'my_column')
->onUpdate(function(ManagesForm $form, $value) {
// To see which row and which field was updated, you can use the following:
dd($form->getStateArgs());
})

If you have a Select field inside your repeater, you can only set the options dynamically for all rows, not for individual rows:

Text::make('Name', 'name')
->onUpdate(function (ManagesForm $form, $value) {
if ($value === 'test') {
$form->setFieldData('test_select', 'options', ['d', 'e', 'f']);
return;
}

$form->setFieldData('test_select', 'options', ['a', 'b', 'c']);
}),

Repeater::make('Test repeater', 'repeater')
->draggable()
->fields(
new FieldCollection(
Text::make('Test Name', 'test_name'),
Select::make('Test Select', 'test_select')
->setRepeatedWidth('md')
->hideClearButton()
->options([
'a',
'b',
'c'
]),
)
)

Making a field repeatable

info

If you want to create your own Repeatable field, or make an existing field repeatable, you need to add the following interface to your field: FieldIsRepeatable. You can then optionally override methods in your field if necessary (BelongsToMany example):

public function getRepeatedFormValue(mixed $value, Model $model): mixed
{
if ($value) {
return json_decode($value);
}

return [];
}

public function getRepeatedDisplayValue(mixed $value, Model $model): mixed
{
if (is_null($value)) {
return null;
}

$value = $this->otherResource->model()::whereIn('id', json_decode($value))->get();

return $value->map(function (Model $model) {
return array_merge([
'id' => $model->id,
'url' => $this->otherResource->url($model->id),
'title' => $this->otherResource->modelTitle($model)
], $this->otherResource->toHttpResource($model)->toArray(request()));
});
}

In your Field.vue in the front-end, you will receive 3 props: isRepeated, repeatedIndex and dense. Based on these props you can adjust your field to work well as a repeatable.

AdvancedRepeater

The AdvancedRepeater field allows more functionality than the Repeater by making use of BaseForm, aiming to make it work with any field. It works by storing field data as a RoamingItem, making fields like FilePicker and Image possible.

AdvancedRepeater::make('Employees', 'employees')
->fields(function () {
return new FieldCollection(
Text::make('Name', 'name')
->setMaxWidth('sm'),
Text::make('Function', 'function'),
Image::make('Image', 'image'),
);
})->setTitle(function(RoamingItem $item) {
return $item->payload['name'] ?? 'Unknown';
})->setDescription(function(RoamingItem $item) {
return $item->payload['function'] ?? 'Unknown';
})

Important notes

A few things are worth noting about the AdvancedRepeater before you run into unexpected behavior:

  • There are some caching mechanisms in place. You should pick a unique name for your field to avoid conflicts.
  • Nothing is logged by default. You can put your own logging in place if you want to.
  • Every item (row) in the repeater is a RoamingItem, meaning that it exists in the roaming_items table, also meaning you should clean up this table based on your project conditions. If you want to set a default value, you should seed a new RoamingItem in the database. See also: Roaming Item.
  • Some fields (e.g. BelongsToManyUsers) are stored as strings in a json array, meaning it will cause a visual bug in the dropdown. You can solve this by setPayloadStoreFormatter on the field and casting the values to integers (more info below).

Storing data

By default, RoamingItem is saved. You can override this:

->fillLazyUsing(function (Model $model, array $data) {
$value = $data[$this->name] ?? [];
$sortOrder = 0;

foreach ($value as $item) {
$roamingItem = RoamingItem::query()->where('uuid', $item['uuid'])->first();

if (! $roamingItem) {
continue;
}

// Do something with the roaming item..

//
$roamingItem->delete();
}
});

Display

You can configure how you want to display your items. By default it supports title, description and image:

})->setTitle(function (RoamingItem $item) {
return $item->payload['name'] ?? 'Unknown';
})
->setDescription(function (RoamingItem $item) {
return $item->payload['function'] ?? 'Unknown';
})
->setImage(function (RoamingItem $item) {
$media = $item->getMedia()->first();

if ($media) {
return media_to_base64($media);
}

return null;
})

Custom display

If you need more control over the display, you can register your own Item component, and use it:

->setItemComponent('TestRepeaterItem')
Vue.component('TestRepeaterItem', () => import('../TestRepeaterItem'))
<template>
<div class="flex items-center gap-md">
<q-img
src="https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZTE3ZmxwMXVmMnVqdmc2ZGkxb2FzZW02YWloNmJpdm1qMHRtM3h0dSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/qZgHBlenHa1zKqy6Zn/giphy.gif"
:style="`width: 60px; height: auto;`"
/>

<div class="column">
<span class="text-bold">{{ item.title }}</span>
<div class="text-italic text-primary">
Some description
</div>
<span>{{ item.a_test }}</span>
</div>
</div>
</template>

<script>
export default {
props: {
item: {
type: Object,
required: true
},
imageWidth: {
type: String,
required: true
},
imageHeight: {
type: String,
required: true
}
}
}
</script>

You can also add extra data to the display:

->setItemData(function (RoamingItem $item) {
return [
'a_test' => $item->created_at->diffForHumans(),
];
})

Configuration

You can listen to when fields are ready:

->onFieldsReady(function(ManagesForm $form) {
//
})

You can set the maximum amount of rows:

->max(3)

Listen to events (e.g. for logging):

->onItemCreated(function (RoamingItem $item) {

})
->onItemUpdated(function (RoamingItem $item) {

})
->onItemDeleted(function (RoamingItem $item) {

})

Set image size:

->setImageSize('80px', '80px')

Set dialog size:

->setDialogWidth(1200)

Set min display width (for detail):

->setMinDisplayWidth('300px')

Set field layout:

->fieldLayout(function() {
return (new FieldLayout)->(...)
})

Disable deleting:

->deletable(false)

Sort order

You can make rows draggable by calling the draggable method:

->draggable()

Quirks

Some fields are incorrectly casted in the payload json array.

For example, the BelongsToManyUsers options are stored as a strings in the payload instead of integers.

You can fix this by using setPayloadStoreFormatter:

->setPayloadStoreFormatter(function (array $payload) {
$payload['users'] = array_map(function ($id) {
return (int)$id;
}, $payload['users'] ?? []);

return $payload;
})

Mutating state

If you want to set a default value, or set the state from another field (outside the repeater):

Text::make('Name', 'name')
->onUpdate(function (ManagesForm $form, $value) {
if ($value !== 'test') {
return;
}

$currentState = $form->getState('employees');

/** @var RoamingItem $newItem */
$newItem = RoamingItem::create([
'uuid' => Str::uuid(),
'payload' => [
'name' => 'Test name',
'function' => 'Test function',
]
]);

$currentState[] = $newItem;
$form->setState('employees', $currentState);
})