K

Table

Wire Table

Enterprise-grade Livewire table component for Laravel. Depends on wire-core and wire-forms.

Wire Table preview

Enterprise-grade Livewire table component for Laravel. Depends on wire-core and wire-forms.

Installation

composer require nyoncode/wire-table

Add to Tailwind content paths:

module.exports = {
content: [
// ...
'./vendor/nyoncode/wire-core/resources/views/**/*.blade.php',
'./vendor/nyoncode/wire-forms/resources/views/**/*.blade.php',
'./vendor/nyoncode/wire-table/resources/views/**/*.blade.php',
],
}

Publish config (optional):

php artisan vendor:publish --tag=wire-table-config

Quick Start

use Livewire\Component;
use NyonCode\WireTable\Concerns\WithTable;
use NyonCode\WireTable\Table;
use NyonCode\WireTable\Columns\TextColumn;
use NyonCode\WireTable\Columns\BadgeColumn;
use NyonCode\WireTable\Filters\SelectFilter;
use NyonCode\WireCore\Actions\Action;
use NyonCode\WireCore\Actions\DeleteAction;
use NyonCode\WireCore\Actions\DeleteBulkAction;
 
class UserTable extends Component
{
use WithTable;
 
public function table(Table $table): Table
{
return $table
->model(User::class)
->columns([
TextColumn::make('name')
->sortable()
->searchable()
->weight('bold'),
 
TextColumn::make('email')
->searchable()
->copyable()
->copyMessage('Email copied!'),
 
BadgeColumn::make('role')
->colors([
'primary' => 'admin',
'success' => 'editor',
'gray' => 'viewer',
]),
 
TextColumn::make('created_at')
->dateTime('d.m.Y')
->sortable()
->size('sm')
->textColor('gray'),
])
->filters([
SelectFilter::make('role')
->options([
'admin' => 'Admin',
'editor' => 'Editor',
'viewer' => 'Viewer',
]),
])
->actions([
Action::make('edit')
->icon('pencil')
->url(fn (User $r) => route('users.edit', $r)),
DeleteAction::make(),
])
->bulkActions([
DeleteBulkAction::make(),
])
->defaultSort('name')
->searchable()
->paginated()
->striped()
->hoverable();
}
 
public function render()
{
return view('livewire.user-table');
}
}
{{-- resources/views/livewire/user-table.blade.php --}}
<div>
{{ $this->table }}
</div>

That's it. The table handles search, sort, filter, pagination, actions, and inline editing — all with zero JavaScript configuration.


WithTable Trait

The WithTable trait is the Livewire integration layer. It provides:

  • All Livewire-bound public properties (search, sort, filters, pagination, selection)
  • Lifecycle hooks (mountWithTable, property watchers)
  • Query building via TableQueryService
  • Action execution pipeline
  • Inline editing pipeline
  • Modal management
  • Row expansion (sub-rows)
  • Column visibility toggling
  • SQL/query debugging

Public Properties (Livewire State)

These are automatically synced with the browser via Livewire:

Property Type Default Description
$tableSearch ?string null Current search term
$tableSortColumn string '' Current sort column name
$tableSortDirection string 'asc' 'asc' or 'desc'
$tablePerPage int 10 Records per page
$tableFilters array [] Active filter values: ['role' => 'admin', ...]
$columnFilters array [] Column-level filter values
$selectedRecords array [] Primary keys of selected records
$hiddenColumns array [] Column names hidden by user
$expandedRows array [] Primary keys of expanded rows (sub-rows)
$flattenMode bool false Show all sub-rows inline

Livewire Methods (wire: callable)

These are called from Alpine.js or Livewire directives in the Blade views:

Method Called When
sortTable($column) User clicks column header
resetSort() User resets sort
updatedTableSearch($value) Search input changes
updatedTableFilters() Filter value changes
updatedColumnFilters() Column filter changes
updatedTablePerPage() Per-page selector changes
toggleColumnVisibility($name) User hides/shows column
selectRecord($key) Checkbox toggled
selectAll() "Select all" toggled
deselectAll() "Deselect all" clicked
expandRow($key) Row expand/collapse
toggleFlattenMode() Flatten sub-rows toggle
executeAction($name, $key) Action button clicked
executeBulkAction($name) Bulk action clicked
updateCell($column, $key, $value) Inline edit committed
confirmActionExecution() Modal "confirm" clicked
cancelAction() Modal "cancel" clicked
submitActionForm() Action form submitted

Table Configuration API

The Table class provides a comprehensive fluent API. Below is the complete reference.

Data Source

// From Eloquent model class (auto-creates query)
->model(string $modelClass)
 
// Custom base query (overrides model)
->query(Builder $query)
 
// Modify the auto-generated query
->modifyQueryUsing(Closure $fn)
 
// Primary key column (default: 'id')
->primaryKey(string $column)

