Skip to main content

Layout primitives

Data Table code

Copy the implementation or inspect every file that belongs to this component unit.

resources/views/components/ui/data-table.blade.php

@props([
    'title' => null,
    'description' => null,
    'search' => false,
    'searchName' => 'search',
    'searchValue' => null,
    'searchPlaceholder' => 'Search...',
    'action' => null,
    'method' => 'GET',
    'striped' => false,
    'sticky' => false,
    'density' => 'comfortable',
    'selectable' => false,
    'selectionIds' => [],
    'hasRows' => true,
    'emptyIcon' => 'table',
    'emptyTitle' => 'No records found',
    'emptyDescription' => null,
    'paginator' => null,
    'count' => null,
    'total' => null,
])

@php
    /*
    | Product data table shell. Keeps x-ui.table as the primitive, but adds
    | workflow chrome: search/filter/action slots, density, optional selection
    | state, empty state and pagination.
    |
    | Usage:
    |   <x-ui.data-table title="Users" search :selection-ids="$users->pluck('id')">
    |       <x-slot:head><x-ui.table.heading>User</x-ui.table.heading></x-slot:head>
    |       ...
    |   </x-ui.data-table>
    */
    $method = strtoupper($method);
    $formMethod = in_array($method, ['GET', 'POST'], true) ? $method : 'POST';
    $searchValue = $searchValue ?? request($searchName, '');
    $ids = collect($selectionIds)->map(fn ($id) => (string) $id)->values()->all();

    $displayCount = $count ?? ($paginator ? $paginator->count() : null);
    $displayTotal = $total ?? ($paginator && method_exists($paginator, 'total') ? $paginator->total() : null);

    $densityClass = [
        'compact' => '[&_td]:px-4 [&_td]:py-2.5 [&_th]:px-4 [&_th]:py-3',
        'comfortable' => '[&_td]:px-6 [&_td]:py-4 [&_th]:px-6 [&_th]:py-3.5',
        'spacious' => '[&_td]:px-6 [&_td]:py-5 [&_th]:px-6 [&_th]:py-4',
    ][$density] ?? '[&_td]:px-6 [&_td]:py-4 [&_th]:px-6 [&_th]:py-3.5';

    $tableClass = implode(' ', [
        'min-w-full divide-y divide-gray-200 dark:divide-gray-800',
        $striped ? '[&_tbody_tr:nth-child(odd)]:bg-gray-50/60 dark:[&_tbody_tr:nth-child(odd)]:bg-gray-800/30' : '',
        $sticky ? '[&_thead]:sticky [&_thead]:top-0 [&_thead]:z-10' : '',
        $densityClass,
    ]);
@endphp

<section
    x-data="{
        selected: [],
        allIds: @js($ids),
        get hasSelection() { return this.selected.length > 0; },
        get allSelected() { return this.allIds.length > 0 && this.selected.length === this.allIds.length; },
        toggleAll() { this.selected = this.allSelected ? [] : [...this.allIds]; },
    }"
    {{ $attributes->merge(['class' => 'overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900']) }}
>
    @if ($title || $description || isset($actions))
        <div class="flex flex-col gap-4 border-b border-gray-200 px-5 py-4 dark:border-gray-800 sm:flex-row sm:items-start sm:justify-between sm:px-6">
            <div class="min-w-0">
                @if ($title)
                    <h3 class="text-base font-semibold text-gray-900 dark:text-white">{{ $title }}</h3>
                @endif

                @if ($description)
                    <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ $description }}</p>
                @endif

                @if ($displayCount !== null || $displayTotal !== null)
                    <p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
                        Showing {{ number_format((int) $displayCount) }}
                        @if ($displayTotal !== null)
                            of {{ number_format((int) $displayTotal) }}
                        @endif
                    </p>
                @endif
            </div>

            @isset($actions)
                <div class="flex shrink-0 items-center gap-2">{{ $actions }}</div>
            @endisset
        </div>
    @endif

    @if ($search || isset($filters))
        <form method="{{ $formMethod }}" @if ($action) action="{{ $action }}" @endif class="border-b border-gray-200 px-5 py-4 dark:border-gray-800 sm:px-6">
            @if (! in_array($method, ['GET', 'POST'], true))
                @csrf
                @method($method)
            @endif

            <div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
                <div class="flex flex-1 flex-col gap-3 sm:flex-row sm:items-center">
                    @if ($search)
                        <div class="w-full sm:max-w-xs">
                            <x-ui.input
                                type="search"
                                :name="$searchName"
                                size="sm"
                                icon="magnifying-glass"
                                :placeholder="$searchPlaceholder"
                                :value="$searchValue"
                            />
                        </div>
                    @endif

                    @isset($filters)
                        <div class="flex flex-1 flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
                            {{ $filters }}
                        </div>
                    @endisset
                </div>

                <div class="flex items-center gap-2">
                    @if ($search || isset($filters))
                        <x-ui.button type="submit" variant="outline" size="sm" icon="funnel-simple">Apply</x-ui.button>
                    @endif

                    @isset($filterActions)
                        {{ $filterActions }}
                    @endisset
                </div>
            </div>
        </form>
    @endif

    @if ($selectable)
        <div
            x-show="hasSelection"
            x-cloak
            class="flex flex-col gap-3 border-b border-brand-200 bg-brand-50/70 px-5 py-3 text-sm dark:border-brand-900 dark:bg-brand-950/30 sm:flex-row sm:items-center sm:justify-between sm:px-6"
        >
            <div class="font-medium text-brand-800 dark:text-brand-200">
                <span x-text="selected.length"></span> selected
            </div>

            @isset($bulk)
                <div class="flex items-center gap-2">{{ $bulk }}</div>
            @else
                <x-ui.button type="button" variant="ghost" size="sm" x-on:click="selected = []">Clear selection</x-ui.button>
            @endisset
        </div>
    @endif

    @if ($hasRows)
        <div class="overflow-x-auto">
            <table class="{{ $tableClass }}">
                @isset($head)
                    <thead class="bg-gray-50 dark:bg-gray-900/60">
                        <tr>
                            @if ($selectable)
                                <th scope="col" class="w-12">
                                    <input
                                        type="checkbox"
                                        class="rounded border-gray-300 text-brand-600 focus:ring-brand-500 dark:border-gray-700 dark:bg-gray-900"
                                        :checked="allSelected"
                                        :disabled="allIds.length === 0"
                                        x-on:change="toggleAll()"
                                        aria-label="Select all rows"
                                    >
                                </th>
                            @endif
                            {{ $head }}
                        </tr>
                    </thead>
                @endisset

                <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-800 dark:bg-gray-900">
                    {{ $slot }}
                </tbody>
            </table>
        </div>
    @else
        <x-ui.empty-state :icon="$emptyIcon" :title="$emptyTitle" :description="$emptyDescription" class="rounded-none border-0 shadow-none" />
    @endif

    @if ($paginator && $paginator->hasPages())
        <div class="border-t border-gray-200 px-5 py-4 dark:border-gray-800 sm:px-6">
            <x-ui.pagination :paginator="$paginator" />
        </div>
    @endif
</section>