devaction-labs/livewire-filterable

A Laravel Livewire package for elegant, reactive model filtering with zero boilerplate

Installs: 1

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/devaction-labs/livewire-filterable

v1.0.0.0 2026-01-27 21:19 UTC

This package is auto-updated.

Last update: 2026-01-27 21:22:02 UTC


README

Livewire Filterable

GitHub Workflow Status Total Downloads Latest Version License

A Laravel Livewire package for elegant, reactive model filtering with zero boilerplate. No more manual when() clauses!

✨ Features

  • 🎯 Zero Boilerplate - No manual when() clauses needed
  • Reactive - Filters update automatically with Livewire
  • 🔥 Elegant API - Same simple syntax: Filter::like('name')
  • 🚀 PHP 8.5+ - Uses cutting-edge PHP features
    • #[\NoDiscard] attribute on all fluent methods (30+ methods)
    • Pipe operator |> for data transformation pipelines
    • Laravel 12's #[Scope] attribute for cleaner scopes
  • 📦 Laravel 11 & 12 - Full support
  • 🎨 Livewire 4 - Built for the latest version
  • 🔍 Full-Text Search - PostgreSQL native support with GIN indexes
  • 🔗 Relationships - Filter with whereAny, whereAll, whereNone
  • 📅 Smart Dates - Carbon integration with startOfDay/endOfDay
  • 🗄️ JSON Fields - Database-agnostic JSON filtering
  • 🎭 Type-Safe - Full PHP 8.5 type coverage

📦 Installation

composer require devaction-labs/livewire-filterable

Requirements:

  • PHP 8.5+
  • Laravel 11 or 12
  • Livewire 4

Note: PHP 8.5 is currently available on Linux. macOS and Windows support will be available when PHP 8.5 stable builds are released for those platforms.

🚀 Quick Start

1. Add traits to your Model

namespace App\Models;

use DevactionLabs\LivewireFilterable\Traits\Filterable;
use DevactionLabs\LivewireFilterable\Concerns\HasCustomPagination;
use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
    use Filterable;
    use HasCustomPagination;
}

2. Create a Livewire Component

namespace App\Livewire;

use App\Models\Customer;
use DevactionLabs\LivewireFilterable\Concerns\LivewireFilterable;
use DevactionLabs\LivewireFilterable\Filter;
use Livewire\Component;

class CustomerList extends Component
{
    use LivewireFilterable;

    // Public properties = automatic filters!
    public string $name = '';
    public string $legal_name = '';
    public string $email = '';
    public string $tax_id = '';
    public string $tax_id_type = '';
    public int $tenant_id = 0;

    public function render()
    {
        $customers = Customer::query()
            ->with('tenant')
            ->filterable([  // Same elegant API!
                Filter::ilike('name'),
                Filter::ilike('legal_name'),
                Filter::ilike('email'),
                Filter::ilike('tax_id'),
                Filter::exact('tax_id_type'),
                Filter::exact('tenant_id'),
            ])
            ->orderBy('name')
            ->customPaginate();

        return view('livewire.customer-list', compact('customers'));
    }
}

3. Create the Blade View

<div>
    {{-- Filters automatically bind to public properties --}}
    <input wire:model.live="name" type="text" placeholder="Nome">
    <input wire:model.live="legal_name" type="text" placeholder="Razão Social">
    <input wire:model.live="email" type="text" placeholder="Email">
    <input wire:model.live="tax_id" type="text" placeholder="CPF/CNPJ">

    <select wire:model.live="tax_id_type">
        <option value="">Tipo</option>
        <option value="cpf">CPF</option>
        <option value="cnpj">CNPJ</option>
    </select>

    {{-- Results --}}
    <div class="grid gap-4">
        @foreach($customers as $customer)
            <div class="border p-4 rounded">
                <h3>{{ $customer->name }}</h3>
                <p>{{ $customer->email }}</p>
            </div>
        @endforeach
    </div>

    {{ $customers->links() }}
</div>

That's it! 🎉 No when() clauses, filters are applied automatically!

📚 Documentation

🔄 Paginação

O pacote suporta 3 tipos de paginação:

// 1. Padrão (com total count)
->customPaginate('paginate', 15)

// 2. Simples (sem total - mais rápido)
->customPaginate('simple', 20)

// 3. Cursor (mais performático - ideal para datasets grandes)
->customPaginate('cursor', 25)

Controle dinâmico via Livewire:

class CustomerList extends Component
{
    use LivewireFilterable;

    public string $name = '';
    public int $perPage = 15;  // ✅ Dinâmico!

    public function render()
    {
        $customers = Customer::query()
            ->filterable([Filter::ilike('name')])
            ->customPaginate('cursor', $this->perPage);  // ✅ Usa a propriedade
    }
}
{{-- Usuário pode escolher --}}
<select wire:model.live="perPage">
    <option value="10">10 por página</option>
    <option value="25">25 por página</option>
    <option value="50">50 por página</option>
    <option value="100">100 por página</option>
</select>

Ver mais sobre paginação →

📖 Available Filters

Basic Comparison Filters

Filter::exact('status')              // WHERE status = ?
Filter::notEquals('status')          // WHERE status != ?
Filter::gt('amount')                 // WHERE amount > ?
Filter::gte('amount')                // WHERE amount >= ?
Filter::lt('amount')                 // WHERE amount < ?
Filter::lte('amount')                // WHERE amount <= ?
Filter::between('created_at')        // WHERE created_at BETWEEN ? AND ?

Text Search Filters

