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);
})