Blade + Alpine

laratribe/laravel-advanced-filters v1.0.0 · 80 products · zero build step — Alpine from a CDN, package JS published to public/

NameSKUCategoryStatus PriceStockReleased
Quartz Lite 31 SKU-0031 Toys Low 119.20 123 17 Oct 2025
Nimbus Pro 32 SKU-0032 Electronics Out 122.90 136 6 Oct 2025
Vertex Mini 33 SKU-0033 Books In stock 126.60 9 25 Sep 2025
Lumen Max 34 SKU-0034 Clothing Low 130.30 22 14 Sep 2025
Atlas Lite 35 Toys Out 134.00 35 3 Sep 2025
Orbit Pro 36 SKU-0036 Electronics In stock 137.70 48 23 Aug 2025
Cobalt Mini 37 SKU-0037 Books Low 141.40 61 16 Sep 2026
Ember Max 38 SKU-0038 Clothing Out 145.10 74 5 Sep 2026
Quartz Lite 39 SKU-0039 Toys In stock 148.80 87 25 Aug 2026
Nimbus Pro 40 SKU-0040 Electronics Low 152.50 100 14 Aug 2026
Vertex Mini 41 SKU-0041 Books Out 156.20 113 3 Aug 2026
Lumen Max 42 Clothing In stock 159.90 126 23 Jul 2026
Atlas Lite 43 SKU-0043 Toys Low 163.60 139 12 Jul 2026
Orbit Pro 44 SKU-0044 Electronics Out 167.30 12 1 Jul 2026
Cobalt Mini 45 SKU-0045 Books In stock 171.00 25 20 Jun 2026
Showing 31–45 of 80
The code behind this page 3 files

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