nazalas / laravel-persistent-table-filters
Persist and restore table filter, sort, and search state per user in Laravel applications.
Package info
github.com/Nazalas/laravel-persistent-table-filters
pkg:composer/nazalas/laravel-persistent-table-filters
Requires
- php: ^8.2
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- pestphp/pest: ^2.0|^3.0
- pestphp/pest-plugin-laravel: ^2.0|^3.0
README
Automatically persist and restore table filter, sort, and search state per user in Laravel applications. Works great with Livewire.
The Problem
Users refine a table — searching, sorting, filtering — then navigate away or refresh. Everything resets. This package silently saves filter state as users interact and restores it automatically on the next visit. Optionally, users can also save named filter presets and switch between them.
Features
- Auto-persist — silently saves filter state on every change; restores it on mount with no user interaction
- Named presets — users can save, label, and reload specific filter configurations
- Default preset — mark one named preset as the default fallback
- Per-user, per-resource — state is scoped to the authenticated user and a resource key you define
- Opt-out — set
$autoSaveFilters = falseon any component to disable auto-persist
Installation
composer require nazalas/laravel-persistent-table-filters
Publish and run the migration:
php artisan vendor:publish --tag="persistent-table-filters-migrations"
php artisan migrate
Optionally publish the config:
php artisan vendor:publish --tag="persistent-table-filters-config"
Usage
Livewire Component
Add the HasPersistentFilters trait, define a resource key and which properties make up your filter state:
use Nazalas\PersistentTableFilters\Traits\HasPersistentFilters; class CampaignIndex extends Component { use HasPersistentFilters; // Unique key for this table — scopes saved state per resource protected string $filterResource = 'campaigns'; // Which component properties make up the filter state protected array $filterKeys = ['search', 'sort_by', 'sort_dir', 'status']; public string $search = ''; public string $sort_by = 'name'; public string $sort_dir = 'asc'; public string $status = ''; public function mount(): void { // Restore last state (auto-persisted or named default) $this->restoreFilters(); } public function updatedSearch(): void { $this->resetPage(); $this->persistFilters(); // silently save on every change } public function updatedStatus(): void { $this->resetPage(); $this->persistFilters(); } }
That's the core usage. Filters now survive page refreshes automatically with no user action needed.
Disabling Auto-Persist
Set $autoSaveFilters = false on any component to prevent persistFilters() from writing to the database:
protected bool $autoSaveFilters = false;
Clearing Persisted State
$this->clearPersistedFilters();
Wipes the auto-persisted record for the current user + resource. The next page load will start fresh (or fall back to a named default if one exists).
Named Presets (Optional)
On top of auto-persist, users can save and reload named filter configurations.
Saving a Preset
// Save current state with a label $filter = $this->saveFilter('Active campaigns'); // Save and mark as the default fallback $filter = $this->saveFilter('My default view', setAsDefault: true);
Loading a Preset
$this->loadFilter($filterId); // Also updates the auto-persisted state
Deleting a Preset
$this->deleteFilter($filterId);
Listing Presets
$filters = $this->getSavedFilters(); // Collection of TableFilter — excludes auto records
Example Blade UI for Presets
<div x-data="{ saving: false, label: '' }"> @if($this->getSavedFilters()->count()) <select wire:change="loadFilter($event.target.value)"> <option value="">Load saved filter...</option> @foreach($this->getSavedFilters() as $filter) <option value="{{ $filter->id }}"> {{ $filter->label }}{{ $filter->is_default ? ' ★' : '' }} </option> @endforeach </select> @endif <div x-show="saving"> <input x-model="label" type="text" placeholder="Filter name" /> <button @click="$wire.saveFilter(label); saving = false">Save</button> </div> <button @click="saving = true">Save current filters</button> </div>
How Restore Priority Works
restoreFilters() follows this order:
- Auto-persisted state — the last state saved by
persistFilters()(if any) - Named default — a named preset marked
is_default = true(if no auto state) - Component defaults — the property values you declared in the class (if neither exists)
Using Without Livewire
The TableFilter model and scopes work independently of Livewire:
use Nazalas\PersistentTableFilters\Models\TableFilter; // Save state manually TableFilter::updateOrCreate( ['user_id' => auth()->id(), 'resource' => 'campaigns', 'is_auto' => true], ['label' => null, 'filters' => $request->only(['search', 'status', 'sort_by'])] ); // Restore state $state = TableFilter::forCurrentUser() ->forResource('campaigns') ->where('is_auto', true) ->first() ?->filters ?? [];
Configuration
// config/persistent-table-filters.php return [ 'table_name' => 'table_filters', 'user_model' => \App\Models\User::class, 'max_per_user_per_resource' => 20, // named presets only; null = no limit ];
Testing
composer test
Changelog
See CHANGELOG.md.
License
MIT. See LICENSE.