K
KoreUI

Repeater

Form

Grupos 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

[{"product":"Licencia anual","qty":"2","price":"120"},{"product":"Soporte premium","qty":"1","price":"450"}]

Equipo — select y máximo de 4 filas

[{"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.

Patrón host-driven, por fila
<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
Cargando