Índice de contenido
- Creando el Datatable Nativo en Laravel Livewire
- Definir Modelo
- Definir trait para los campos de búsqueda mediante queryString
- protected $queryString en Laravel Livewire
- ¿Para qué sirve?
- Procesar data para el listado, función de render
- Código completo del proyecto
- Código de la vista
- Ordenación
- Filtros sobre el listado
- Los filtros en Laravel Livewire
- Listado paginado con Volt
- Extra wire:sort en Livewire 4
- ¿Qué es wire:sort?
- Ejemplo de uso básico
- Características avanzadas de wire:sort
- Ejemplo completo: Datatable ordenable con Drag and Drop en Livewire 4
- 1. Preparación de los Datos
- 2. Uso de Directivas en la Vista
- 3. El Algoritmo de Reordenación
- 4. Aplicación en Tablas (DataTables)
Una tabla de tipo datatable, es el término que nosotros empleamos para indicar una tabla en HTML básico con los datos con las operaciones tipo CRUD para cada uno de estos datos; esta tabla; es una tabla que nos permite definir un conjunto de opciones útiles para nuestro usuario como paginación, campos de búsqueda, ordenación de columnas y por supuesto, opciones para editar, eliminar y todas las que necesites; en esta entrada solamente vamos a crear la tabla, con las opciones correspondientes para la misma empleando para nuestra interfaz, tailwind.css, aunque puedes emplear cualquier otra cosa como CSS nativo o incluso las tablas que tenemos para Bootstrap.
En este punto, que ya sabemos como emplear la propiedades de componentes en Livewire, que serán una pieza clave para convertir nuestra tabla o listado en un datatable.
Un DataTable puede almacenar datos de cualquier tipo, incluyendo textos, números, fechas, imágenes entre otros tipos. Se utiliza comúnmente en aplicaciones web para mostrar datos en una tabla de forma ordenada y estructurada. Por ejemplo, si se tiene información de un listado de publicaciones, en este supuesto listado pueden aparecer las opciones para eliminar, crear, editar, ver el detalle, filtrar los post.
A la final, un Datatable es una tabla con esteroides en las cuales defines multiples opciones de gestión, filtro y ordenación.
En Laravel Livewire, gracias a todo el comportamiento reactivo y la comunicación con el servidor de manera directa desde el cliente, podemos crear este tipo de tablas muy fácilmente de manera nativa.
Creando el Datatable Nativo en Laravel Livewire
Como mencionamos antes, hay cierto componente de gestión en las tablas, por lo tanto, debemos de definir el modelo, la vista y el componente.
Definir Modelo
Primero es lo primero, y necesitamos una fuente de datos, nuestro modelo de usuario, que es el que nos viene de gratis con Laravel Livewire, nos viene bastante bien, pero tú puedes emplear cualquier otra; en nuestro modelo de toda la vida para los usuarios con los traits que tenemos por defecto:
class User extends Authenticatable #implements MustVerifyEmail
{
use HasApiTokens;
use HasFactory;
use HasProfilePhoto;
use HasTeams;
use Notifiable;
use TwoFactorAuthenticatable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password'
];
}Definir trait para los campos de búsqueda mediante queryString
Ahora vamos a nuestro componente, para eso vamos a emplear un componente de Livewire que podemos crear con:
php artisan make:livewire UserListY recuerda que con esto, se crean 3 elementos, nuestro Componente del lado del servidor y nuestra vista; ahora con el componente creado, tenemos que definir algunas características para poder emplear la búsqueda; empleamos la propiedad llamada queryString, para indicar que propiedades vamos a emplear para la búsqueda; esto es útil ya que si nuestro usuario refresca la página, los datos que están escribiendo el mismo quedan guardados en la URL de la aplicación y la búsqueda se mantendría:
use WithPagination;
protected $queryString = ['name'];
public $name = "";
public $sortColumn = "id";
public $sortDirection = "asc";
public $columns = [
'id',
'name',
'email'
];
public function sort($column)
{
$this->sortColumn = $column;
$this->sortDirection = $this->sortDirection == 'asc' ? 'desc' : 'asc';
}Por lo demás, definimos algunas propiedades más para indicar las columnas que vamos a emplear, el nombre de las mismas y también una función y propiedades que nos permitirán realizar la búsqueda por columnas, y de una el tipo de ordenación, si ascendente o descendente.
protected $queryString en Laravel Livewire
protected $queryString es una propiedad fundamental en el ecosistema de Laravel, específicamente cuando trabajas con Livewire. Su función principal es sincronizar el estado de los componentes con la URL del navegador.
Aquí tienes una explicación básica para entender cómo y por qué se usa:
¿Para qué sirve?
Imagina que tienes una tabla con un buscador o filtros. Si el usuario filtra por "Zapatos" y luego refresca la página, normalmente el filtro se borraría.
Al usar $queryString, Livewire guarda ese valor en la URL (ejemplo: tuweb.com/productos?search=Zapatos). Esto permite que:
- Al refrescar, el filtro se mantenga.
- El usuario pueda copiar y compartir la URL con el filtro ya aplicado.
- El botón de "Atrás" del navegador funcione correctamente con los cambios de estado.
Procesar data para el listado, función de render
Ahora, el corazón de nuestro componente, en el cual armamos el query o la consulta a la base de datos, así como mapear las propiedades que queramos emplear para filtrar nuestra data, que en este caso es el nombre/name; por lo demás empleamos la paginación y devolvemos a la vista que queremos emplear:
public function render()
{
$users = User::orderBy($this->sortColumn, $this->sortDirection);
if ($this->name) {
$users->where('name', 'like', '%' . $this->name . '%')
->orWhere('email', 'like', '%' . $this->name . '%');
}
$users = $users->paginate(10);
return view('livewire.users-list', ['users' => $users]); //->layout('layouts.app');
}Código completo del proyecto
Finalmente, por aquí puedes ver el código fuente del componente usado anteriormente, en la cual, tenemos las columnas, así como las columnas para indicar el filtro, ordenación, en la función de render, indicamos la vista así como aplicar los filtros correspondientes, recuerda que cada una de esas propiedades usadas en a la función de render, es utilizada a nivel de la vista como modelos en los campos, gracias a la comunicación entre el backend con el fronend, la comunicación se realiza de manera directa y este es el secreto y punto más importante de este proyecto:
namespace App\Http\Livewire;
use App\Models\User;
//use Illuminate\View\Component;
use Livewire\Component;
use Livewire\WithPagination;
class UsersList extends Component
{
use WithPagination;
//public $users;
public $columns = [
'id',
'name',
'email'
];
public $name = "";
public $sortColumn = "id";
public $sortDirection = "asc";
protected $queryString = ['name'];
public function render()
{
$users = User::orderBy($this->sortColumn, $this->sortDirection);
if ($this->name) {
$users->where('name', 'like', '%' . $this->name . '%')
->orWhere('email', 'like', '%' . $this->name . '%');
}
$users = $users->paginate(10);
return view('livewire.users-list', ['users' => $users]); //->layout('layouts.app');
}
public function sort($column)
{
$this->sortColumn = $column;
$this->sortDirection = $this->sortDirection == 'asc' ? 'desc' : 'asc';
}
public function cleanFilter()
{
$this->name = "";
}
public function delete(User $user)
{
$user->delete();
}
}Código de la vista
La vista, es realmente sencillo; como te comentamos, estamos empleando Tailwind, tenemos un bloque de inputs referenciando las propiedades del modelo anterior mediante el wire:model, que es para definir los campos del formulario para filtrar y luego tenemos un bloque para la tabla, que al ya tener toda la lógica implementada en el componente desde Laravel, nos dedicamos a pintar la data y columnas sin ninguna condición:
<div class="flex mt-1 ">
<x-jet-input wire:model="name" class="block w-full" />
<x-jet-secondary-button class="ml-2" wire:click="cleanFilter">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
</x-jet-secondary-button>
</div>
<table class="table-auto w-full">
<thead>
<tr>
@foreach ($columns as $c)
<th class="p-3" wire:click="sort('{{ $c }}')">
<button>
{{ $c }}
@if ($sortColumn == $c)
@if ($sortDirection == 'asc')
↑
@else
↓
@endif
@endif
</button>
</th>
@endforeach
<th class="p-3">Acciones</th>
</tr>
</thead>
<tbody>
@forelse ($users as $u)
<tr>
<td class="border p-3">{{ $u->id }}</td>
<td class="border p-3">{{ $u->name }}</td>
<td class="border p-3">{{ $u->email }}</td>
<td class="border p-3 flex justify-center">
<x-a class="p-1 bg-blue-600 mr-1" href="{{ route('user.edit', $u) }}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</x-a>
<x-jet-danger-button class="p-sm-button" wire:click="delete({{ $u->id }})">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</x-jet-danger-button>
</td>
</tr>
@empty
<tr>
<td colspan="3">
<p>No hay registros</p>
</td>
</tr>
@endforelse
</tbody>
</table>
<br>
{{ $users->links() }}Puntos importantes es notar el llamado a las funciones mediante los eventos de click que nos permiten llamar a funciones del componente desde la vista como la de delete o la de ordenación/sort para realizar la ordenación.
Ordenación
Vamos a trabajar en ordenación de columnas de manera individual y ascendente/descendente en el listado.
El esquema que vamos a seguir, para poder ordenar fácilmente todas las columnas de un listado, es definiendo las columnas que vamos a mostrar en el listado a nivel de una propiedad en el componente de Livewire; con esto, podremos fácilmente indicar los eventos clicks en base a la estructura propuesta en este libro:
app/Http/Livewire/Dashboard/Post/Index.php
class Index extends Component
{
use WithPagination;
// ***
// order
public $sortColumn = 'id';
public $sortDirection = 'desc';
public $columns = [
'id' => "Id",
'title' => "Title",
'date' => "Date ",
'description' => "Description",
'posted' => "Posted",
'type' => "Type",
'category_id' => "Category",
];
public function render()
{
//$this->confirmingDeletePost = true;
$posts = Post::orderBy($this->sortColumn,$this->sortDirection);
if ($this->search)
// ***
}
// order
public function sort($column)
{
$this->sortColumn = $column;
$this->sortDirection = $this->sortDirection == 'asc' ? 'desc' : 'asc';
}Explicación del código anterior
La columna de $columns definimos las columnas que vamos a mostrar en el listado, como; por ejemplo:
'id' => "Id",Como key del array, indicamos el nombre de la columna en la base de datos (el nombre de la propiedad en el modelo).
Como valor, es el label o texto que vamos a colocar como cabecera en la tabla.
Las propiedades de $sortColumn y $sortDirection definen la columna que vamos a ordenar en un momento dado y el tipo (ascendente o descendente), respectivamente.
Los cuales actualizamos a nivel del método llamada sort() la cual permite actualizar la columna de ordenación y el tipo; claro está, esta función será la que es consumida desde la vista mediante un wire:click.
En la vista:
resources/views/livewire/dashboard/post/index.blade.php
***
<table class="table w-full border">
<thead class="text-left bg-gray-100 ">
<tr class="border-b">
@foreach ($columns as $key => $c)
<th>
<button wire:click="sort('{{ $key }}')">
{{ $c }}
@if ($key == $sortColumn)
@if ($sortDirection == 'asc')
↑
@else
↓
@endif
@endif
</button>
</th>
@endforeach
<th class="p-2">
Actions
</th>
</tr>
</thead>
***Filtros sobre el listado
Los filtros en el desarrollo web se utilizan para clasificar o limitar datos en algún listado, son muy utilizados en los módulos administrativos para limitar los datos de un listado Por ejemplo, un filtro puede utilizarse para mostrar solo aquellos elementos de un conjunto de datos que cumplan con ciertos criterios, como una fecha específica o una categoría en particular; realmente podemos colocar cualquier tipo de campo; en la práctica, los filtros no son más que formularios HTML con sus respectivos campos en HTML; en el cual, usualmente se envían peticiones vía GET y no POST, ya que, se busca es clasificar datos y no cambiar ningún tipo de dato. Veamos como usarlos en Livewire.
Los filtros en Laravel Livewire
En Laravel Livewire, podemos aplicar filtros fácilmente para búsqueda en nuestros listados, gracias a que existe una especie de sincronización o lazo entre el lado del cliente con el servidor, a lo que a sus propiedades se refiere, este tipo de operación resultan muy fáciles de realizar.
Estos listados pueden ser cualquier cosa como una tabla o lista estructuradas en base a DIVs.
En este artículo, vamos a partir de un modelo de publicaciones como el siguiente:
class Post extends Model
{
use HasFactory;
protected $fillable=['title', 'slug','date','image','text','description','posted','type', 'category_id'];
protected $casts =[
'date' => 'datetime'
];
public function category()
{
return $this->belongsTo(Category::class);
}
}y el listado típico usando una paginación:
<table class="table w-full border">
<thead class="text-left bg-gray-100 ">
<tr class="border-b">
@foreach ($columns as $key => $c)
<th>
<button wire:click="sort('{{ $key }}')">
{{ $c }}
@if ($key == $sortColumn)
@if ($sortDirection == 'asc')
↑
@else
↓
@endif
@endif
</button>
</th>
@endforeach
<th class="p-2">
Acciones
</th>
</tr>
</thead>
<tbody>
@foreach ($posts as $p)
<tr class="border-b">
<td class="p-2">
{{ $p->id }}
</td>
<td class="p-2">
{{ str($p->title)->substr(0, 15) }}
</td>
<td class="p-2">
{{ $p->date }}
</td>
<td class="p-2">
<textarea class="w-48">
{{ $p->description }}
</textarea>
</td>
<td class="p-2">
{{ $p->posted }}
</td>
<td class="p-2">
{{ $p->type }}
</td>
<td class="p-2">
{{ $p->category->title }}
</td>
<td class="p-2">
<x-jet-nav-link href="{{ route('d-post-edit', $p) }}" class="mr-2">Editar
</x-jet-nav-link>
<x-jet-danger-button {{-- onclick="confirm('Seguro que deseas eliminar el registro seleccionado?') || event.stopImmediatePropagation()" --}}
wire:click="seletedPostToDelete({{ $p }})">
Eliminar
</x-jet-danger-button>
</td>
</tr>
@endforeach
</tbody>
</table>
<br>
{{ $posts->links() }}Para filtrar datos en la clase componente, usamos condiciones where, en este caso sobre el tipo:
if ($this->type) {
$posts->where('type', $this->type);
}La categoría:
if ($this->category_id) {
$posts->where('category_id', $this->category_id);
}Y si esta posteado o no:
if ($this->posted) {
$posts->where('posted', $this->posted);
}Todas son estructuras modelos existentes y que por supuesto, puedes personalizar.
Así que, nuestro componente para obtener los post filtrados:
La clase componente:
//***
// filters
public $type;
public $category_id;
public $posted;
//***
public function render()
{
$posts = Post::where("id", ">=", 1);
if ($this->type) {
$posts->where('type', $this->type);
}
if ($this->category_id) {
$posts->where('category_id', $this->category_id);
}
if ($this->posted) {
$posts->where('posted', $this->posted);
}
$categories = Category::pluck("title", "id");
$posts = $posts->paginate(10);
return view('livewire.dashboard.post.index', compact('posts', 'categories'));
}
//***Importante notar que:
- Tenemos una propiedad por filtro.
- Se compone la consulta a la base de datos para obtener los posts en varios pasos y se verifica si las propiedades filtros tienen un valor establecido para aplicar en la consulta.
En la vista, colocamos nuestros filtros en base a la estructura que tengamos:
<div class="flex gap-2 mb-2">
<select class="block w-full" wire:model="posted">
<option value="">{{__("Posted")}}</option>
<option value="not">No</option>
<option value="yes">Si</option>
</select>
<select class="block w-full" wire:model="type">
<option value="">{{__("Type")}}</option>
<option value="adverd">adverd</option>
<option value="post">post</option>
<option value="course">course</option>
<option value="movie">movie</option>
</select>
<select class="block w-full" wire:model="category_id">
<option value="">{{__("Category")}}</option>
@foreach ($categories as $i => $c)
<option value="{{ $i }}">{{ $c }}</option>
@endforeach
</select>
</div>
<table class="table w-full border">
//***Ya en este punto, tendríamos un listado paginado, es interesante darse cuenta que podemos usar las propiedades en Livewire en conjunto con los wire:model no solamente para la gestión de registros, si no también para otros propósitos como en este caso, sería aplicar un filtro.
Al final, tendremos:

Y con esto, tenemos un sencillo filtro, recuerda que puedes colocar más campos siguiente la misma organización que la presentada en esta entrada; los filtros son unos de los muchos temas que tratamos en el curso y libro sobre Laravel Livewire.
Listado paginado con Volt
Es muy fácil convertir un componente de Livewire clásico, es decir, una clase y vista a un componente de volt, partiremos de:
resources/views/livewire/dashboard/category/index.blade.php
<div>
<x-action-message on="deleted">
<div class="box-action-message">
{{ __("Category delete success") }}
</div>
</x-action-message>
<flux:heading>{{ __('Category List') }}</flux:heading>
<flux:text class="mt-2">Lorem ipsum dolor sit amet consectetur adipisicing.</flux:text>
<div class="separation"></div>
<flux:button class="ml-1 mb-3" variant='primary' icon="plus" href="{{ route('d-category-create') }}">
{{ __('Create') }}
</flux:button>
<!-- <flux:modal.trigger name="delete-category">
<flux:button>Delete</flux:button>
</flux:modal.trigger> -->
<flux:modal name="delete-category">
<div class="m-1">
<flux:heading>{{ __('Delete Category') }}</flux:heading>
<flux:text class="mt-2">{{ __('Are you sure you want to delete this category?') }}</flux:text>
<div class="flex flex-row-reverse">
<flux:button class="mt-4" variant='danger' icon="trash" wire:click="delete()">
{{ __('Delete') }}
</flux:button>
</div>
</div>
</flux:modal>
<div class="overflow-x-auto shadow-md rounded-lg">
<table class="table w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="rounded-lg text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th>
Id
</th>
<th>
Title
</th>
<th>
Actions
</th>
</tr>
</thead>
<tbody>
@foreach ($categories as $c)
<tr
class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700 border-gray-200">
<td>
{{ $c->id }}
</td>
<td>
{{ $c->title }}
</td>
<td>
<a href="{{ route('d-category-edit', $c) }}">Edit</a>
<flux:modal.trigger wire:click="selectCategodyToDelete({{ $c }})" name="delete-category">
<flux:button class="ml-3" variant='danger' size="xs">{{ __('Delete') }}</flux:button>
</flux:modal.trigger>
<!-- <flux:button class="ml-3" variant='danger' size="xs" wire:click="delete({{ $c }})">
{{ __('Delete') }}
</flux:button> -->
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<br>
{{ $categories->links() }}
</div>app/Livewire/Dashboard/Category/Index.php
<?php
namespace App\Livewire\Dashboard\Category;
use Livewire\Component;
use Livewire\WithPagination;
use App\Models\Category;
use Flux\Flux;
class Index extends Component
{
use WithPagination;
public $categoryToDelete;
public function render()
{
$categories = Category::paginate(10);
return view('livewire.dashboard.category.index', compact('categories'));
}
function selectCategodyToDelete(Category $category){
$this->categoryToDelete = $category;
}
function delete(){
$this->dispatch("deleted");
Flux::modal("delete-category")->close();
$this->categoryToDelete->delete();
}
}Y un componente de Volt es exactamente lo mismo:
$ php artisan make:volt Volt/Dashboard/Category/Index<?php
use Livewire\Volt\Component;
use Illuminate\Support\Facades\Storage;
use Livewire\WithFileUploads;
use App\Models\Category;
new class extends Component {
use WithFileUploads;
public $title;
public $slug;
public $text;
public $image;
protected $rules = [
'title' => "required|min:2|max:255",
'text' => "nullable",
'image' => "nullable|image|max:1024",
];
public $category;
public function mount(?int $id = null)
{
if ($id != null) {
$this->category = Category::findOrFail($id);
$this->title = $this->category->title;
$this->text = $this->category->text;
}
}
function submit()
{
// validate
$this->validate();
if ($this->category) {
$this->category->update([
'title' => $this->title,
'text' => $this->text,
]);
$this->dispatch("updated");
} else {
$this->category = Category::create([
'title' => $this->title,
'slug' => str($this->title)->slug(),
'text' => $this->text,
]);
$this->dispatch("created");
}
// upload
if ($this->image) {
// delete old img
if ($this->category->image) {
Storage::disk('public_upload')
->delete('images/category/' . $this->category->image);
}
$imageName = $this->category->slug . '.' . $this->image->getClientOriginalExtension();
$this->image->storeAs('images/category', $imageName, 'public_upload');
$this->category->update([
'image' => $imageName
]);
}
}
}; ?>
<div>
<x-action-message on="created">
<div class="box-action-message">
{{ __('Created category success') }}
</div>
</x-action-message>
<x-action-message on="updated">
<div class="box-action-message">
{{ __('Updated category success') }}
</div>
</x-action-message>
<flux:heading>
@if ($category)
{{ __('Category edit: ') }} <span class="font-bold">{{ $category->title }}</span>
@else
{{ __('Category create') }}
@endif
</flux:heading>
<flux:text class="mt-2">Lorem ipsum dolor sit amet consectetur adipisicing.</flux:text>
<div class="separation"></div>
<form wire:submit.prevent="submit" class="flex flex-col gap-4">
<!-- <input type="text" wire:model="title">
<textarea wire:model="text"></textarea>
<button type="submit" wire:click="submit">Send</button> -->
{{-- <input type="text" wire:model="title"> --}}
{{-- @error('title')
{{ $message }}
@enderror --}}
<flux:input wire:model="title" :label="__('Title')" />
<flux:textarea wire:model="text" :label="__('Text')" />
<flux:input wire:model="image" type='file' :label="__('Image')" />
{{-- @error('text')
{{ $message }}
@enderror --}}
<div>
<flux:button variant="primary" type="submit">
{{ __('Save') }}
</flux:button>
</div>
</form>
@if ($category && $category->image)
<img class="w-40 my-3" src="{{ $category->getImageUrl() }}" />
@endif
</div>Creamos la ruta:
// demo volt
Volt::route('volt', 'volt.dashboard.category.index')->name('volt-d-category-index');Como puedes ver, removimos el método de render y el caso de la paginación es especial, debemos de usar el método de with() y retornar los datos a la vista.
Extra wire:sort en Livewire 4
A diferencia de las versiones anteriores donde necesitabas plugins externos o Alpine.js, ahora Livewire tiene una directiva oficial y simplificada. Aquí tienes la explicación básica:
¿Qué es wire:sort?
Es una directiva diseñada para hacer que cualquier lista sea ordenable mediante arrastre, encargándose automáticamente de las animaciones y la lógica del navegador.
Ejemplo de uso básico
En tu archivo Blade, envuelves tu lista en un contenedor con wire:sort y etiquetas cada elemento con wire:sort:item:
<ul wire:sort="reorder">
@foreach ($tasks as $task)
<li wire:sort:item="{{ $task->id }}" wire:key="{{ $task->id }}">
{{ $task->title }}
</li>
@endforeach
</ul>En tu Componente PHP, defines el método que recibirá el cambio:
public function reorder($id, $position)
{
// $id: El identificador del elemento que se movió.
// $position: La nueva posición (empezando desde 0).
$task = Task::find($id);
$task->update(['position' => $position]);
// Opcional: Reordenar el resto de la colección en la DB
}Características avanzadas de wire:sort
wire:sort:handle: Si no quieres que todo el elemento sea "arrastrable", puedes definir un icono o botón específico como manija:
<li wire:sort:item="{{ $task->id }}">
<span wire:sort:handle>☰</span> {{ $task->title }}
</li>wire:sort:ignore: Para elementos dentro de la lista que no deben activar el arrastre (como botones de borrar o inputs):
- <button wire:sort:ignore>Eliminar</button>
wire:sort:group: Para mover elementos entre diferentes listas (estilo Kanban). - Usas wire:sort:group="nombre_del_grupo" en los contenedores.
- El método PHP recibirá un tercer parámetro: $groupId.
Ejemplo completo: Datatable ordenable con Drag and Drop en Livewire 4
Esta es una implementación de wire:sort, que nos permite reordenar elementos mediante Drag and Drop, una característica nueva a partir de Livewire 4.
1. Preparación de los Datos
Partimos de un listado de tareas (Todo List) muy sencillo. Para que funcione, necesitamos un título y, obligatoriamente, un campo para el orden (llámalo position, sort, o como prefieras) para guardar la ubicación en la base de datos.
En el componente, al montarlo (mount), obtenemos todas las tareas. Es importantísimo que estas vengan ordenadas mediante el campo correspondiente para que la vista inicial sea correcta.
app\Models\TodoList.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TodoList extends Model
{
use HasFactory;
protected $fillable = [
'title',
'order',
];
}2. Uso de Directivas en la Vista
Para emplear la nueva característica de Wire Sore, solo tenemos que definir un par de campos en nuestro HTML:
- wire:sortable: Esta directiva se coloca en el contenedor principal e indica el nombre del método que se encargará de procesar el reordenamiento.
- wire:sortable.item: Se asigna al elemento que estamos moviendo, pasándole usualmente el ID de la tarea.
- wire:sortable.handle: (Opcional) Si quieres un icono específico para arrastrar.
El evento se dispara al terminar la reordenación (on drag end), llamando al método definido y pasando el ID del ítem junto con su nueva posición.
3. El Algoritmo de Reordenación
Actualizar la posición no es tan trivial. Si simplemente le asignas la nueva posición a una tarea, podrías terminar con dos tareas con el mismo número de orden (por ejemplo, dos tareas en la posición "2"). Por lo tanto, debes actualizar las demás.
Te propongo el siguiente algoritmo para simplificar el proceso:
- Filtrar: Obtenemos todas las tareas de la base de datos excepto la que acabamos de mover.
- Insertar: Tomamos la tarea actual y la insertamos manualmente en la nueva posición dentro de la colección obtenida en el paso anterior.
- Reindexar: Mediante un ciclo foreach, iteramos esta nueva lista y asignamos a cada tarea una posición basada simplemente en su índice dentro del array.
- Persistir: Guardamos los cambios en la base de datos y recargamos la lista para que la interfaz se actualice.
resources\views\components\⚡mysort.blade.php
<?php
use Livewire\Component;
use App\Models\TodoList;
new class extends Component {
public $lists;
public function mount()
{
$this->lists = TodoList::orderBy('order')->get();
}
public function handleSort($id, $position)
{
// 1. Obtenemos todos los elementos ordenados, excluyendo el que se movió
$items = TodoList::where('id', '!=', $id)->orderBy('order')->get();
// 2. Buscamos el elemento que se movió
$movedItem = TodoList::find($id);
// 3. Lo insertamos en la nueva posición dentro de la colección (splice)
// Nota: $position suele venir en base 0 desde el frontend
$items->splice($position, 0, [$movedItem]);
// 4. Actualizamos la base de datos
foreach ($items as $index => $item) {
$item->update(['order' => $index]);
}
// 5. Refrescamos la propiedad para que la vista se actualice
$this->lists = TodoList::orderBy('order')->get();
}
};
?>
<div>
<ul wire:sort="handleSort">
@foreach ($lists as $l)
<li class="border p-4 mb-1" wire:key="{{ $l->id }}" wire:sort:item="{{ $l->id }}">
<span class="rounded-full px-2 py-1 bg-purple-500 text-sm">{{ $l->order }}</span> -
{{ $l->title }}
</li>
@endforeach
</ul>
</div>4. Aplicación en Tablas (DataTables)
Si estás trabajando con una tabla, la lógica es la misma. El wire:sort va justamente encima del @foreach (en el <tbody>) y el sortable.item al mismo nivel que la etiqueta <tr>. Esto es ideal para completar el Data Table que estamos construyendo en esta entrada.
resources\views\components\⚡mysort.blade.php
<div>
<table class="table">
<thead>
<tr>
<th class="px-6 py-4 ">ID</th>
<th class="px-6 py-4 ">Title</th>
<th class="px-6 py-4 ">Order</th>
</tr>
</thead>
<tbody wire:sort="handleSort">
@foreach ($lists as $l)
<tr wire:key="{{ $l->id }}" wire:sort:item="{{ $l->id }}">
<td class="px-6 py-4 ">{{ $l->id }}</td>
<td class="px-6 py-4 ">{{ $l->title }}</td>
<td class="px-6 py-4 ">{{ $l->order }}</td>
</tr>
@endforeach
<tbody>
</table>
</div>
De tal forma, que puedes adaptar el wire:sort fácilmente para poder ordenar las filas de tu datatable fácilmente.
El siguiente paso, es aprender a la carga de archivos o imágenes mediante formularios y Drag and Drop en Laravel Livewire.