A comprehensive Laravel Livewire table component package built on top of Flux UI that provides a modular, customizable way to create data tables with advanced features like filtering, sorting, searching, and PostgreSQL-specific functionality.
Important
This package is NOT "in development". If I need to extend it i will do so. Feel free to fork it or use it as is. But feature requests are probably being ignored. Same for pull requests
Warning
This package is meant to be used in combination with a PostgreSQL database as it provides utils that most databases dont have. For example json path walking & vectors.
Flux Tables provides a complete table solution for Laravel applications with:
- Modular Architecture: Use traits to build custom table components with only the features you need
- Rich Column Types: Text, DateTime, Boolean, JSON, Component, Action, and List columns
- Advanced Filtering: Boolean, Date Range, Select, Value Present, and Deleted filters with session persistence
- PostgreSQL Features: JSON path querying, advanced sorting, and database-specific optimizations
- Livewire Integration: Reactive components with real-time updates and state management
- Flux UI Integration: Beautiful, accessible UI components out of the box
The package offers two main approaches:
- Quick Setup: Use the pre-built
SimpleTablecomponent for rapid development - Custom Components: Build your own table components using the modular trait system
The modular design allows you to pick and choose features:
HasEloquentTable- Core Eloquent query functionalityHasFilters- Filter management and applicationHasSorting- Column sorting with PostgreSQL JSON supportHasSearch- Global search functionalityHasActions- Row-level actions (edit, delete, etc.)HasToggleableColumns- Show/hide columnsHasDynamicPagination- Configurable pagination
You can install the package via composer:
composer require idkwhoami/flux-tablesRun the installation command:
php artisan flux-tables:installThis will use the flux:icon artisan command to fetch the Lucide icons used in the package.
Here's a basic example using the pre-built SimpleTable component:
$filters = [
\Idkwhoami\FluxTables\Concretes\Filter\DeletedFilter::make('deleted')
->label('Deletion State')
->default(\Idkwhoami\FluxTables\Enums\DeletionState::WithoutDeleted->value),
\Idkwhoami\FluxTables\Concretes\Filter\DateRangeFilter::make('created')
->property('created_at')
->label('Created'),
\Idkwhoami\FluxTables\Concretes\Filter\ValuePresentFilter::make('email_verified')
->property('email_verified_at')
->label('Exclude unverified')
->description('Hide all users that haven\'t verified their email address.')
->pillContent('Unverified excluded'),
\Idkwhoami\FluxTables\Concretes\Filter\BooleanFilter::make('banned')
->property('banned'),
];
$columns = [
\Idkwhoami\FluxTables\Concretes\Column\ComponentColumn::make('name')
->label('Username')
->sortable()
->searchable()
->component('columns.user-name-input')
->property('name'),
\Idkwhoami\FluxTables\Concretes\Column\DatetimeColumn::make('created')
->humanReadable()
->label("Created")
->sortable()
->property('created_at'),
\Idkwhoami\FluxTables\Concretes\Column\TextColumn::make('posts')
->count()
->label('Posts')
->relation('posts')
->property('posts_count'),
\Idkwhoami\FluxTables\Concretes\Column\DatetimeColumn::make('email_verified')
->label("Email Verified At")
->sortable()
->property('email_verified_at'),
\Idkwhoami\FluxTables\Concretes\Column\BooleanColumn::make('banned')
->label('Banned')
->property('banned'),
\Idkwhoami\FluxTables\Concretes\Column\DatetimeColumn::make('deleted')
->label("Deleted")
->default('n/a')
->property('deleted_at'),
\Idkwhoami\FluxTables\Concretes\Column\ActionColumn::make('actions')
->actions([
Idkwhoami\FluxTables\Abstracts\Action\ModalAction::make('open')
->label('Open')
->icon('arrow-top-right-on-square')
->link()
->component('user-delete-confirmation'),
Idkwhoami\FluxTables\Abstracts\Action\DirectAction::make('delete')
->visible(fn(\Illuminate\Database\Eloquent\Model $model) => auth()->user()->isNot($model) && !$model->deleted_at)
->label('Delete')
->icon('trash-2')
->operation(\Idkwhoami\FluxTables\Concretes\Operation\DeleteOperation::make('delete')),
Idkwhoami\FluxTables\Abstracts\Action\DirectAction::make('restore')
->visible(fn(\Illuminate\Database\Eloquent\Model $model) => auth()->user()->isNot($model) && $model->deleted_at)
->label('Restore')
->icon('rotate-ccw')
->operation(\Idkwhoami\FluxTables\Concretes\Operation\RestoreOperation::make('restore')),
]),
];<livewire:flux-simple-table
create="user-create-form"
title="Users"
:model="\App\Models\User::class"
:default-toggled-columns="['created']"
:$filters
:$columns
/>Base class for all table actions.
Methods:
label(string $label): static- Set the action labelicon(string $icon): static- Set the action icon (Lucide icon name)variant(?string $variant): static- Set the action variant (default, danger, outline, filled, primary, ghost, subtle)visible(Closure|bool $visible): static- Set visibility conditionaccess(Closure|bool $access): static- Set access control conditionlink(bool $link = true): static- Render as link instead of buttonrender(mixed $id): string|HtmlString|View|null- Abstract method to render the action
Base class for all table columns.
Methods:
label(?string $label): static- Set the column labelsortable(bool $sortable = true): static- Make column sortablesearchable(bool $searchable = true): static- Make column searchabletoggleable(bool $toggleable): static- Make column toggleablevisible(bool|Closure $visible = true): static- Set visibility conditionrender(object $value): string|HtmlString|View|null- Abstract method to render the column
Extends Column for property-based columns.
Methods:
property(string $property): static- Set the model propertyrelation(string $relation): static- Set the relation namecount(bool $count = true): static- Count relation itemsdefault(mixed $default): static- Set default valuetransform(Closure $transform): static- Transform the value before rendering
Base class for all table filters.
Methods:
label(string $label): static- Set the filter labeldefault(mixed $default): static- Set default valuevisible(Closure|bool $visible): static- Set visibility conditionapply(Builder $query): void- Abstract method to apply filter to querycomponent(): string- Abstract method to return Livewire component namerenderPill(): string|HtmlString|View- Abstract method to render filter pill
Base class for table configuration.
Methods:
columns(array $columns): static- Set table columnsfilters(array $filters): static- Set table filtersgetColumns(): array- Get visible columnsgetFilters(): array- Get visible filtersgetColumn(string $key): Column- Get specific column by key
Simple text column for displaying string values.
Column for displaying dates and times.
Methods:
format(string $format): static- Set date format (default: 'm/d/Y H:i:s')humanReadable(bool $readable = true): static- Display as human-readable format (e.g., "2 hours ago")
Column for displaying boolean values as badges or icons.
PostgreSQL-specific column for querying JSON data.
Methods:
path(array|string $path): static- Set JSON path (e.g., 'user.name' or ['user', 'name'])type(JsonPropertyType $type): static- Set PostgreSQL cast type (text, integer, boolean, etc.)
Column that renders a custom Blade component.
Methods:
component(string $component): static- Set the component name
Column for displaying row actions.
Methods:
actions(array $actions): static- Set the actions array
Column for displaying arrays or collections as lists.
Filter for boolean values with true/false/all options.
Filter for date ranges with start and end date inputs.
Methods:
property(string $property): static- Set the date property to filter
Filter for soft-deleted models with options for all/only deleted/without deleted.
Filter with predefined options in a select dropdown.
Methods:
options(array $options): static- Set the available options
Filter to show/hide records based on whether a field has a value.
Methods:
property(string $property): static- Set the property to checkdescription(string $description): static- Set filter descriptionpillContent(string $content): static- Set the pill display text
Action that executes immediately when clicked.
Methods:
operation(Operation $operation): static- Set the operation to execute
Action that opens a modal component.
Methods:
component(string $component): static- Set the modal component name
Operation for soft-deleting models.
Operation for restoring soft-deleted models.
Operation for redirecting to a route.
Methods:
route(string $route): static- Set the route nameparameters(array $parameters): static- Set route parameters
Core trait for Eloquent-based tables.
Methods:
table(string $model, array $columns, array $filters): Table- Configure the tablegetQuery(): Builder- Get the base queryoptimizeSelects(bool $optimize): static- Enable/disable select optimizationapplyRelations(Builder $query): void- Apply relation joinsapplyColumns(Builder $query): void- Apply column selections
Adds filtering functionality.
Methods:
applyFilters(Builder $query): void- Apply active filters to querygetFilters(): array- Get all filtersgetActiveFilters(): array- Get currently active filtersresetFilter(string $filter): void- Reset specific filterresetFilters(): void- Reset all filtershasActiveFilters(): bool- Check if any filters are active
Adds sorting functionality with PostgreSQL JSON support.
Properties:
?string $sortingColumn- Current sort column?string $sortingDirection- Current sort direction
Methods:
applySorting(Builder $query): void- Apply sorting to querysort(string $column): void- Sort by column (cycles through asc/desc/none)resetSorting(): void- Reset sortinggetSortingColumn(): string- Get current sort columngetSortingDirection(): string- Get current sort directiondefaultSortingColumn(): string- Default sort columndefaultSortingDirection(): string- Default sort direction
Adds global search functionality.
Properties:
string $search- Current search term
Methods:
applySearch(Builder $query): void- Apply search to queryresetSearch(): void- Clear search
Adds row-level actions functionality.
Methods:
getActions(): array- Get all actionsexecuteAction(string $action, mixed $id): void- Execute an action
Adds column visibility toggling.
Methods:
toggleColumn(string $column): void- Toggle column visibilitygetToggledColumns(): array- Get currently visible columnsdefaultToggledColumns(): array- Default visible columns
Adds configurable pagination.
Methods:
getPaginationOptions(): array- Get pagination size optionsdefaultPaginationValue(): int- Get default pagination size
All- Show all recordsOnlyDeleted- Show only soft-deleted recordsWithoutDeleted- Show only non-deleted records
PostgreSQL cast types for JSON properties:
Text- Cast as textInteger- Cast as integerBoolean- Cast as booleanNumeric- Cast as numeric
Pre-built table component with all features enabled.
Properties:
string $title- Table titlestring $model- Eloquent model classarray $columns- Column definitionsarray $filters- Filter definitions?string $create- Create component namearray $defaultToggledColumns- Initially visible columns
Here's how to create a custom table component using the modular trait system. This example demonstrates building a comprehensive table with all available features:
<?php
namespace App\Livewire;
use App\Models\User;
use Idkwhoami\FluxTables\Abstracts\Table\Table;
use Idkwhoami\FluxTables\Concretes\Table\EloquentTable;
use Idkwhoami\FluxTables\Traits\HasEloquentTable;
use Idkwhoami\FluxTables\Traits\HasFilters;
use Idkwhoami\FluxTables\Traits\HasSearch;
use Idkwhoami\FluxTables\Traits\HasSorting;
use Idkwhoami\FluxTables\Traits\HasActions;
use Idkwhoami\FluxTables\Traits\HasToggleableColumns;
use Idkwhoami\FluxTables\Traits\HasDynamicPagination;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Computed;
use Livewire\Component;
use Livewire\WithPagination;
class CustomUserTable extends Component
{
use HasEloquentTable;
use HasFilters;
use HasSorting;
use HasSearch;
use HasActions;
use HasToggleableColumns;
use HasDynamicPagination;
use WithPagination;
public string $search = '';
public function mount(): void
{
// Livewire automatically calls trait mount methods
// No manual initialization needed
}
public function render(): View
{
return view('livewire.custom-user-table');
}
#[Computed]
public function models(): LengthAwarePaginator
{
return $this->getQuery()->paginate($this->getPaginationValue());
}
public function getQuery(): Builder
{
$query = User::query();
// Apply the modular functionality
$this->applySearch($query);
$this->applyFilters($query);
$this->applySorting($query);
$this->applyRelations($query);
$this->applyColumns($query);
return $query;
}
protected function getColumns(): array
{
return [
\Idkwhoami\FluxTables\Concretes\Column\TextColumn::make('name')
->label('Name')
->property('name')
->searchable()
->sortable(),
\Idkwhoami\FluxTables\Concretes\Column\TextColumn::make('email')
->label('Email')
->property('email')
->searchable()
->sortable(),
\Idkwhoami\FluxTables\Concretes\Column\ComponentColumn::make('avatar')
->label('Avatar')
->component('columns.user-avatar')
->property('avatar_url')
->toggleable(false), // Always visible
\Idkwhoami\FluxTables\Concretes\Column\TextColumn::make('posts')
->count()
->label('Posts')
->relation('posts')
->property('posts_count')
->sortable(),
\Idkwhoami\FluxTables\Concretes\Column\DatetimeColumn::make('created_at')
->label('Created')
->property('created_at')
->humanReadable()
->sortable(),
\Idkwhoami\FluxTables\Concretes\Column\DatetimeColumn::make('email_verified_at')
->label('Email Verified')
->property('email_verified_at')
->default('Not verified')
->sortable(),
\Idkwhoami\FluxTables\Concretes\Column\BooleanColumn::make('banned')
->label('Banned')
->property('banned')
->sortable(),
\Idkwhoami\FluxTables\Concretes\Column\JsonColumn::make('preferences')
->label('Theme')
->property('preferences')
->path(['ui', 'theme'])
->type(\Idkwhoami\FluxTables\Enums\JsonPropertyType::Text)
->default('default'),
\Idkwhoami\FluxTables\Concretes\Column\DatetimeColumn::make('deleted_at')
->label('Deleted')
->property('deleted_at')
->default('n/a')
->sortable(),
\Idkwhoami\FluxTables\Concretes\Column\ActionColumn::make('actions')
->label('Actions')
->actions([
\Idkwhoami\FluxTables\Abstracts\Action\ModalAction::make('edit')
->label('Edit')
->icon('pencil')
->link()
->component('user-edit-modal'),
\Idkwhoami\FluxTables\Abstracts\Action\ModalAction::make('view')
->label('View Details')
->icon('eye')
->variant('outline')
->component('user-details-modal'),
\Idkwhoami\FluxTables\Abstracts\Action\DirectAction::make('ban')
->visible(fn(\Illuminate\Database\Eloquent\Model $model) =>
auth()->user()->isNot($model) && !$model->banned && !$model->deleted_at)
->label('Ban')
->icon('user-x')
->variant('danger')
->operation(\Idkwhoami\FluxTables\Concretes\Operation\RouteOperation::make('ban')
->route('admin.users.ban')
->parameters(['user' => fn($model) => $model->id])),
\Idkwhoami\FluxTables\Abstracts\Action\DirectAction::make('delete')
->visible(fn(\Illuminate\Database\Eloquent\Model $model) =>
auth()->user()->isNot($model) && !$model->deleted_at)
->label('Delete')
->icon('trash-2')
->variant('danger')
->operation(\Idkwhoami\FluxTables\Concretes\Operation\DeleteOperation::make('delete')),
\Idkwhoami\FluxTables\Abstracts\Action\DirectAction::make('restore')
->visible(fn(\Illuminate\Database\Eloquent\Model $model) =>
auth()->user()->isNot($model) && $model->deleted_at)
->label('Restore')
->icon('rotate-ccw')
->operation(\Idkwhoami\FluxTables\Concretes\Operation\RestoreOperation::make('restore')),
]),
];
}
protected function getFilters(): array
{
return [
\Idkwhoami\FluxTables\Concretes\Filter\DeletedFilter::make('deleted')
->label('Deletion State')
->default(\Idkwhoami\FluxTables\Enums\DeletionState::WithoutDeleted->value),
\Idkwhoami\FluxTables\Concretes\Filter\ValuePresentFilter::make('verified')
->label('Email Verified')
->property('email_verified_at')
->description('Show only verified users')
->pillContent('Verified only'),
\Idkwhoami\FluxTables\Concretes\Filter\BooleanFilter::make('banned')
->label('Banned Status')
->property('banned'),
\Idkwhoami\FluxTables\Concretes\Filter\DateRangeFilter::make('created')
->label('Registration Date')
->property('created_at'),
\Idkwhoami\FluxTables\Concretes\Filter\SelectFilter::make('role')
->label('User Role')
->property('role')
->options([
'admin' => 'Administrator',
'moderator' => 'Moderator',
'user' => 'Regular User',
]),
];
}
// Required by HasSorting trait
public function defaultSortingColumn(): string
{
return 'created_at';
}
public function defaultSortingDirection(): string
{
return 'desc';
}
// Required by HasToggleableColumns trait
public function defaultToggledColumns(): array
{
return ['name', 'email', 'created_at', 'actions']; // Default visible columns
}
// Required by HasDynamicPagination trait
public function defaultPaginationValue(): int
{
return 15;
}
public function getPaginationOptions(): array
{
return [10, 15, 25, 50, 100];
}
}Create resources/views/livewire/custom-user-table.blade.php:
<div class="space-y-4">
{{-- Header with title, search, filters, and controls --}}
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold">Users Management</h2>
<div class="flex items-center space-x-3">
{{-- Search Input --}}
<div class="w-64">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="Search users..."
icon="search"
clearable
/>
</div>
{{-- Pagination Size Selector --}}
<flux:select
wire:model.live="paginationValue"
placeholder="Per page"
class="w-24"
>
@foreach($this->getPaginationOptions() as $option)
<flux:option value="{{ $option }}">{{ $option }}</flux:option>
@endforeach
</flux:select>
{{-- Column Toggle Button --}}
<flux:modal.trigger name="columns-modal">
<flux:button variant="outline" icon="columns">
Columns
</flux:button>
</flux:modal.trigger>
{{-- Filter Button --}}
@if($this->table->hasFilters())
<flux:modal.trigger name="filters-modal">
<flux:button variant="outline" icon="filter">
Filters
@if($this->hasActiveFilters())
<flux:badge size="sm" color="blue">{{ count($this->getActiveFilters()) }}</flux:badge>
@endif
</flux:button>
</flux:modal.trigger>
@endif
{{-- Create User Button --}}
<flux:modal.trigger name="create-user-modal">
<flux:button icon="plus">
Add User
</flux:button>
</flux:modal.trigger>
</div>
</div>
{{-- Active Filter Pills --}}
@if($this->hasActiveFilters())
<div class="flex flex-wrap gap-2">
@foreach($this->getActiveFilters() as $filter)
<flux:badge
color="blue"
size="sm"
wire:click="resetFilter('{{ $filter->getName() }}')"
class="cursor-pointer hover:bg-blue-600"
>
{{ $filter->renderPill() }}
<flux:icon.x-mark class="w-3 h-3 ml-1" />
</flux:badge>
@endforeach
<flux:button
variant="ghost"
size="sm"
wire:click="resetFilters"
class="text-gray-500 hover:text-gray-700"
>
Clear all filters
</flux:button>
</div>
@endif
{{-- Table --}}
<flux:table :paginate="$this->models">
<flux:columns>
@foreach($this->getToggledColumns() as $column)
<flux:column
:sortable="$column->isSortable()"
:sorted="$this->getSortingColumn() === $column->getName() ? $this->getSortingDirection() : null"
wire:click="sort('{{ $column->getName() }}')"
class="cursor-pointer hover:bg-gray-50"
>
<div class="flex items-center space-x-1">
<span>{{ $column->getLabel() }}</span>
@if($column->isSortable())
@if($this->getSortingColumn() === $column->getName())
@if($this->getSortingDirection() === 'asc')
<flux:icon.chevron-up class="w-4 h-4" />
@else
<flux:icon.chevron-down class="w-4 h-4" />
@endif
@else
<flux:icon.chevron-up-down class="w-4 h-4 text-gray-400" />
@endif
@endif
</div>
</flux:column>
@endforeach
</flux:columns>
<flux:rows>
@foreach($this->models as $user)
<flux:row :key="$user->id" class="hover:bg-gray-50">
@foreach($this->getToggledColumns() as $column)
<flux:cell>
{!! $column->render($user) !!}
</flux:cell>
@endforeach
</flux:row>
@endforeach
</flux:rows>
</flux:table>
{{-- Column Toggle Modal --}}
<flux:modal name="columns-modal" class="md:w-96">
<div class="space-y-6">
<div>
<flux:heading size="lg">Toggle Columns</flux:heading>
<flux:subheading>Show or hide table columns</flux:subheading>
</div>
<div class="space-y-3">
@foreach($this->table->getColumns() as $column)
@if($column->isToggleable())
<flux:checkbox
wire:model.live="toggledColumns.{{ $column->getName() }}"
label="{{ $column->getLabel() }}"
/>
@endif
@endforeach
</div>
<div class="flex justify-end space-x-2">
<flux:modal.close>
<flux:button variant="ghost">Close</flux:button>
</flux:modal.close>
</div>
</div>
</flux:modal>
{{-- Filter Modal --}}
@if($this->table->hasFilters())
<flux:modal name="filters-modal" class="md:w-96">
<div class="space-y-6">
<div>
<flux:heading size="lg">Filters</flux:heading>
<flux:subheading>Refine your search results</flux:subheading>
</div>
@foreach($this->getFilters() as $filter)
<div>
<livewire:dynamic-component
:component="$filter->component()"
:filter="$filter"
:key="$filter->getName()"
/>
</div>
@endforeach
<div class="flex justify-end space-x-2">
<flux:modal.close>
<flux:button variant="ghost">Close</flux:button>
</flux:modal.close>
<flux:button wire:click="resetFilters" variant="danger">
Clear All Filters
</flux:button>
</div>
</div>
</flux:modal>
@endif
{{-- Create User Modal (placeholder) --}}
<flux:modal name="create-user-modal" class="md:w-2xl">
<div class="space-y-6">
<div>
<flux:heading size="lg">Create New User</flux:heading>
<flux:subheading>Add a new user to the system</flux:subheading>
</div>
{{-- This would contain your user creation form --}}
<div class="text-center py-8 text-gray-500">
<flux:icon.user-plus class="w-12 h-12 mx-auto mb-4" />
<p>User creation form would go here</p>
<p class="text-sm">Implement your user creation component</p>
</div>
<div class="flex justify-end space-x-2">
<flux:modal.close>
<flux:button variant="ghost">Cancel</flux:button>
</flux:modal.close>
<flux:button>Create User</flux:button>
</div>
</div>
</flux:modal>
</div>Add to your AppServiceProvider or create a dedicated service provider:
use Livewire\Livewire;
public function boot(): void
{
Livewire::component('custom-user-table', CustomUserTable::class);
}<livewire:custom-user-table />This comprehensive example showcases all available features:
- Complete Trait Integration: All major traits working together (HasEloquentTable, HasFilters, HasSorting, HasSearch, HasActions, HasToggleableColumns, HasDynamicPagination)
- Rich Column Types: Text, Component, DateTime, Boolean, JSON, and Action columns with various configurations
- Advanced Filtering: Multiple filter types including DeletedFilter, ValuePresentFilter, BooleanFilter, DateRangeFilter, and SelectFilter
- Interactive Actions: Both modal actions (edit, view details) and direct actions (ban, delete, restore) with conditional visibility
- Dynamic UI Controls: Column toggles, pagination size selection, search functionality, and filter management
- PostgreSQL Features: JSON column with path querying and type casting
- User Experience: Hover effects, sorting indicators, filter pills, and responsive modals
- Modularity: Pick and choose only the traits you need for simpler implementations
- Full Feature Set: This example shows how all features work together seamlessly
- Customization: Complete control over UI, behavior, and data presentation
- Performance: Optimized queries with relation handling, select optimization, and efficient pagination
- Flexibility: Easy to extend, modify, or simplify based on your specific requirements
- Reusability: Use this pattern across different models and use cases
- Maintainability: Clear separation of concerns with trait-based architecture
You can adapt this example by:
- Simplifying: Remove traits you don't need (e.g., remove HasActions for read-only tables)
- Extending: Add custom column types by extending the abstract Column class
- Custom Filters: Build specialized filters by extending the abstract Filter class
- Custom Operations: Create new operations for DirectAction beyond the provided ones
- UI Modifications: Customize the Blade template for your design system
- Model-Specific Features: Add model-specific logic in visibility conditions and transformations
- Integration: Connect with your existing authentication, authorization, and business logic
This modular approach allows you to start with this comprehensive example and adapt it to your exact needs, whether you want a full-featured admin interface or a simple data display table.
- Nothing for now
Please see CHANGELOG for more information on what has changed recently.
Please see fork the project and adjust as much as u want to. But please dont expect me to answer to any PR or Issues.
The MIT License (MIT). Please see License File for more information.