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.
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
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 theroaming_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 newRoamingItem
in the database. See also: Roaming Item. - Some fields (e.g.
BelongsToManyUsers
) are stored as strings in ajson
array, meaning it will cause a visual bug in the dropdown. You can solve this bysetPayloadStoreFormatter
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);
})