laratribe/laravel-advanced-filters v1.0.0 · 80 products ·
zero build step — Alpine from a CDN, package JS published to public/
Select column
| Name | SKU | Category | Status | Price | Stock | Released |
|---|---|---|---|---|---|---|
| Vertex Mini 1 | SKU-0001 | Books | Low | 8.20 | 13 | 12 Sep 2026 |
| Lumen Max 2 | SKU-0002 | Clothing | Out | 11.90 | 26 | 1 Sep 2026 |
| Atlas Lite 3 | SKU-0003 | Toys | In stock | 15.60 | 39 | 21 Aug 2026 |
| Orbit Pro 4 | SKU-0004 | Electronics | Low | 19.30 | 52 | 10 Aug 2026 |
| Cobalt Mini 5 | SKU-0005 | Books | Out | 23.00 | 65 | 30 Jul 2026 |
| Ember Max 6 | SKU-0006 | Clothing | In stock | 26.70 | 78 | 19 Jul 2026 |
| Quartz Lite 7 | — | Toys | Low | 30.40 | 91 | 8 Jul 2026 |
| Nimbus Pro 8 | SKU-0008 | Electronics | Out | 34.10 | 104 | 27 Jun 2026 |
| Vertex Mini 9 | SKU-0009 | Books | In stock | 37.80 | 117 | 16 Jun 2026 |
| Lumen Max 10 | SKU-0010 | Clothing | Low | 41.50 | 130 | 5 Jun 2026 |
| Atlas Lite 11 | SKU-0011 | Toys | Out | 45.20 | 3 | 25 May 2026 |
| Orbit Pro 12 | SKU-0012 | Electronics | In stock | 48.90 | 16 | 14 May 2026 |
| Cobalt Mini 13 | SKU-0013 | Books | Low | 52.60 | 29 | 3 May 2026 |
| Ember Max 14 | — | Clothing | Out | 56.30 | 42 | 22 Apr 2026 |
| Quartz Lite 15 | SKU-0015 | Toys | In stock | 60.00 | 55 | 11 Apr 2026 |
The model below is the same file the Livewire and JSON API pages use. Only the last few lines differ per page — the filtering itself is declared once.
app/Models/Product.php
identical on all three pages <?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laratribe\AdvancedFilters\Concerns\HasFilters;
use Laratribe\AdvancedFilters\Contracts\Filterable;
use Laratribe\AdvancedFilters\Filters\{DateFilter, NumericFilter, SetFilter, TextFilter};
class Product extends Model implements Filterable
{
use HasFilters;
protected $guarded = [];
public $timestamps = false;
protected function casts(): array
{
return ['price' => 'float', 'stock' => 'integer', 'released_at' => 'date'];
}
/**
* The one place that decides what can be filtered — and therefore the allow-list.
* Anything not declared here can never reach the query, whatever the request says.
*/
public static function filters(): array
{
return [
TextFilter::make('name', 'Name'),
TextFilter::make('sku', 'SKU')->nullable(),
SetFilter::make('category', 'Category')
->options([
'electronics' => 'Electronics',
'books' => 'Books',
'clothing' => 'Clothing',
'toys' => 'Toys',
])
->multiple(),
SetFilter::make('status', 'Status')->options([
'in_stock' => 'In stock',
'low' => 'Low stock',
'out' => 'Out of stock',
]),
NumericFilter::make('price', 'Price'),
NumericFilter::make('stock', 'Stock'),
DateFilter::make('released_at', 'Released'),
];
}
}
routes/web.php
the /blade route <?php
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Laratribe\AdvancedFilters\Support\FilteredQuery;
Route::get('/', fn () => view('home'))->name('demo.home');
// ── Blade + Alpine ───────────────────────────────────────────────────────────
Route::get('/blade', function (Request $request) {
$query = FilteredQuery::for(Product::class)->fromRequest($request);
return view('blade', [
'products' => $query->paginate(15),
'fields' => $query->filterDefinitions(), // wire contract OUT
'active' => $query->activeFilters(), // normalised rows, for the chips
]);
})->name('demo.blade');
// ── Livewire ─────────────────────────────────────────────────────────────────
Route::get('/livewire', fn () => view('livewire-page'))->name('demo.livewire');
// ── JSON API (no UI at all) ──────────────────────────────────────────────────
Route::get('/api', fn () => view('api'))->name('demo.api');
Route::get('/api/products/filters', fn () => response()->json([
'fields' => Product::filterDefinitions(),
]))->name('api.filters');
Route::match(['get', 'post'], '/api/products', fn (Request $request) => response()->json(
FilteredQuery::for(Product::class)->fromRequest($request)->paginate(15)
))->name('api.products');
resources/views/blade.blade.php
this page @extends('layout')
@section('title', 'Blade + Alpine — Advanced Filters')
@section('heading', 'Blade + Alpine')
@section('frontend', 'zero build step — Alpine from a CDN, package JS published to public/')
@push('head')
<script type="module">
import Alpine from 'https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/module.esm.js'
import '{{ asset('vendor/advanced-filters/advanced-filters.js') }}?v={{ filemtime(public_path('vendor/advanced-filters/advanced-filters.js')) }}'
window.Alpine = Alpine
Alpine.start()
</script>
@endpush
@section('content')
<div class="card">
<x-advanced-filters::panel :fields="$fields" :active="$active" :base-url="route('demo.blade')" />
</div>
@include('_table')
@include('_code', [
'intro' => 'The model below is the <strong>same file</strong> the Livewire and JSON API pages use.
Only the last few lines differ per page — the filtering itself is declared once.',
'files' => [
['path' => 'app/Models/Product.php', 'note' => 'identical on all three pages'],
['path' => 'routes/web.php', 'note' => 'the /blade route'],
['path' => 'resources/views/blade.blade.php', 'note' => 'this page'],
],
])
@endsection