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>