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

  1. Use pagination - Always paginate large datasets
  2. Enable search - Let users find specific records
  3. Add relevant filters - Filter by common attributes
  4. Show loading states - Use wire:loading for feedback
  5. Implement bulk actions - Allow batch operations
  6. Custom column rendering - Format data appropriately
  7. Responsive design - Ensure mobile usability
  8. Sort important columns - Enable sorting on key fields

Next Steps

/