Building a Custom Table Filter for Filament

Learn how to create a custom table filter from scratch, complete with configuration options and integration into your existing Filament resources.

Custom Table Filter for Filament

As a developer working with Laravel and Filament, you’ve probably encountered the need to provide more advanced filtering capabilities for your users. Filament ships with ready-made table filters for common cases such as dropdowns and true/false toggles, but more specific needs like date range selection or searching across several columns call for a custom filter.

If you’re tired of digging through Filament’s documentation or scouring online forums for solutions, this tutorial is for you. You’ll build a custom table filter from scratch, complete with its own configuration options and integration into your existing Filament resources. Specifically, by the end of this guide, you’ll have created a custom date range filter that works seamlessly with your Eloquent model’s data, and be able to apply it to any resource in your application.

Setting Up Filament and the Required Packages

To start building a custom table filter for Filament, you’ll need to set up the necessary packages and create a new project or update an existing one.

First, ensure you have Composer installed on your system. You can verify this by running composer --version in your terminal.

Next, create a new Laravel project using Composer:

composer create-project --prefer-dist laravel/laravel filament-table-filter-example

Alternatively, if you already have an existing Laravel project, simply navigate to its root directory instead:

cd my-existing-laravel-project

Now that we have the base application set up, let’s install Filament itself using Composer. Open your terminal in the project root directory and run:

composer require filament/filament
php artisan filament:install --panels
php artisan make:filament-user

This will install the filament/filament package, which already includes the table builder and its filters, scaffold an admin panel (with its own login page) at /admin, and create a user you can log in with.

Make sure you’re in the correct directory by checking the project name listed above the command prompt or terminal window. You should see something like (filament-table-filter-example)$ indicating you’re currently working with your new Laravel project.

Verify that everything installed correctly by running composer show filament/filament. This will display the installed Filament version and its details; plain composer show lists all packages and their versions in your project.

At this point, you’ve set up the necessary tools to build your custom table filter for Filament.

Creating a Custom Table Filter Component

To create a custom table filter component in Filament, we’ll first create a new PHP file within the app/Filament/Tables/Filters directory. This will house our custom date range filter, which extends Filament’s base Filter class.

// app/Filament/Tables/Filters/DateRangeFilter.php

namespace App\Filament\Tables\Filters;

use Filament\Forms\Components\DatePicker;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\Indicator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;

class DateRangeFilter extends Filter
{
    protected ?string $dateColumn = null;

    protected function setUp(): void
    {
        parent::setUp();

        $this->schema([
            DatePicker::make('from'),
            DatePicker::make('until'),
        ]);

        $this->query(function (Builder $query, array $data): Builder {
            $column = $this->getDateColumn();

            return $query
                ->when(
                    $data['from'] ?? null,
                    fn (Builder $query, $date): Builder => $query->whereDate($column, '>=', $date),
                )
                ->when(
                    $data['until'] ?? null,
                    fn (Builder $query, $date): Builder => $query->whereDate($column, '<=', $date),
                );
        });
    }

    public function dateColumn(string $column): static
    {
        $this->dateColumn = $column;

        return $this;
    }

    public function getDateColumn(): string
    {
        return $this->dateColumn ?? $this->getName();
    }
}

In the code above, we define a DateRangeFilter class that extends Filament’s built-in Filter. Its setUp() method gives the filter a schema with two DatePicker fields and a query() callback that constrains the table’s Eloquent query. By default it filters the column matching the filter’s name, and the dateColumn() method lets you point it at a different column.

Next, let’s show the active date range above the table as removable indicators. Add the following to the end of the setUp() method:

// app/Filament/Tables/Filters/DateRangeFilter.php (inside setUp())

$this->indicateUsing(function (array $data): array {
    $indicators = [];

    if ($data['from'] ?? null) {
        $indicators[] = Indicator::make('From ' . Carbon::parse($data['from'])->toFormattedDateString())
            ->removeField('from');
    }

    if ($data['until'] ?? null) {
        $indicators[] = Indicator::make('Until ' . Carbon::parse($data['until'])->toFormattedDateString())
            ->removeField('until');
    }

    return $indicators;
});

Filament renders the filter’s fields inside the table’s filter dropdown automatically, so no Blade template is needed for our component.

