Creación de un DataTable Profesional con Laravel Livewire (CRUD Educativo – Parte 1)

Creación de un DataTable Profesional con Laravel Livewire (CRUD Educativo – Parte 1)

Creación de un DataTable Profesional

En este tutorial aprenderás paso a paso cómo construir un DataTable profesional completamente personalizado usando Laravel Livewire. Sin depender de librerías externas como jQuery DataTables, y con funciones avanzadas como:

  • 🔍 Búsqueda en tiempo real
  • 📌 Ordenamiento dinámico
  • 📄 Paginación integrada
  • 🎚 Filtrado avanzado (por estado)
  • ⚡ Carga diferida (lazy loading)

Este DataTable forma parte de un CRUD educativo donde hoy cubriremos la sección de listar. En un próximo artículo complementaremos con crear, editar y eliminar.


📌 1. ¿Qué es un DataTable y por qué hacerlo con Livewire?

Un DataTable es un listado dinámico de registros que permite ordenar, filtrar y paginar información. Aunque existen librerías como jQuery DataTables o Vue.js, construirlo con Livewire tiene muchas ventajas:

  • ✔ Integración perfecta con Laravel
  • ✔ No requiere JavaScript
  • ✔ Se actualiza en tiempo real
  • ✔ Código ordenado y fácil de mantener
  • ✔ Total control del diseño con TailwindCSS

Es ideal para aplicaciones administrativas o dashboards profesionales.


📌 2. Estructura del componente Livewire

El DataTable lo construimos dentro del componente:


readyToLoad = true;
    }

    public function order($sort)
    {
        if ($this->sort == $sort) {
            $this->direction = $this->direction == 'desc' ? 'asc' : 'desc';
        } else {
            $this->sort = $sort;
            $this->direction = 'asc';
        }
    }

    public function render()
    {
        $companyId = auth()->user()->employee->company->id;

        $transportistas = $this->readyToLoad
            ? Transportista::where('company_id', $companyId)
                ->where(function($query) {
                    $query->where('nomrazonsocial', 'like', '%' . $this->search . '%')
                          ->orWhere('numdoc', 'like', '%' . $this->search . '%');
                })
                ->when($this->state, fn($q) => $q->where('state', 1))
                ->orderBy($this->sort, $this->direction)
                ->paginate($this->cant)
            : [];

        return view('livewire.admin.transportista-list', compact('transportistas'));
    }
}
?>

Este código nos permite filtrar, buscar, paginar y ordenar sin recargar la página.


📌 3. Vista del DataTable (HTML + Tailwind + Livewire)

La vista del componente es la encargada de mostrar nuestro DataTable con un diseño limpio y profesional usando TailwindCSS y la potencia reactiva de Livewire. Aquí combinamos:

  • Selector de cantidad de registros por página
  • Buscador en tiempo real
  • Filtro de estado (Activos)
  • Botón para crear un nuevo registro
  • Tabla con ordenamiento dinámico en las columnas
  • Paginación nativa de Laravel

A continuación, el código completo de la vista livewire.admin.transportista-list:


