Datatable Components
Feature-rich data table components with sorting, filtering, pagination, bulk actions, and Livewire integration.
Datatable Components
LaraDashboard provides a comprehensive datatable system built on Livewire for displaying, sorting, filtering, and managing tabular data. The datatable includes features like checkbox selection, bulk actions, and responsive design.
Available Datatable Components
| Component | Purpose |
|---|---|
<x-datatable.datatable> |
Main datatable component |
<x-datatable.searchbar> |
Search input for filtering |
<x-datatable.responsive-filters> |
Filter dropdowns |
<x-datatable.skeleton> |
Loading state placeholder |
<x-table.empty-state> |
No results message |
Datatable Component
The main datatable component with full feature support.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title |
string | '' |
Table title |
enableLivewire |
boolean | true |
Enable Livewire integration |
enableSearchbar |
boolean | true |
Show search input |
searchbarPlaceholder |
string | 'Search...' |
Search placeholder |
customSearchForm |
string | null |
Custom search form HTML |
enableFilters |
boolean | true |
Show filter dropdowns |
filters |
array | [] |
Filter configuration |
customFilters |
string | null |
Custom filter HTML |
enableBulkActions |
boolean | true |
Enable bulk selection |
customBulkActions |
string | null |
Custom bulk actions |
direction |
string | 'desc' |
Default sort direction |
enableNewResourceLink |
boolean | false |
Show create button |
newResourceLinkPermission |
string | '' |
Required permission |
newResourceLinkIcon |
string | 'feather:plus' |
Button icon |
newResourceLinkRouteName |
string | '' |
Route name |
newResourceLinkRouteUrl |
string | '' |
Direct URL |
newResourceLinkLabel |
string | 'Create New' |
Button label |
data |
paginator | required | Paginated data |
enableCheckbox |
boolean | true |
Show row checkboxes |
noResultsMessage |
string | 'No data found.' |
Empty state message |
enablePagination |
boolean | true |
Show pagination |
headers |
array | [] |
Column configuration |
sort |
string | '' |
Current sort column |
perPage |
integer | 10 |
Items per page |
perPageOptions |
array | [10,20,50,100,'All'] |
Page size options |
Header Configuration
Each header in the headers array supports:
| Key | Type | Description |
|---|---|---|
id |
string | Column identifier (model attribute) |
title |
string | Display title |
sortable |
boolean | Enable sorting |
sortBy |
string | Sort column (if different from id) |
width |
string | Column width |
align |
string | Text alignment: left, center, right |
renderContent |
string | Method name for custom rendering |
renderRawContent |
string | Raw HTML content |
Basic Usage with Livewire
// Livewire Component
class UsersList extends Component
{
use WithPagination;
public $search = '';
public $sort = 'created_at';
public $direction = 'desc';
public $perPage = 10;
public $selectedItems = [];
public function render()
{
$users = User::query()
->when($this->search, fn($q) => $q->where('name', 'like', "%{$this->search}%"))
->orderBy($this->sort, $this->direction)
->paginate($this->perPage);
return view('livewire.users-list', [
'users' => $users,
'headers' => $this->getHeaders(),
]);
}
public function getHeaders()
{
return [
['id' => 'name', 'title' => 'Name', 'sortable' => true, 'sortBy' => 'name'],
['id' => 'email', 'title' => 'Email', 'sortable' => true, 'sortBy' => 'email'],
['id' => 'created_at', 'title' => 'Created', 'sortable' => true, 'sortBy' => 'created_at'],
['id' => 'actions', 'title' => 'Actions', 'align' => 'right'],
];
}
public function sortBy($column)
{
if ($this->sort === $column) {
$this->direction = $this->direction === 'asc' ? 'desc' : 'asc';
} else {
$this->sort = $column;
$this->direction = 'asc';
}
}
public function getBulkDeleteAction()
{
return [
'url' => route('admin.users.bulk-delete'),
'method' => 'DELETE',
];
}
// Custom column rendering
public function renderNameColumn($item, $header)
{
return view('components.users.name-cell', ['user' => $item])->render();
}
public function renderActionsColumn($item, $header)
{
return view('components.users.actions-cell', ['user' => $item])->render();
}
}
{{-- Blade Template --}}
<x-datatable.datatable
:data="$users"
:headers="$headers"
:sort="$sort"
:direction="$direction"
:perPage="$perPage"
searchbarPlaceholder="{{ __('Search users...') }}"
:enableNewResourceLink="true"
newResourceLinkPermission="create users"
newResourceLinkRouteName="admin.users.create"
newResourceLinkLabel="{{ __('Add User') }}"
/>
With Filters
// In Livewire component
public $statusFilter = '';
public $roleFilter = '';
public function getFilters()
{
return [
[
'id' => 'status',
'label' => 'Status',
'options' => [
'' => 'All Statuses',
'active' => 'Active',
'inactive' => 'Inactive',
],
],
[
'id' => 'role',
'label' => 'Role',
'options' => Role::pluck('name', 'id')->prepend('All Roles', '')->toArray(),
],
];
}
public function hasActiveFilters()
{
return !empty($this->statusFilter) || !empty($this->roleFilter);
}
<x-datatable.datatable
:data="$users"
:headers="$headers"
:filters="$this->getFilters()"
...
/>
Custom Column Rendering
There are multiple ways to customize column content:
Method 1: Auto-discovered Method
Name your method render{PascalCaseId}Column:
// For header with id='status', create:
public function renderStatusColumn($item, $header)
{
$statusClass = $item->is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800';
return "<span class='px-2 py-1 rounded text-xs {$statusClass}'>"
. ($item->is_active ? 'Active' : 'Inactive')
. "</span>";
}
// For header with id='created_at', create:
public function renderCreatedAtColumn($item, $header)
{
return $item->created_at->format('M d, Y');
}
Method 2: Explicit Method Reference
$headers = [
[
'id' => 'avatar',
'title' => 'Avatar',
'renderContent' => 'renderAvatarCell',
],
];
public function renderAvatarCell($item, $header)
{
return '<img src="' . $item->avatar_url . '" class="w-8 h-8 rounded-full" />';
}
Method 3: Raw Content
$headers = [
[
'id' => 'badge',
'title' => '',
'renderRawContent' => '<span class="w-2 h-2 rounded-full bg-primary"></span>',
],
];
Bulk Actions
The datatable includes built-in bulk delete functionality. For custom bulk actions:
<x-datatable.datatable
:data="$users"
:headers="$headers"
:customBulkActions="view('partials.custom-bulk-actions')->render()"
/>
{{-- partials/custom-bulk-actions.blade.php --}}
<div x-show="selectedItems.length > 0" class="flex items-center gap-2">
<x-buttons.button
@click="$wire.bulkActivate(selectedItems)"
variant="success"
icon="lucide:check"
>
Activate Selected
</x-buttons.button>
<x-buttons.button
@click="$wire.bulkDeactivate(selectedItems)"
variant="warning"
icon="lucide:x"
>
Deactivate Selected
</x-buttons.button>
</div>
Real Example from Codebase
{{-- Users datatable --}}
<x-datatable.datatable
:data="$users"
:headers="[
['id' => 'name', 'title' => __('Name'), 'sortable' => true, 'sortBy' => 'name'],
['id' => 'email', 'title' => __('Email'), 'sortable' => true, 'sortBy' => 'email'],
['id' => 'roles', 'title' => __('Roles')],
['id' => 'status', 'title' => __('Status'), 'sortable' => true, 'sortBy' => 'is_active'],
['id' => 'created_at', 'title' => __('Created'), 'sortable' => true, 'sortBy' => 'created_at'],
['id' => 'actions', 'title' => __('Actions'), 'align' => 'right'],
]"
:sort="$sort"
:direction="$direction"
:perPage="$perPage"
:filters="$this->getFilters()"
searchbarPlaceholder="{{ __('Search users by name or email...') }}"
:enableNewResourceLink="true"
newResourceLinkPermission="create users"
newResourceLinkRouteName="admin.users.create"
newResourceLinkLabel="{{ __('Add User') }}"
newResourceLinkIcon="feather:user-plus"
/>
Searchbar Component
Search input component for the datatable.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder |
string | 'Search...' |
Input placeholder |
enableLivewire |
boolean | true |
Enable wire:model |
Basic Usage
<x-datatable.searchbar
placeholder="{{ __('Search...') }}"
/>
Standalone Usage
<x-datatable.searchbar
placeholder="Search products..."
:enableLivewire="true"
/>
Responsive Filters Component
Filter dropdown controls for the datatable.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
filters |
array | [] |
Filter configuration |
enableLivewire |
boolean | true |
Livewire integration |
hasActiveFilters |
boolean | false |
Show active indicator |
Filter Configuration
$filters = [
[
'id' => 'status', // Maps to $status property
'label' => 'Status',
'options' => [
'' => 'All',
'active' => 'Active',
'pending' => 'Pending',
'inactive' => 'Inactive',
],
],
[
'id' => 'category_id',
'label' => 'Category',
'options' => Category::pluck('name', 'id')->prepend('All Categories', '')->toArray(),
],
];
Usage
<x-datatable.responsive-filters
:filters="$filters"
:hasActiveFilters="$this->hasActiveFilters()"
/>
Skeleton Component
Loading placeholder while datatable data loads.
Basic Usage
{{-- Shows while loading --}}
<div wire:loading wire:target="search">
<x-datatable.skeleton />
</div>
Empty State Component
Displayed when no results are found.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
message |
string | 'No results found' |
Display message |
icon |
string | 'lucide:inbox' |
Icon to display |
Basic Usage
<x-table.empty-state
message="{{ __('No users found matching your criteria.') }}"
icon="lucide:users"
/>
Complete Livewire Component Example
<?php
namespace App\Livewire\Admin;
use App\Models\User;
use App\Models\Role;
use Livewire\Component;
use Livewire\WithPagination;
class UsersDatatable extends Component
{
use WithPagination;
public $search = '';
public $sort = 'created_at';
public $direction = 'desc';
public $perPage = 10;
public $selectedItems = [];
public $statusFilter = '';
public $roleFilter = '';
protected $queryString = [
'search' => ['except' => ''],
'sort' => ['except' => 'created_at'],
'direction' => ['except' => 'desc'],
'perPage' => ['except' => 10],
];
public function updatingSearch()
{
$this->resetPage();
}
public function sortBy($column)
{
if ($this->sort === $column) {
$this->direction = $this->direction === 'asc' ? 'desc' : 'asc';
} else {
$this->sort = $column;
$this->direction = 'asc';
}
}
public function getHeaders()
{
return [
['id' => 'name', 'title' => 'Name', 'sortable' => true, 'sortBy' => 'name'],
['id' => 'email', 'title' => 'Email', 'sortable' => true, 'sortBy' => 'email'],
['id' => 'roles', 'title' => 'Roles'],
['id' => 'status', 'title' => 'Status', 'sortable' => true, 'sortBy' => 'is_active'],
['id' => 'created_at', 'title' => 'Created', 'sortable' => true],
['id' => 'actions', 'title' => 'Actions', 'align' => 'right'],
];
}
public function getFilters()
{
return [
[
'id' => 'statusFilter',
'label' => 'Status',
'options' => [
'' => 'All',
'active' => 'Active',
'inactive' => 'Inactive',
],
],
[
'id' => 'roleFilter',
'label' => 'Role',
'options' => Role::pluck('name', 'id')
->prepend('All Roles', '')
->toArray(),
],
];
}
public function hasActiveFilters()
{
return !empty($this->statusFilter) || !empty($this->roleFilter);
}
public function getBulkDeleteAction()
{
return [
'url' => null, // Use Livewire method
'method' => 'DELETE',
];
}
public function bulkDelete()
{
User::whereIn('id', $this->selectedItems)->delete();
$this->selectedItems = [];
$this->dispatch('resetSelectedItems');
$this->dispatch('notify', [
'variant' => 'success',
'title' => 'Deleted',
'message' => 'Selected users have been deleted.',
]);
}
// Custom column renderers
public function renderNameColumn($user, $header)
{
return '<div class="flex items-center gap-3">
<img src="' . $user->avatar_url . '" class="w-8 h-8 rounded-full" />
<span>' . e($user->name) . '</span>
</div>';
}
public function renderRolesColumn($user, $header)
{
$badges = $user->roles->map(function ($role) {
return '<span class="px-2 py-1 text-xs rounded bg-primary/10 text-primary">'
. e($role->name) . '</span>';
})->join(' ');
return '<div class="flex flex-wrap gap-1">' . $badges . '</div>';
}
public function renderStatusColumn($user, $header)
{
$class = $user->is_active
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
$label = $user->is_active ? 'Active' : 'Inactive';
return "<span class='px-2 py-1 text-xs rounded {$class}'>{$label}</span>";
}
public function renderCreatedAtColumn($user, $header)
{
return $user->created_at->format('M d, Y');
}
public function renderActionsColumn($user, $header)
{
return view('livewire.users.action-buttons', ['user' => $user])->render();
}
public function render()
{
$users = User::query()
->with('roles')
->when($this->search, function ($query) {
$query->where(function ($q) {
$q->where('name', 'like', "%{$this->search}%")
->orWhere('email', 'like', "%{$this->search}%");
});
})
->when($this->statusFilter, function ($query) {
$query->where('is_active', $this->statusFilter === 'active');
})
->when($this->roleFilter, function ($query) {
$query->whereHas('roles', fn($q) => $q->where('id', $this->roleFilter));
})
->orderBy($this->sort, $this->direction)
->paginate($this->perPage);
return view('livewire.admin.users-datatable', [
'users' => $users,
'headers' => $this->getHeaders(),
'filters' => $this->getFilters(),
]);
}
}
Best Practices
- Use pagination - Always paginate large datasets
- Enable search - Let users find specific records
- Add relevant filters - Filter by common attributes
- Show loading states - Use
wire:loadingfor feedback - Implement bulk actions - Allow batch operations
- Custom column rendering - Format data appropriately
- Responsive design - Ensure mobile usability
- Sort important columns - Enable sorting on key fields