DXRepeater
A repeatable sub-form (field array) primitive for nested, dynamic collections —
order lines, variants, key/value rows, and the like. Each row binds to an array
on form.data (e.g. form.data.lines[i]) and renders its sub-fields through
DXField, using Laravel-style nested error keys (lines.0.unit_price).
You normally declare a repeater as a field with type: 'repeater' inside a
DXForm, which renders DXRepeater for you. It is also
exported directly for custom layouts.
Live Examples
Order Line Items
A repeatable sub-form (field array). Add and remove rows; each row binds to form.data.lines[i] with Laravel-style nested error keys.
{
"reference": "ORD-1001",
"lines": [
{
"description": "Widget",
"quantity": 2,
"unit_price": 9.99
}
]
}Table layout (compact mode)
Setting repeaterLayout: 'table' renders sub-fields as columns with one row per item — far more compact than the cards layout above for simple 2-3-field rows.
Props
| Name Click to sort ascending | Type | Required Click to sort ascending | Default | Description |
|---|---|---|---|---|
form | UseFormReturn<any> | Yes | - | Form instance owning the repeater array. |
field | FieldDefinition | Yes | - | The repeater field definition. Provides sub-fields (field.fields), addLabel, minItems, maxItems, softDeleteKey, repeaterLayout, showRowIndex. |
keyPath | string | No | field.key | Dot path into form.data for the array. |
errorKey | string | No | keyPath | Base error key for nested validation. |
model | any | No | - | Model passed to predicates from the parent context. |
Defining a repeater field
Declare it as a field within a form’s fields array:
const fields = [
{
key: 'lines',
type: 'repeater',
label: 'Line items',
addLabel: 'Add line',
minItems: 1,
maxItems: 10,
fields: [
{ key: 'description', type: 'text', label: 'Description' },
{ key: 'quantity', type: 'number', label: 'Qty', default: 1 },
{ key: 'unit_price', type: 'currency', label: 'Unit price' },
],
},
];
New rows are seeded from each sub-field’s default (or a sensible per-type
default). Rows keep stable identity across reordering/removal, so editing a row
and removing another never shuffles your inputs.
In cards layout, each row’s header shows only a Remove button by default —
set showRowIndex: true to also show its 1-based position (e.g. “1”, “2”)
next to it, for cases where rows are meaningfully ordered/numbered. Not
applicable to table layout, which has no equivalent column.
Upsert-children APIs (softDeleteKey)
Some Laravel backends manage child rows via an upsert contract: existing rows
carry an id and are only deleted server-side when submitted flagged, e.g.
{ id, to_delete: true }. Splicing a removed row out of the array (the
default behaviour) never tells such a server the row was removed — it just
silently survives.
Set softDeleteKey to the sub-field name the server expects, and removing a
row that already has an id flags it instead of deleting it from the array:
const fields = [
{
key: 'lines',
type: 'repeater',
label: 'Line items',
softDeleteKey: 'to_delete',
fields: [
{ key: 'description', type: 'text', label: 'Description' },
{ key: 'unit_price', type: 'currency', label: 'Unit price' },
],
},
];
A row without an id (freshly added, never persisted) is still spliced away
as normal — the server never saw it. Flagged rows stay in form.data (so
they’re submitted) but are hidden from the UI and excluded from
minItems/maxItems counts.
Table layout (compact mode)
The default cards layout stacks each row’s sub-fields vertically inside its
own bordered box — right for rows with several or complex sub-fields, but
tall for simple lists. Set repeaterLayout: 'table' for a compact
one-row-per-item table instead, where sub-fields become columns and Remove is
a trailing icon-button cell:
const fields = [
{
key: 'lines',
type: 'repeater',
label: 'Line items',
repeaterLayout: 'table',
fields: [
{ key: 'description', type: 'text', label: 'Description' },
{ key: 'quantity', type: 'number', label: 'Qty', default: 1 },
{ key: 'unit_price', type: 'currency', label: 'Unit price' },
],
},
];
table is a preference, not a guarantee. A table has no room to shrink
into a narrow column, and the same viewport width can put this repeater in
a wide standalone form or a cramped sidebar column — a viewport breakpoint
can’t tell those apart. Instead, a ResizeObserver measures the repeater’s
own rendered width and falls back to the cards layout whenever there
isn’t enough room for its columns to stay legible, scaling the required
width to this repeater’s own column count and types (a currency/percentage
column gets extra budget for its affix). Same field config either way, no
extra work — and no layout-swap flicker, since the table and its cards
fallback are both always present in the DOM (toggled via v-show, not
conditionally rendered) so the measurement never has a hidden element to
contend with.
Column headers come from each sub-field’s label. The #row slot still fully
overrides a row’s rendering in table layout — it renders as the <tr>’s
children, so supply one <td> per sub-field plus a trailing <td> to match
the header’s column count (the header always reserves an empty column for the
built-in remove button), and call the slot’s remove() binding yourself since
the built-in Remove control is bypassed along with the rest of the row. For
table-shaped editors with computed columns beyond a simple field list, build
your own table on the raw DXField/DTable primitives instead.
Slots
| Name Click to sort ascending | Description | Scoped Props |
|---|---|---|
row | Custom layout for a single row. Render your own controls and call remove() to delete the row. | - |
Extended Component
This is a custom component that extends beyond simple Bootstrap Vue Next wrappers, providing additional functionality specific to Laravel dashboards.