<div wire:init="loadTransportistas">

    <!-- Encabezado -->
    <div class="flex items-center mb-4">
        <h2 class="text-xl font-semibold leading-tight text-gray-600">
            Lista de Transportistas
        </h2>
    </div>

    <div class="container py-6 mx-auto max-w-7xl sm:px-6 lg:px-8">

        <!-- Barra de controles (Mostrar, Buscar, Nuevo, Activos) -->
        <div class="flex flex-wrap items-center px-6 py-4 bg-gray-100 rounded-lg gap-4">

            <!-- Cantidad por página -->
            <div class="flex items-center">
                <span>Mostrar</span>
                <select wire:model="cant"
                        class="ml-3 mr-3 block py-1.5 px-3 text-sm border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500">
                    <option value="10">10</option>
                    <option value="25">25</option>
                    <option value="50">50</option>
                    <option value="100">100</option>
                </select>
                <span>registros</span>
            </div>

            <!-- Buscador -->
            <div class="flex-1 min-w-[200px]">
                <input type="text" wire:model="search"
                    class="w-full px-3 py-2 text-sm border rounded-lg focus:ring-indigo-500 focus:border-indigo-500"
                    placeholder="Buscar por RUC / Razón Social">
            </div>

            <!-- Botón Nuevo -->
            <div>
                <a href="#"
                   class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-orange-600 rounded-lg shadow hover:bg-orange-700">
                    <i class="fa-regular fa-file mr-2"></i> Nuevo
                </a>
            </div>

            <!-- Checkbox Activos -->
            <div class="flex items-center">
                <input type="checkbox" wire:model="state" class="mr-2 rounded border-gray-300">
                <span>Activos</span>
            </div>

        </div>

        <!-- Tabla -->
        <x-table>

            @if (count($transportistas))
                <table class="min-w-full divide-y divide-gray-200">
                    <thead class="bg-gray-50">
                        <tr>
                            <th scope="col"
                                class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase cursor-pointer"
                                wire:click="order('id')">
                                Id
                                @if ($sort == 'id')
                                    @if ($direction == 'asc')
                                        <i class="float-right mt-1 fas fa-sort-alpha-up-alt"></i>
                                    @else
                                        <i class="float-right mt-1 fas fa-sort-alpha-down-alt"></i>
                                    @endif
                                @else
                                    <i class="float-right mt-1 fas fa-sort"></i>
                                @endif
                            </th>

                            <th scope="col"
                                class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
                                Tipo Doc.
                            </th>

                            <th scope="col"
                                class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase cursor-pointer"
                                wire:click="order('numdoc')">
                                Num Doc
                                @if ($sort == 'numdoc')
                                    @if ($direction == 'asc')
                                        <i class="float-right mt-1 fas fa-sort-alpha-up-alt"></i>
                                    @else
                                        <i class="float-right mt-1 fas fa-sort-alpha-down-alt"></i>
                                    @endif
                                @else
                                    <i class="float-right mt-1 fas fa-sort"></i>
                                @endif
                            </th>

                            <th scope="col"
                                class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase cursor-pointer"
                                wire:click="order('nomrazonsocial')">
                                Razón Social
                                @if ($sort == 'nomrazonsocial')
                                    @if ($direction == 'asc')
                                        <i class="float-right mt-1 fas fa-sort-alpha-up-alt"></i>
                                    @else
                                        <i class="float-right mt-1 fas fa-sort-alpha-down-alt"></i>
                                    @endif
                                @else
                                    <i class="float-right mt-1 fas fa-sort"></i>
                                @endif
                            </th>

                            <th scope="col"
                                class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase cursor-pointer"
                                wire:click="order('address')">
                                Dirección
                                @if ($sort == 'address')
                                    @if ($direction == 'asc')
                                        <i class="float-right mt-1 fas fa-sort-alpha-up-alt"></i>
                                    @else
                                        <i class="float-right mt-1 fas fa-sort-alpha-down-alt"></i>
                                    @endif
                                @else
                                    <i class="float-right mt-1 fas fa-sort"></i>
                                @endif
                            </th>

                            <th scope="col"
                                class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase cursor-pointer"
                                wire:click="order('nromtc')">
                                Nro. MTC
                                @if ($sort == 'nromtc')
                                    @if ($direction == 'asc')
                                        <i class="float-right mt-1 fas fa-sort-alpha-up-alt"></i>
                                    @else
                                        <i class="float-right mt-1 fas fa-sort-alpha-down-alt"></i>
                                    @endif
                                @else
                                    <i class="float-right mt-1 fas fa-sort"></i>
                                @endif
                            </th>

                            <th scope="col"
                                class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
                                Estado
                            </th>

                            <th scope="col"
                                class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
                                Acciones
                            </th>
                        </tr>
                    </thead>

                    <tbody class="bg-white divide-y divide-gray-200">
                        @foreach ($transportistas as $transportista)
                            <tr>
                                <td class="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
                                    {{ $transportista->id }}
                                </td>

                                <td class="px-6 py-4 text-sm text-gray-500">
                                    {{ $transportista->tipodocumento->name }}
                                </td>

                                <td class="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
                                    {{ $transportista->numdoc }}
                                </td>

                                <td class="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
                                    {{ $transportista->nomrazonsocial }}
                                </td>

                                <td class="px-6 py-4 text-sm text-gray-500">
                                    {{ $transportista->address }}
                                </td>

                                <td class="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
                                    {{ $transportista->nromtc }}
                                </td>

                                <td class="px-6 py-4 whitespace-nowrap">
                                    @switch($transportista->state)
                                        @case(0)
                                            <span wire:click="activar({{ $transportista->id }})"
                                                class="inline-flex px-2 text-xs font-semibold leading-5 text-red-800 bg-red-100 rounded-full cursor-pointer">
                                                inactivo
                                            </span>
                                        @break

                                        @case(1)
                                            <span wire:click="desactivar({{ $transportista->id }})"
                                                class="inline-flex px-2 text-xs font-semibold leading-5 text-green-800 bg-green-100 rounded-full cursor-pointer">
                                                activo
                                            </span>
                                        @break
                                    @endswitch
                                </td>

                                <td class="px-6 py-4 text-sm font-medium text-right whitespace-nowrap">
                                    <a href="#"
                                       class="btn btn-green">
                                        <i class="fa-solid fa-pen-to-square"></i>
                                    </a>

                                    <a class="btn btn-red ml-2"
                                       wire:click="$emit('deletetransportista', {{ $transportista->id }})">
                                        <i class="fa-solid fa-trash-can"></i>
                                    </a>
                                </td>
                            </tr>
                        @endforeach
                    </tbody>
                </table>

                @if ($transportistas->hasPages())
                    <div class="px-6 py-4">
                        {{ $transportistas->links() }}
                    </div>
                @endif
            @endif

        </x-table>

    </div>

