Back to Course |
Filament 3 From Scratch: Practical Course

Table Filters for Select and Dates

Now let's return to our tables and see how to filter the data. You have already seen the searchable() for columns, but we can add a more complex global filter.

Remember, in the ProductResource, we have this array empty by default:

app/Filament/Resources/ProductResource.php:

return $table
->columns([
// ...
])
->filters([
// <- THIS ONE
])

It's time to fill it in!

You can define the filters that would appear in the top-right corner of the table.

The most simple example provided in the docs would render a checkbox filter:

->filters([
Tables\Filters\Filter::make('is_featured')
->query(fn (Builder $query): Builder => $query->where('is_featured', true))
])

You need to provide a query that would filter by that checkbox.

But in our case, we don't have a checkbox field. We have Product status with three possible options. For this case, there's a SelectFilter:

->filters([
Tables\Filters\SelectFilter::make('status')
->options([
'in stock' => 'in stock',
'sold out' => 'sold out',
'coming soon' => 'coming soon',
])
])

This is the result seen on the top-right:

And when we choose the value, the table is filtered, with the value shown on top of the table:


Off-topic: Refactor Repeating Options

Not sure if you noticed, but we have two identical arrays of status options.

app/Filament/Resources/ProductResource.php:

class ProductResource extends Resource
{
public static function form(Form $form): Form
{
return $form
->schema([
// ...
 
Forms\Components\Radio::make('status')
->options([
'in stock' => 'in stock',
'sold out' => 'sold out',
'coming soon' => 'coming soon',
]),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
// ...
])
->filters([
Tables\Filters\SelectFilter::make('status')
->options([
'in stock' => 'in stock',
'sold out' => 'sold out',
'coming soon' => 'coming soon',
])
])
}
}

Let's follow the DRY ("Don't Repeat Yourself") principle and put that array somewhere.

That may be a separate Enum class, but for now, let's simplify that and extract it to a variable inside the same ProductResource because we will likely not use it anywhere else.

With that, I want to show you one Filament "limitation" and a workaround.

Let's try to put that as a private array $statuses inside the same Class and then use it as $this->statuses:

app/Filament/Resources/ProductResource.php:

class ProductResource extends Resource
{
private array $statuses = [
'in stock' => 'in stock',
'sold out' => 'sold out',
'coming soon' => 'coming soon',
];
 
public static function form(Form $form): Form
{
return $form
->schema([
// ...
 
Forms\Components\Radio::make('status')
->options($this->statuses),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
// ...
])
->filters([
Tables\Filters\SelectFilter::make('status')
->options($this->statuses)
])
}
}

Unfortunately, that wouldn't work because both the form() and table() methods are static. This means they don't know anything about $this and will throw an error "Using $this when not in object context".

So, if you want to declare some property inside the Filament Resource, it should also be static.

Look at the auto-generated default properties: they are both protected static:

class ProductResource extends Resource
{
protected static ?string $model = Product::class;
 
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
 
// ...
}

So, let's repeat precisely that and reference the array as self::$statuses instead.

class ProductResource extends Resource
{
protected static array $statuses = [
'in stock' => 'in stock',
'sold out' => 'sold out',
'coming soon' => 'coming soon',
];
 
// ...
 
Forms\Components\Radio::make('status')
->options(self::$statuses),
 
// ...
 
Tables\Filters\SelectFilter::make('status')
->options(self::$statuses)
}

And now, both form and filter should work correctly.


Multiple Filters and Relations

You can also add multiple filters. That's why it's an array variable.

Let's do that and add another SelectFilter for Product Category, and it's easy to use a relationship for value options.

->filters([
Tables\Filters\SelectFilter::make('status')
->options(self::$statuses),
Tables\Filters\SelectFilter::make('category')
->relationship('category', 'name')
])

I remind you that category is the name of the relationship method:

app/Models/Product.php:

class Product extends Model
{
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}

And then, we have two dropdowns on the top-right. The table data is refreshed on each change on any of them:


Custom Filter: Date Picker

You can also create custom filters using the same syntax as you would build a typical Resource Form in Filament.

Here's an example of filtering between created_at values:

->filters([
Tables\Filters\SelectFilter::make('status')
->options(self::$statuses),
Tables\Filters\SelectFilter::make('category')
->relationship('category', 'name'),
Tables\Filters\Filter::make('created_at')
->form([
Forms\Components\DatePicker::make('created_from'),
Forms\Components\DatePicker::make('created_until'),
])
->query(function (Builder $query, array $data): Builder {
return $query
->when(
$data['created_from'],
fn (Builder $query, $date): Builder => $query->whereDate('created_at', '>=', $date),
)
->when(
$data['created_until'],
fn (Builder $query, $date): Builder => $query->whereDate('created_at', '<=', $date),
);
})
])

This is the result visually:


Look & Feel: Above Table and Columns Grid

The filter you always need to turn on/off may only sometimes be convenient. The websites often show data tables with filters already visible on top from the beginning.

In Filament, it's easy to configure. Just add another parameter to the filters() method:

->filters([
Tables\Filters\SelectFilter::make('status')->...,
Tables\Filters\SelectFilter::make('category')->...,
Tables\Filters\Filter::make('created_at')->...,
], layout: Tables\Enums\FiltersLayout::AboveContent)

Then our filter will always be visible above the table:

But wait, now it takes more space than the table itself! Let's configure it to be divided into columns. In our case, we need four columns and use the ->filtersFormColumns() method.

->filters([
Tables\Filters\SelectFilter::make('status')->...,
Tables\Filters\SelectFilter::make('category')->...,
Tables\Filters\Filter::make('created_at')->...,
], layout: Tables\Enums\FiltersLayout::AboveContent)
->filtersFormColumns(4)

Here's the updated version:

Better, but still not ideal.

The final step: let's separate the created_at filter into two separate ones, so they would each be in its own filter column.

Here's the full code now:

->filters([
Tables\Filters\SelectFilter::make('status')
->options(self::$statuses),
Tables\Filters\SelectFilter::make('category')
->relationship('category', 'name'),
Tables\Filters\Filter::make('created_from')
->form([
Forms\Components\DatePicker::make('created_from'),
])
->query(function (Builder $query, array $data): Builder {
return $query
->when(
$data['created_from'],
fn (Builder $query, $date): Builder => $query->whereDate('created_at', '>=', $date),
);
}),
Tables\Filters\Filter::make('created_until')
->form([
Forms\Components\DatePicker::make('created_until'),
])
->query(function (Builder $query, array $data): Builder {
return $query
->when(
$data['created_until'],
fn (Builder $query, $date): Builder => $query->whereDate('created_at', '<=', $date),
);
}),
], layout: Tables\Enums\FiltersLayout::AboveContent)
->filtersFormColumns(4)

And the visual result: