Laravel Livewire Implementar un listado Datatable

Video thumbnail

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.

Creando el Datatable 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 UserList

Y 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.

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')
                                                    &uarr;
                                                @else
                                                    &darr;
                                                @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')
                                   &uarr;
                               @else
                                   &darr;
                               @endif
                           @endif
                           
                       </button>
                   </th>
               @endforeach
               <th class="p-2">
                   Actions
               </th>
           </tr>
       </thead>
***

Filtros sobre el listado

Video thumbnail

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')
                                    &uarr;
                                @else
                                    &darr;
                                @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:

Filtro
Filtro

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

Video thumbnail

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.

El siguiente paso, es aprender a la carga de archivos o imágenes mediante formularios y Drag and Drop en Laravel Livewire.

Acepto recibir anuncios de interes sobre este Blog.

Vamos a crear un sencillo Datatable con Laravel Livewire que es una tabla con campo de ordenación, acciones, campo de búsqueda y paginación, también vamos a crear la estructura para la ordenación de las columnas del listado y un ejemplo con Volt.

| 👤 Andrés Cruz

🇺🇸 In english