Defining the Filter Logic and Configuration

In this step, we’ll define the filter logic and configure it to work seamlessly with our custom table filter component. Create a new file FilterLogic.php within your app/Services directory:

// app/Services/FilterLogic.php

namespace App\Services;

use App\Filament\Tables\Filters\DateRangeFilter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter;
use Illuminate\Database\Eloquent\Builder;

class FilterLogic
{
    public function getFilters(): array
    {
        return [
            TernaryFilter::make('active')
                ->label('Active')
                ->queries(
                    true: fn (Builder $query) => $query->where('is_active', true),
                    false: fn (Builder $query) => $query->where('is_active', false),
                    blank: fn (Builder $query) => $query,
                ),

            SelectFilter::make('status')
                ->options([
                    'pending' => 'Pending',
                    'in_progress' => 'In Progress',
                    'completed' => 'Completed',
                ])
                ->default('pending'),

            DateRangeFilter::make('created_at')
                ->label('Created'),
        ];
    }
}

Here, we’ve defined three filters: a TernaryFilter for the is_active column, a SelectFilter for the status column, and our custom DateRangeFilter for the created_at column. Each filter applies its own constraint to the table’s query, so no separate method is needed to apply them. This assumes your users table has a boolean is_active column and a string status column; add them with a migration if it doesn’t. This will ensure that our custom table filter works as expected when used within the Filament resource.

Next, we’ll plug these filters into a Filament resource.

Implementing the Filter in the Filament Resource

Now that our custom filter is defined and configured, it’s time to implement it within a Filament resource. This is done in the table configuration of the relevant Filament resource.

Let’s generate a resource for our example model (App\Models\User):

php artisan make:filament-resource User

Filament creates an app/Filament/Resources/Users directory containing UserResource.php, the resource pages, a Schemas/UserForm.php class and a Tables/UsersTable.php class. Next, we need to modify our UsersTable class to include the filters we’ve just created. This can be done in the filters() call inside its configure() method:

// app/Filament/Resources/Users/Tables/UsersTable.php

namespace App\Filament\Resources\Users\Tables;

use App\Services\FilterLogic;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;

class UsersTable
{
    public static function configure(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('name')->searchable(),
                TextColumn::make('email')->searchable(),
                IconColumn::make('is_active')->boolean()->label('Active'),
                TextColumn::make('status')->badge(),
                TextColumn::make('created_at')->dateTime()->sortable(),
            ])
            ->filters([
                ...app(FilterLogic::class)->getFilters(),
            ]);
    }
}

With these modifications in place, the filter should now be visible on your Filament user list page.

Configuring the Filter to Work with Your Model’s Data

To make our custom filter work seamlessly with your model’s data, we need to configure it to fetch and display the correct data from the database.

Firstly, let’s update the App\Models\YourModel.php file (replace YourModel with the actual name of your Eloquent model). We’ll add a new method called searchableColumns that returns an array of column names that should be searchable.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class YourModel extends Model
{
    public function searchableColumns(): array
    {
        return [
            'name',
            'email',
            // Add more columns as needed
        ];
    }
}

Next, we’ll create a second custom filter that fetches its search columns from the model. Create app/Filament/Tables/Filters/SearchFilter.php with the following code:

// app/Filament/Tables/Filters/SearchFilter.php

namespace App\Filament\Tables\Filters;

use Filament\Forms\Components\TextInput;
use Filament\Tables\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;

class SearchFilter extends Filter
{
    protected function setUp(): void
    {
        parent::setUp();

        $this->schema([
            TextInput::make('search')->label('Search'),
        ]);

        $this->query(function (Builder $query, array $data): Builder {
            $search = $data['search'] ?? null;

            if (blank($search)) {
                return $query;
            }

            $columns = $query->getModel()->searchableColumns();

            return $query->where(function (Builder $query) use ($columns, $search): void {
                foreach ($columns as $column) {
                    $query->orWhere($column, 'like', '%' . $search . '%');
                }
            });
        });
    }
}

Register it alongside the other filters by adding SearchFilter::make('search') to the array returned by FilterLogic::getFilters() (and importing App\Filament\Tables\Filters\SearchFilter). Because the filter reads the column list from searchableColumns() on the table’s model, this will ensure our filter is fetching the correct data from your model and displaying it in the table.

