Repeater
FormGrupos de campos repetibles: el usuario añade, elimina y reordena filas de un mini-formulario —ítems de factura, variantes de producto, miembros de equipo—. Generaliza el key-value a un número arbitrario de campos definidos por un schema.
Requiere Livewire. El repeater sincroniza su estado con $wire.$set desde un plugin Alpine (KoreRepeater), y reorderable usa x-sort —el plugin de Alpine que Livewire trae embebido—, así que la vista necesita un componente Livewire con una propiedad pública recibiendo el wire:model.
En vivo
El primero es reordenable y sincroniza un array de objetos con tres campos por fila. El segundo trae un campo select y un máximo de 4 filas. El JSON debajo de cada uno muestra el estado real.
Ítems de factura
Sin filas. Añade la primera.
[{"product":"Licencia anual","qty":"2","price":"120"},{"product":"Soporte premium","qty":"1","price":"450"}]
Equipo — select y máximo de 4 filas
Sin filas. Añade la primera.
[{"name":"Ana Garc\u00eda","role":"admin"},{"name":"Carlos L\u00f3pez","role":"editor"}]
{{-- Ítems de factura --}}
<x-kore::repeater
wire:model="lineItems"
label="Conceptos"
reorderable
:fields="[
['key' => 'product', 'label' => 'Producto', 'type' => 'text'],
['key' => 'qty', 'label' => 'Cantidad', 'type' => 'number'],
['key' => 'price', 'label' => 'Precio', 'type' => 'number'],
]"
/>
{{-- Con select y límite --}}
<x-kore::repeater wire:model="teamMembers" label="Equipo" :max="4" :fields="[
['key' => 'name', 'label' => 'Nombre', 'type' => 'text'],
['key' => 'role', 'label' => 'Rol', 'type' => 'select', 'options' => ['admin' => 'Admin', 'editor' => 'Editor', 'viewer' => 'Viewer']],
]" />{{-- Ítems de factura --}}
<x-kore::repeater
wire:model="lineItems"
label="Conceptos"
reorderable
:fields="[
['key' => 'product', 'label' => 'Producto', 'type' => 'text'],
['key' => 'qty', 'label' => 'Cantidad', 'type' => 'number'],
['key' => 'price', 'label' => 'Precio', 'type' => 'number'],
]"
/>
{{-- Con select y límite --}}
<x-kore::repeater wire:model="teamMembers" label="Equipo" :max="4" :fields="[
['key' => 'name', 'label' => 'Nombre', 'type' => 'text'],
['key' => 'role', 'label' => 'Rol', 'type' => 'select', 'options' => ['admin' => 'Admin', 'editor' => 'Editor', 'viewer' => 'Viewer']],
]" />Schema de campos
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
key
|
string | — | Nombre de la propiedad en cada objeto de fila (requerido) |
label
|
string|null | null | Etiqueta encima del input |
type
|
string | text | text, number o select |
placeholder
|
string|null | null | Placeholder del input |
options
|
array|null | null | Para type «select»: mapa de valor a etiqueta, por ejemplo admin => Admin |
Cada entrada de fields define una columna de la fila. Las clases de rejilla (sm:grid-cols-N) se resuelven con match() a strings literales, para que Tailwind v4 las capture del source.
Alcance y schemas complejos
El motor Alpine cubre filas de inputs simples: text, number y select.
Para schemas complejos —upload por fila, select async con datos de servidor, validación por fila— usa el patrón host-driven con Livewire, que gestiona cada fila como un componente de servidor en lugar de delegarla al motor Alpine.
<div wire:sort="reorderRows">
@foreach($rows as $i => $row)
<div wire:key="row-{{ $i }}" wire:sort:item="{{ $i }}" class="...">
<x-kore::input wire:model="rows.{{ $i }}.name" label="Nombre" />
<x-kore::select wire:model="rows.{{ $i }}.country" :options="$countries" searchable />
<x-kore::button wire:click="removeRow({{ $i }})" icon="x" variant="ghost" />
</div>
@endforeach
</div>
<x-kore::button wire:click="addRow" label="Añadir" icon="plus" variant="outline" /><div wire:sort="reorderRows">
@foreach($rows as $i => $row)
<div wire:key="row-{{ $i }}" wire:sort:item="{{ $i }}" class="...">
<x-kore::input wire:model="rows.{{ $i }}.name" label="Nombre" />
<x-kore::select wire:model="rows.{{ $i }}.country" :options="$countries" searchable />
<x-kore::button wire:click="removeRow({{ $i }})" icon="x" variant="ghost" />
</div>
@endforeach
</div>
<x-kore::button wire:click="addRow" label="Añadir" icon="plus" variant="outline" />
Misma base de array que kore::key-value y kore::tag-input: estado en Alpine, sincronizado con $wire.$set(model, rows). El contenedor va con wire:ignore, y cada fila se rastrea con :key="index" dentro del x-for.
Props
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
fields
|
array | [] | Schema de campos por fila |
label
|
string|null | null | Etiqueta del campo |
hint
|
string|null | null | Texto de ayuda |
name
|
string|null | null | Nombre (se deriva de wire:model si se omite) |
error
|
string|null | null | Mensaje de error manual |
size
|
string | config(md) | Tamaño: sm, md, lg |
required
|
bool | false | Marca el campo como obligatorio |
disabled
|
bool | false | Desactiva el editor |
readonly
|
bool | false | Deja las filas a la vista sin editar, reordenar ni borrar, y sigue enviando el valor |
showError
|
bool | true | Auto-detectar errores del $errors bag |
min
|
int | 0 | Filas mínimas, no se puede bajar de aquí |
max
|
int|null | null | Filas máximas |
addLabel
|
string | Añadir | Texto del botón de añadir |
reorderable
|
bool | false | Reordenar arrastrando, usa x-sort |
default
|
array | [] | Filas iniciales si el modelo llega vacío |