edmirkasapi / live-datatable
A simple abstract implementation of a datatable component class for livewire applications.
Requires
- php: ^8.2
- illuminate/support: ^12.0
- livewire/livewire: ^4.4
Requires (Dev)
- orchestra/testbench: ^10.0
- phpunit/phpunit: ^11.0
README
A simple, extensible abstract datatable component for Laravel and Livewire applications.
edmirkasapi/live-datatable provides a reusable base component with built-in support for:
- Column configuration and ordering
- Visible/hidden columns
- Searching
- Filtering
- Sorting
- Pagination
- Automatic pagination reset when table state changes
- Automatic navigation to the previous page when the current page becomes empty
- Configurable pagination theme
- Configurable default number of results per page
The package is designed to be extended by your own Livewire datatable components.
Requirements
- PHP 8.2+
- Laravel 12+
- Livewire 4.4+
Installation
Install the package using Composer:
composer require edmirkasapi/live-datatable
Laravel will automatically discover the package service provider.
Basic Usage
Create a Livewire component that extends the LiveDatatable base class:
<?php namespace App\Livewire; use App\Models\User; use Edmirkasapi\LiveDatatable\abstracts\LiveDatatable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Contracts\View\View; class UsersTable extends LiveDatatable { protected function columns(): array { return [ 'name' => [ 'label' => 'Name', 'sortable' => true, 'visible' => true, ], 'email' => [ 'label' => 'Email', 'sortable' => true, 'visible' => true, ], 'created_at' => [ 'label' => 'Created', 'sortable' => true, 'visible' => true, ], ]; } protected function query(): Builder { return User::query(); } protected function searchable(): array { return [ 'name', 'email', ]; } public function render(): View { return view('livewire.users-table', [ 'data' => $this->getData(), ]); } }
The base class handles the query processing pipeline:
Base Query
↓
Search
↓
Filters
↓
Sorting
↓
Pagination
↓
Paginated Results
Defining Columns
Every datatable must implement the columns() method.
Columns are defined using an associative array where the key represents the database/query column:
protected function columns(): array { return [ 'name' => [ 'label' => 'Name', 'sortable' => true, 'visible' => true, ], 'email' => [ 'label' => 'Email', 'sortable' => true, ], 'created_at' => [ 'label' => 'Created At', 'sortable' => false, ], ]; }
Column Visibility
Columns are visible by default.
'email' => [ 'label' => 'Email', ]
is equivalent to:
'email' => [ 'label' => 'Email', 'visible' => true, ]
To hide a column:
'email' => [ 'label' => 'Email', 'visible' => false, ]
Retrieve only visible columns using:
$this->getvisiblecolumns();
Note: the method is currently named
getvisiblecolumns()with a lowercasev.
Sortable Columns
A column is not sortable unless sortable is explicitly set to true:
'name' => [ 'label' => 'Name', 'sortable' => true, ]
You can check whether a column is sortable:
$this->isSortable('name');
Column Ordering
The base component supports custom column ordering through the $columnOrder property:
protected array $columnOrder = [ 'email', 'name', 'created_at', ];
The order defined here is applied to the columns returned by columns().
Columns specified in $columnOrder that don't exist in columns() are ignored.
Any configured columns that weren't explicitly included in $columnOrder are appended afterward in their original order.
Retrieve the final order with:
$this->getOrderedColumns();
For example:
protected function columns(): array { return [ 'name' => ['label' => 'Name'], 'email' => ['label' => 'Email'], 'created_at' => ['label' => 'Created'], ]; } protected array $columnOrder = [ 'email', 'name', ];
Results in:
email
name
created_at
Query
Every datatable must implement the query() method and return an Eloquent query builder:
protected function query(): Builder { return User::query(); }
The returned query is then passed through the package's search, filter, sorting, and pagination pipeline.
Searching
Override searchable() to define which database columns can be searched:
protected function searchable(): array { return [ 'name', 'email', ]; }
The component exposes the search value through:
public string $search = '';
For example, in a Blade view:
<input type="text" wire:model.live="search" placeholder="Search..." >
The search is applied using a LIKE query:
WHERE name LIKE '%search term%' OR email LIKE '%search term%'
Searching automatically resets the current pagination page.
Resetting Search
Reset the search programmatically:
$this->resetSearch();
This clears the search value and resets the pagination page.
Filtering
Filters are stored in the public $filters property:
public array $filters = [];
A filter can be supplied using the database column name as the key:
$this->filters = [ 'status' => 'active', ];
The resulting query applies:
$query->where('status', 'active');
Only columns defined by columns() are considered valid filters.
Empty filter values and unknown filter columns are ignored.
Example
<select wire:model.live="filters.status"> <option value="">All</option> <option value="active">Active</option> <option value="inactive">Inactive</option> </select>
Changing filters automatically resets the pagination page.
Resetting Filters
Reset all filters with:
$this->resetFilters();
This clears the filters and resets pagination.
Sorting
Sorting is controlled by two public properties:
public ?string $sortColumn = null; public ?string $sortDirection = null;
A column must be configured as sortable before it can be sorted.
Trigger sorting using:
$this->sortBy('name');
The sorting state cycles through:
ascending
↓
descending
↓
none
Specifically:
null → asc → desc → null
When a new column is selected, sorting starts with ascending order.
For example:
$this->sortBy('name');
sets:
$sortColumn = 'name'; $sortDirection = 'asc';
Calling it again changes the direction to:
$sortDirection = 'desc';
Calling it a third time removes the sorting.
Blade Example
<button wire:click="sortBy('name')"> Name </button>
You can also use the sorting state to display the current direction:
<button wire:click="sortBy('name')"> Name @if ($sortColumn === 'name') @if ($sortDirection === 'asc') ↑ @elseif ($sortDirection === 'desc') ↓ @endif @endif </button>
Resetting Sorting
Reset sorting with:
$this->resetSorting();
This sets both sorting properties back to null.
Pagination
The component uses Laravel's LengthAwarePaginator and Livewire's pagination functionality.
The number of results per page is controlled by:
public ?int $perPage = null;
If $perPage has not been explicitly set, the component uses:
config('live-datatable.pagination.per_page')
with a fallback of 10.
Changing Results Per Page
You can change the value:
$this->perPage = 25;
Then reset the pagination:
$this->updatePerPage();
The updatePerPage() method resets the current page.
Reset Pagination
To reset the per-page value and pagination:
$this->resetPagination();
This restores the configured default:
config('live-datatable.pagination.per_page', 10)
and resets the current page.
Automatic Empty Page Handling
The component handles a common pagination problem automatically.
For example:
- A user is viewing page 5.
- Records are deleted or filtered out.
- Page 5 no longer contains any records.
- The component automatically moves back to the previous page.
This behavior is handled by:
$this->goToPreviousPageIfEmpty($paginator);
The component will only move backward when the current page is empty and the current page number is greater than 1.
Pagination Theme
The pagination theme is loaded from:
config('live-datatable.theme')
If no theme is configured, the default is:
bootstrap
Retrieve the current theme with:
$this->getPaginationTheme();
For example:
$theme = $this->getPaginationTheme();
Table State
The following public properties are available for controlling the table:
| Property | Type | Default | Description |
|---|---|---|---|
$perPage |
?int |
null |
Number of records per page |
$search |
string |
'' |
Current search term |
$filters |
array |
[] |
Active filters |
$sortColumn |
?string |
null |
Currently sorted column |
$sortDirection |
?string |
null |
Current sort direction |
Resetting Table State
The component provides separate methods for resetting different parts of the table:
$this->resetSearch(); $this->resetFilters(); $this->resetSorting(); $this->resetPagination();
Pagination is automatically reset when:
- Search changes
- Filters change
- Results-per-page changes
- Sorting changes
The centralized pagination reset is handled by:
$this->resetTablePage();
The method also listens for the filters-updated Livewire event.
Rendering
The base component's render() method processes the query and provides the resulting paginator as:
$data
The component therefore expects its view to use the $data variable when displaying the results.
A typical table view might look like:
<div> <input type="text" wire:model.live="search" placeholder="Search..." > <table> <thead> <tr> @foreach ($this->getvisiblecolumns() as $key => $column) <th> @if ($this->isSortable($key)) <button wire:click="sortBy('{{ $key }}')"> {{ $column['label'] ?? $key }} </button> @else {{ $column['label'] ?? $key }} @endif </th> @endforeach </tr> </thead> <tbody> @foreach ($data as $row) <tr> @foreach ($this->getvisiblecolumns() as $key => $column) <td> {{ $row->{$key} }} </td> @endforeach </tr> @endforeach </tbody> </table> {{ $data->links() }} </div>
The exact markup is intentionally left to the consuming application, allowing the datatable to be styled with Bootstrap, Tailwind, or another frontend framework.
Example Component
A more complete example:
<?php namespace App\Livewire; use App\Models\User; use Edmirkasapi\LiveDatatable\abstracts\LiveDatatable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Contracts\View\View; class UsersTable extends LiveDatatable { protected array $columnOrder = [ 'name', 'email', 'created_at', ]; protected function columns(): array { return [ 'name' => [ 'label' => 'Name', 'sortable' => true, 'visible' => true, ], 'email' => [ 'label' => 'Email', 'sortable' => true, 'visible' => true, ], 'created_at' => [ 'label' => 'Created', 'sortable' => true, 'visible' => true, ], ]; } protected function searchable(): array { return [ 'name', 'email', ]; } protected function query(): Builder { return User::query(); } public function render(): View { return view('livewire.users-table', [ 'data' => $this->getData(), ]); } }
Development & Testing
The package uses PHPUnit for testing and Orchestra Testbench for package development.
Install development dependencies:
composer install
Run the package test suite:
vendor/bin/phpunit
Development dependencies include:
- PHPUnit 11
- Orchestra Testbench 10
Package Structure
live-datatable/
├── src/
│ ├── abstracts/
│ │ └── LiveDatatable.php
│ └── ...
├── resources/
│ └── views/
├── tests/
│ ├── Feature/
│ ├── Unit/
│ └── ...
├── composer.json
└── phpunit.xml
Contributing
Contributions, bug reports, and feature requests are welcome.
When contributing:
- Fork the repository.
- Create a feature or bug-fix branch.
- Make your changes.
- Add or update tests where appropriate.
- Run the test suite.
- Submit a pull request.
License
This package is open-sourced software licensed under the MIT License.