Testing the Custom Table Filter in Action

To verify that our custom table filter is working correctly, we’ll need to test it on a real-world dataset. For this purpose, let’s assume we have a users table with several records.

Firstly, navigate to your project root and run the following command to seed the database with some sample data using the User factory that ships with Laravel:

php artisan tinker --execute="App\Models\User::factory()->count(10)->create()"

This will populate our users table with 10 sample records. Now that we have some data, let’s go back to our application and access the Filament resource where we implemented our custom filter.

In your browser, navigate to the URL of your application, followed by /admin/users, which should open the Users resource page in Filament. On this page, you’ll find a table displaying all users with their respective details.

Look for the Status column and verify that our custom filter is working as expected. We can apply different filters by opening the filter dropdown (the funnel icon above the table) and changing the Status, Active or Created fields. For example, if we set Active to “Yes”, the table should only display active users.

You can automate the same check with Filament’s Livewire testing helpers:

// tests/Feature/UserFilterTest.php

namespace Tests\Feature;

use App\Filament\Resources\Users\Pages\ListUsers;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;

class UserFilterTest extends TestCase
{
    use RefreshDatabase;

    public function test_it_filters_users_by_status(): void
    {
        $this->actingAs(User::factory()->create());

        $pending = User::factory()->create(['status' => 'pending']);
        $completed = User::factory()->create(['status' => 'completed']);

        Livewire::test(ListUsers::class)
            ->filterTable('status', 'completed')
            ->assertCanSeeTableRecords([$completed])
            ->assertCanNotSeeTableRecords([$pending]);
    }
}

This is a basic test to ensure that our custom filter is functioning as intended. With this in place, we can now customize the appearance and behavior of our table filter as needed.

With our custom filter working correctly, we’re one step closer to fine-tuning it according to our application’s specific requirements.

Customizing the Filter Appearance and Behavior

Now that our custom table filter is up and running, it’s time to make some adjustments to its appearance and behavior. We can do this by using various Filament features such as configuring the form fields in our filter class or changing the table’s filter layout.

Let’s say we want to change the default label for our search filter from “Search” to something more descriptive like “Search by Name”. We can do this by adding a new line to the schema() call in the setUp() method of SearchFilter:

$this->schema([
    TextInput::make('search')
        ->label('Search by Name') // Change the default label
        ->placeholder('Type a name or email'),
]);

Alternatively, we can change where the filters are rendered. This will allow us to customize the layout and design of the filters, for example by showing them above the table instead of inside a dropdown, arranged in a three-column grid. To do this, we update the filters() call in our UsersTable class:

// app/Filament/Resources/Users/Tables/UsersTable.php

use Filament\Tables\Enums\FiltersLayout;

return $table
    // ...
    ->filters([
        ...app(FilterLogic::class)->getFilters(),
    ], layout: FiltersLayout::AboveContent)
    ->filtersFormColumns(3);

With these changes, our custom table filter now has a more descriptive label and a customized layout. We can further customize the behavior of our filter by using Filament’s hooks or modifying the underlying code.

Our tutorial is now complete! With this final section, we’ve covered all aspects of creating a custom table filter with Filament and have been able to customize its appearance and behavior.

Frequently Asked Questions

What are the system requirements for setting up Filament and creating a custom table filter?

You need PHP 8.2 or newer, a Laravel 11.28+ application, and Composer installed on your system. You can verify Composer by running composer --version in your terminal.

How do I troubleshoot common errors when installing Filament packages using Composer?

Check the project directory to ensure you’re in the correct location and run composer show to display a list of all packages and their versions. This can help identify any issues with package installation.

Is it possible to use an alternative approach, such as using Laravel’s built-in filtering features instead of creating a custom table filter?

Yes, you can use Laravel’s built-in filtering features, but this tutorial focuses on creating a custom table filter for Filament. Using the built-in features might require more code and customization than using a custom filter.

What are some common pitfalls to avoid when implementing date range selection in a custom table filter?

Make sure to use the correct Eloquent model and database column names when defining the date range filter, as incorrect column names can lead to errors or unexpected results.

Comments

comments