Filter::like('name')                 // WHERE name LIKE %?%
Filter::ilike('email')               // Case-insensitive (database-specific)
Filter::notLike('description')       // WHERE description NOT LIKE %?%
Filter::startsWith('sku')            // WHERE sku LIKE ?%
Filter::endsWith('domain')           // WHERE domain LIKE %?

Full-Text Search

// PostgreSQL native with GIN index support
Filter::fullText(['title', 'content'], 'search')
    ->setFullTextLanguage('portuguese')
    ->setFullTextPrefixMatch(true)

Array Filters

Filter::in('category_id')            // WHERE category_id IN (?)
Filter::notIn('status')              // WHERE status NOT IN (?)

Null Filters

Filter::isNull('deleted_at')         // WHERE deleted_at IS NULL
Filter::isNotNull('verified_at')     // WHERE verified_at IS NOT NULL

Relationship Filters

// Simple relationship
Filter::relationship('category', 'slug', '=', 'category')
    ->with()

// OR logic (whereAny)
Filter::relationship('tags', 'name')
    ->whereAny([
        ['name', '=', 'featured'],
        ['name', '=', 'sale'],
    ])
    ->with()

// AND logic (whereAll)
Filter::relationship('permissions', 'name')
    ->whereAll([
        ['name', '=', 'edit-posts'],
        ['is_active', '=', true],
    ])
    ->with()

// NOT logic (whereNone)
Filter::relationship('tags', 'is_banned')
    ->whereNone([
        ['is_banned', '=', true],
    ])
    ->with()

JSON Field Filters

Filter::json('attributes', 'color', '=', 'color')
    ->setDatabaseDriver('pgsql')

Filter::json('metadata', 'specs.weight', '>', 'min_weight')
    ->setDatabaseDriver('mysql')

🎯 Custom Property Names

If your Livewire property name differs from the database column:

public string $searchName = '';
public string $customerEmail = '';

// Second parameter = Livewire property name
Filter::ilike('name', 'searchName')
Filter::ilike('email', 'customerEmail')

📅 Date Handling

public string $created_date = '';

Filter::exact('created_at', 'created_date')
    ->castDate()
    ->endOfDay()  // Sets time to 23:59:59

🔗 URL Persistence

Use Livewire's #[Url] attribute to persist filters in the URL:

use Livewire\Attributes\Url;

class CustomerList extends Component
{
    use LivewireFilterable;

    #[Url]
    public string $name = '';

    #[Url]
    public string $email = '';

    #[Url(as: 'type')]
    public string $tax_id_type = '';
}

Now filters appear in URL: ?name=john&email=test@&type=cpf

⏱️ Debouncing

Add debouncing to specific inputs in Blade:

<input wire:model.live.debounce.500ms="name" type="text">

Or programmatically:

Filter::like('name')->debounce(500)  // 500ms

🧹 Clear Filters

public function clearFilters(): void
{
    $this->reset(['name', 'email', 'tax_id']);
}

In Blade:

<button wire:click="clearFilters">Limpar Filtros</button>

🎨 Complete Example

class ProductList extends Component
{
    use LivewireFilterable;

    #[Url] public string $search = '';
    #[Url] public array $price_range = [];
    #[Url] public ?string $category = null;
    #[Url] public array $tags = [];
    #[Url] public string $created_date = '';

    public function render()
    {
        $products = Product::query()
            ->filterable([
                // Full-text search
                Filter::fullText(['name', 'description'], 'search')
                    ->setFullTextLanguage('portuguese'),

                // Price range
                Filter::between('price', 'price_range'),

                // Category relationship
                Filter::relationship('category', 'slug', '=', 'category')
                    ->with(),

                // Tags with OR logic
                Filter::relationship('tags', 'name', 'IN', 'tags')
                    ->with(),

                // Date filter
                Filter::exact('created_at', 'created_date')
                    ->castDate()
                    ->endOfDay(),
            ])
            ->orderBy('created_at', 'desc')
            ->customPaginate('paginate', 20);

        return view('livewire.product-list', compact('products'));
    }

    public function clearFilters(): void
    {
        $this->reset(['search', 'price_range', 'category', 'tags', 'created_date']);
    }
}

🗄️ Database-Specific Features

PostgreSQL Full-Text Search with GIN Index

// Migration
Schema::table('products', function (Blueprint $table) {
    $table->tsvector('search_vector')->nullable();
});

DB::statement('CREATE INDEX products_search_idx ON products USING GIN(search_vector)');

// Update trigger
DB::statement("
    CREATE TRIGGER products_search_update
    BEFORE INSERT OR UPDATE ON products
    FOR EACH ROW EXECUTE FUNCTION
    tsvector_update_trigger(search_vector, 'pg_catalog.portuguese', name, description);
");

// Filter
Filter::fullText('search_vector', 'q')
    ->useTsVector()
    ->setDatabaseDriver('pgsql')

Performance: 5ms vs 500ms on 1M rows (100x faster!)

Case-Insensitive Search (ILIKE)

Automatically adapts to your database:

  • PostgreSQL: Native ILIKE
  • MySQL: LOWER() function
  • SQLite: Standard LIKE (case-insensitive by default)
Filter::ilike('email')  // Works on all databases!

🧪 Testing

composer test

Run specific test suites:

composer test:unit
composer test:types
composer test:lint

📝 License

The MIT License (MIT). Please see License File for more information.

🤝 Contributing

Please see CONTRIBUTING.md for details.

🙏 Credits

Livewire Filterable - Elegant filtering for Laravel Livewire 🚀