DXRepeater

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.

Order reference
Line items
Description
Qty
Unit price
£
{
  "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.

Tags
KeyValue

Props

Name Click to sort ascendingType Required Click to sort ascendingDefault Description
formUseFormReturn<any>Yes-Form instance owning the repeater array.
fieldFieldDefinitionYes-The repeater field definition. Provides sub-fields (field.fields), addLabel, minItems, maxItems, softDeleteKey, repeaterLayout, showRowIndex.
keyPathstringNofield.keyDot path into form.data for the array.
errorKeystringNokeyPathBase error key for nested validation.
modelanyNo-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 ascendingDescription Scoped Props
rowCustom 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.