</div>

📌 4. Funciones clave explicadas

Función Livewire ¿Qué hace?
wire:model="search" Actualiza la búsqueda en tiempo real sin recargar la página.
wire:click="order('campo')" Ordena asc/desc como un DataTable profesional.
wire:init="loadTransportistas" Evita cargar datos hasta que el componente esté listo (mejora el rendimiento).
$this->paginate() Añade paginación automática sin JavaScript.

📌 5. Ventajas de crear tu propio DataTable sin plugins

  • ✔ No dependes de librerías externas como DataTables.js
  • ✔ Menos peso, más rendimiento
  • ✔ Mejor integración con el backend
  • ✔ Total libertad de diseño con Tailwind
  • ✔ Lógica limpia, escalable y mantenible
  • ✔ Ideal para CRUD profesionales

🚀 Conclusión

Crear un DataTable profesional con Livewire es una de las mejores formas de construir paneles administrativos modernos. En este artículo hemos desarrollado un listado completo con:

  • ✔ Búsqueda
  • ✔ Filtros
  • ✔ Paginación
  • ✔ Ordenamiento
  • ✔ Activar / desactivar

En el siguiente artículo avanzaremos con la creación de:

  • 🟢 Crear nuevo transportista
  • 🟡 Editar
  • 🔴 Eliminar con confirmación

De esta forma construiremos el CRUD completo usando Livewire.


📞 Contáctanos

En Ticom Software somos especialistas en Laravel, PHP y Odoo ERP. Ofrecemos desarrollo de aplicaciones web, capacitaciones y soporte técnico.

  • 🔗 Web: https://www.ticomsoftware.com
  • 📧 Email: info@ticomsoftware.com
  • 📱 WhatsApp: +51 996 929 478
  • 🔵 Facebook: https://www.facebook.com/ticomperuscrl/
  • 📸 Instagram: https://www.instagram.com/ticom.peru/
  • 🎵 TikTok: https://www.tiktok.com/@ticomscrl


Etiquetas :
F