Examples:

// Simple model
$table->model(User::class);
 
// Custom query with eager loads and scopes
$table->query(
User::query()
->where('tenant_id', auth()->user()->tenant_id)
->withCount(['posts', 'comments'])
->with(['department', 'team'])
);
 
// Modify auto-query
$table->model(User::class)
->modifyQueryUsing(fn (Builder $q) => $q->where('active', true));
 
// UUID primary key
$table->model(Order::class)->primaryKey('uuid');

Columns

->columns(array $columns)

See Columns Reference for all 13 column types.

Filters

->filters(array $filters)

See Filters Reference for all filter types.

Actions

// Row actions (per-record)
->actions(array $actions)
 
// Bulk actions (for selected records)
->bulkActions(array $actions)
 
// Header actions (table-level, no record context)
->headerActions(array $actions)
 
// Actions column position
->actionsPosition(string 'start'|'end') // default: 'end'
 
// Actions column alignment
->actionsAlignment(string 'left'|'center'|'right')
 
// Actions column header label
->actionsColumnLabel(string $label)
 
// Actions column fixed width
->actionsColumnWidth(string $width) // e.g., '120px'

See Actions for the full Actions API.

// Enable global search across all searchable columns
->searchable(bool $searchable = true)

Search uses a database-aware strategy:

  • MySQL: MATCH ... AGAINST fulltext (if index exists) or LIKE
  • PostgreSQL: to_tsvector / ts_query
  • SQLite: LIKE '%term%' fallback

Sorting

// Enable column header sorting
->sortable(bool $sortable = true)
 
// Default sort on initial load
->defaultSort(string $column, string $direction = 'asc')

Pagination

// Enable pagination
->paginated(bool $paginated = true)
 
// Default per-page count
->perPage(int $perPage = 10)
 
// Per-page dropdown options
->perPageOptions(array $options = [10, 25, 50, 100])
 
// Simple pagination — no COUNT(*) query, just Previous/Next
->simplePagination()
 
// Cursor pagination — offset-free, constant-time
->cursorPagination()
 
// Standard pagination (default) — full page numbers
->standardPagination()

When to use which:

Mode Best For Trade-offs
Standard < 100k records, users need page numbers COUNT(*) on every page load
Simple 100k-1M records, sequential browsing No total count, no page numbers
Cursor > 1M records, real-time data No random page access, opaque cursors

Selection (Bulk Actions)

// Enable checkbox selection column
->selectable(bool $selectable = true)

When enabled, checkboxes appear. Selected record keys are stored in $selectedRecords. Bulk actions operate on the selection.

Appearance

// Alternating row colors
->striped(bool $striped = true)
 
// Row hover highlight (default: true)
->hoverable(bool $hoverable = true)
 
// Reduced cell padding
->compact(bool $compact = true)
 
// Table/cell borders
->bordered(bool $bordered = true)
 
// Custom CSS class on <table> element
->tableClass(string $class)
 
// Custom CSS class on <thead>
->headerClass(string $class)
 
// Custom CSS class on <tr>
->rowClass(string|Closure $class)

Dynamic row classes:

->rowClass(fn (User $record) => match(true) {
$record->is_banned => 'bg-red-50 dark:bg-red-900/10',
$record->is_admin => 'bg-blue-50 dark:bg-blue-900/10',
default => '',
})

Record URL (Clickable Rows)

// Make entire row clickable
->recordUrl(string|Closure $url)
// With Closure
->recordUrl(fn (User $record) => route('users.show', $record))

Responsive Layout

// Stack columns vertically on mobile
->stackedOnMobile(bool $stacked = true)
 
// Breakpoint below which to stack (default: 'md')
->stackedBreakpoint(string $breakpoint) // 'sm', 'md', 'lg', 'xl'

Empty State

->emptyState(?string $heading = null, ?string $description = null, ?string $icon = null)
$table->emptyState(
heading: 'No users found',
description: 'Try adjusting your filters or search term.',
icon: 'users',
)

Polling (Auto-Refresh)

// Enable polling at interval
->poll(string $interval = '5s')
 
// Continue polling when browser tab is hidden
->pollKeepAlive(bool $keepAlive = true)
 
// Only poll when element is visible in viewport
->pollOnlyVisible(bool $onlyVisible = true)
 
// Conditional polling
->pollWhen(Closure $condition)
 
// Livewire method to call on poll (default: re-render)
->pollMethod(string $method)
// Poll every 5s while there are pending jobs
$table->poll('5s')
->pollWhen(fn () => Job::where('status', 'pending')->exists());

Lazy Loading

// Defer initial table render
->lazy(bool $lazy = true)
 
// Placeholder HTML during loading
->lazyPlaceholder(string $html)
$table->lazy()
->lazyPlaceholder(
'<div class="flex items-center justify-center p-12">
<x-wire::icon name="refresh" class="w-8 h-8 animate-spin text-gray-400" />
</div>'
);

