alp-develop/laravel-livewire-tables
Reactive Livewire data tables for Laravel—search, sort, filter, paginate, export, and bulk actions with zero JavaScript. Supports Laravel 10–13, Livewire 3–4, PHP 8.1+, Tailwind or Bootstrap 4/5, plus dark mode and configurable themes.
Installation:
composer require alp-develop/laravel-livewire-tables
php artisan vendor:publish --tag=livewire-tables-config
Configure config/livewire-tables.php (set theme to tailwind, bootstrap-5, or bootstrap-4).
Generate a Table:
php artisan make:livewiretable UsersTable User
This creates a scaffolded UsersTable class in app/Livewire/Tables/.
First Use Case:
UsersTable to define columns, filters, and actions (see Quick Start in README).<livewire:tables.users-table />
docs/columns.md (e.g., TextColumn, BooleanColumn, ActionColumn).docs/filters.md (e.g., SelectFilter, DateFilter).config/livewire-tables.php (themes, dark mode, defaults).UsersTable example in the README covers 90% of use cases.Define the Table Class:
Extend Livewire\Tables\Livewire\DataTableComponent and implement:
query(): Return the base Eloquent query.columns(): Define columns (e.g., TextColumn::make('name')->sortable()).filters(): Add filters (e.g., SelectFilter::make('status')).bulkActions(): Register actions (e.g., deleteSelected, exportCsvAuto).configure(): Set defaults (e.g., setDefaultPerPage(25)).Render the Table:
Use <livewire:tables.users-table /> in Blade. For multiple instances, pass a unique table-key:
<livewire:tables.users-table table-key="active-users" />
Handle Actions:
Implement methods for bulk/actions (e.g., deleteSelected(), edit(int $id)). Use $this->getSelectedIds() for bulk operations.
Dynamic Data: Pass dynamic data via Livewire properties:
<livewire:tables.users-table :user-role="$role" />
Access in the component with public $userRole.
Conditional Columns:
Use isVisible() in columns:
TextColumn::make('email')->visible(fn ($row) => $row->isAdmin()),
Custom Queries:
Override query() to scope data:
public function query(): Builder
{
return User::where('department', $this->department)->query();
}
Toolbar Slots: Add custom content to the toolbar using slots:
<livewire:tables.users-table>
<x-slot name="toolbar-right">
<button wire:click="customAction">Custom Button</button>
</x-slot>
</livewire:tables.users-table>
Dark Mode: Enable in config and toggle via JavaScript:
document.documentElement.classList.toggle('lt-dark');
window.dispatchEvent(new Event('lt-dark-toggled'));
Store session value (e.g., session(['lt-dark' => true])).
Export Customization:
Override exportCsvAuto() or use export() for custom exports:
public function export(): void
{
$this->exportCsv('users_export', function ($query) {
return $query->select('name', 'email');
});
}
Joins:
Use query() to add joins, then reference joined columns in columns():
public function query(): Builder
{
return User::query()->join('roles', 'users.role_id', 'roles.id');
}
// In columns:
TextColumn::make('roles.name')->label('Role'),
Events:
Listen for table events (e.g., table.rendered) to trigger side effects:
public function boot()
{
$this->dispatchBrowserEvent('table-rendered');
}
Shared State Without table-key:
table-key is set.table-key for multiple instances:
<livewire:tables.users-table table-key="users-active" />
Column Field Mismatches:
field() in a column doesn’t match the query results, the column may render blank or throw errors.field() matches the database column or use format() to transform values:
TextColumn::make('created_at')->format(fn ($value) => $value->format('M d, Y')),
Filter Value Normalization:
normalizeValue() in custom filters or ensure filter values match the query:
SelectFilter::make('status')
->setOptions(['active' => 'Active', 'inactive' => 'Inactive'])
->filter(fn (Builder $q, $v) => $q->where('status', $v)),
Bulk Actions and Selected IDs:
$this->getSelectedIds() may return unexpected results if rows are deleted/reordered between selections.getSelectedIds() immediately before the bulk action and handle edge cases:
public function deleteSelected(): void
{
$ids = $this->getSelectedIds();
if (empty($ids)) return;
User::whereIn('id', $ids)->delete();
}
Dark Mode Not Applying:
$this->darkMode is always false.dark_mode.enabled is true in config.selector) matches the stored value.lt-dark-toggled event after toggling the CSS class.Performance with Large Datasets:
with() sparingly in query().columns() to only those needed.Custom Steps Not Triggering:
SoftDeleteStep) aren’t applied.Engine in getEngine() or via configure().Engine instance in $this->cachedEngine:
protected function getEngine(): Engine
{
if ($this->cachedEngine) return $this->cachedEngine;
return $this->cachedEngine = (new Engine($columns, $filters))
->addStep(new SoftDeleteStep);
}
Export Formulas Vulnerability:
export() with a closure to control output:
public function export(): void
{
$this->exportCsv('safe_export', function ($query) {
return $query->select('name', 'email')->addSelect(\DB::raw('CONCAT(first_name, " ", last_name) as full_name'));
});
}
Log the Query:
Use toSql() and getBindings() to debug the generated query:
public function query(): Builder
{
$query = User::query();
\Log::info($query->toSql(), $query->getBindings());
return $query;
}
Inspect State:
Dump the State object to verify search, filters, and sort values:
public function mount()
{
\Log::info($this->state);
}
Check Engine Steps: Log the pipeline steps to ensure custom steps are applied:
protected function getEngine(): Engine
{
$engine = (new Engine($columns, $filters))
->addStep(new \App\Steps\SoftDeleteStep);
\Log::info('Engine steps:', $engine->steps());
return $this->cachedEngine = $engine;
}
Verify Table Key:
Check if table-key is being passed correctly:
public function mount($tableKey
How can I help you explore Laravel packages today?