Performance

// Cache query results
->cacheQuery(int $ttl, ?string $key = null)
 
// Process records in chunks (for bulk operations)
->chunk(int $size, Closure $callback)
// Cache for 60 seconds — key auto-generated from state hash
$table->cacheQuery(60);
 
// Custom cache key
$table->cacheQuery(300, 'users-table');

Notifications

// Override notification driver for this table
->notificationDriver(string $driver)

Debugging

// Get the QueryPlan object for inspection
->debugQueryPlan(): QueryPlan
 
// Get raw SQL with bindings interpolated
->toSql(): string
 
// Get column metadata analysis
->getColumnsInfo(): array
->getDatabaseColumns(): array
->getDatabaseColumnsInfo(): array

Inline Editing

Three column types support inline editing — cells become editable inputs that validate and save immediately:

Column Type UI Element Saves On
TextInputColumn <input> Blur or Enter
SelectColumn <select> Change
ToggleColumn Switch Click
use NyonCode\WireTable\Columns\TextInputColumn;
use NyonCode\WireTable\Columns\SelectColumn;
use NyonCode\WireTable\Columns\ToggleColumn;
 
$table->columns([
TextInputColumn::make('name')
->rules(['required', 'string', 'max:255'])
->saveOnBlur(),
 
SelectColumn::make('status')
->options([
'draft' => 'Draft',
'review' => 'In Review',
'published' => 'Published',
])
->rules(['required', 'in:draft,review,published']),
 
ToggleColumn::make('is_featured')
->onColor('success')
->offColor('gray')
->disabled(fn ($record) => ! $record->is_published),
]);

Inline Edit Lifecycle

  1. User modifies cell value
  2. updateCell($column, $recordKey, $newValue) is called
  3. Validation runs against column rules
  4. Event CellUpdating dispatched (can be listened to)
  5. Eloquent update persists the new value
  6. Event CellUpdated dispatched
  7. Success notification shown

If validation fails, the cell reverts and shows an error message.

Custom Save Logic

TextInputColumn::make('name')
->rules(['required', 'string', 'max:255'])
->editableUsing(function (Model $record, string $column, mixed $value) {
// Custom save logic
$record->update([$column => Str::title($value)]);
Cache::forget("user:{$record->id}");
})

Real-World Patterns

Multi-Tenant Table

public function table(Table $table): Table
{
return $table
->query(
Order::query()->where('tenant_id', auth()->user()->tenant_id)
)
->columns([...])
->filters([...]);
}

Table with Complex Relations

$table->model(Invoice::class)
->columns([
TextColumn::make('number')->searchable(),
TextColumn::make('client.company.name') // nested relation
->label('Company')
->searchable(),
TextColumn::make('items.sum.amount') // aggregate
->label('Total')
->money('CZK'),
TextColumn::make('payments.count') // count aggregate
->label('Payments'),
BadgeColumn::make('status')
->colors([...]),
]);

Conditional Actions

$table->actions([
Action::make('approve')
->icon('check')
->color('success')
->visible(fn ($record) => $record->status === 'pending')
->action(fn ($record) => $record->approve()),
 
Action::make('edit')
->icon('pencil')
->disabled(fn ($record) => $record->is_locked)
->url(fn ($record) => route('invoices.edit', $record)),
 
ActionGroup::make('more', [
Action::make('duplicate')
->icon('copy')
->action(fn ($r) => $r->replicate()->save()),
Action::make('pdf')
->icon('document')
->url(fn ($r) => route('invoices.pdf', $r))
->openUrlInNewTab(),
Action::divider(),
Action::make('delete')
->icon('trash')
->color('danger')
->requiresConfirmation()
->modalHeading('Delete Invoice?')
->action(fn ($r) => $r->delete()),
]),
]);

Dynamic Per-Page with URL Sync

All state properties are Livewire-bound, so they persist across page loads via query string (if configured in your Livewire component):

class UserTable extends Component
{
use WithTable;
 
// Persist state in URL
protected $queryString = [
'tableSearch' => ['except' => ''],
'tableSortColumn' => ['except' => ''],
'tableSortDirection' => ['except' => 'asc'],
'tablePerPage' => ['except' => 10],
];
}

Document What It Covers
Columns All 13 column types — TextColumn, BadgeColumn, BooleanColumn, IconColumn, ImageColumn, ButtonColumn, ToggleColumn, SelectColumn, TextInputColumn, StackedColumn, SplitColumn, PollColumn
Filters SelectFilter, DateFilter, NumberRangeFilter, TernaryFilter, custom filters, column-level filters
Exports CSV, Excel, and PDF exports for the current table query
Advanced Sub-rows, summary footer, polling, lazy loading, caching, debug, responsive
Actions Full Action system — modals, forms, wizard steps, lifecycle