Skip to content

Custom filters

A filter is a field; its clauses are the comparisons users choose for it. Each clause takes no value, one value, a list or a range. Add clauses to a built-in filter, or start from a blank Filter.

This date filter also offers “In the last … days”, which takes a number of days:

Laravel

use Illuminate\Database\Eloquent\Builder;
use InertiaX\Table\Enums\DataSourceKind;
use InertiaX\Table\Filters\Clauses\Clause;
use InertiaX\Table\Filters\DateFilter;
DateFilter::make('created_at', 'Created')
->addClause(
Clause::make('acme/last-days', 'In the last … days')
->valueType('number')
->validateUsing(fn (mixed $value): bool => is_int($value) && $value > 0)
->applyUsing(
fn (Builder $query, string $column, int $value): Builder =>
$query->where($column, '>=', now()->subDays($value)),
DataSourceKind::Eloquent,
),
);

Custom clause keys are namespaced, such as acme/last-days. valueType('number') gives the clause a number input; without it, the clause would take a date, like the filter’s other clauses. validateUsing() rejects fractions and negative numbers before the int callback runs. applyUsing() receives the filter’s column and the validated value, and runs only on the source kinds you declare.

A clause can also take no value:

Laravel

use InertiaX\Table\Filters\TextFilter;
TextFilter::make('email')
->addClause(
Clause::make('acme/work-email', 'Is a work email')
->withoutValue()
->applyUsing(
fn (Builder $query, string $column): Builder =>
$query->where($column, 'not like', '%@gmail.com'),
DataSourceKind::Eloquent,
),
);

range() takes a [from, to] pair and multiple() a list of select values. See Clause API for every input and value type.

Filter::make() starts a filter with no comparisons. This one finds users who belong to an organization:

Laravel

use InertiaX\Table\Filters\Filter;
Filter::make('is_org', 'Organization')
->addClause(
Clause::make('acme/org', 'Is an organization')
->withoutValue()
->applyUsing(
fn (Builder $query): Builder => $query->whereNotNull('company_id'),
DataSourceKind::Eloquent,
),
);

To reuse it, extend Filter and declare its key and clauses:

Laravel

namespace App\Tables\Filters;
use Illuminate\Database\Eloquent\Builder;
use InertiaX\Table\Enums\DataSourceKind;
use InertiaX\Table\Filters\Clauses\Clause;
use InertiaX\Table\Filters\Filter;
class OrganizationFilter extends Filter
{
protected string $key = 'is_org';
protected ?string $label = 'Organization';
public function clauses(): array
{
return [
Clause::make('acme/org', 'Is an organization')
->withoutValue()
->applyUsing(
fn (Builder $query): Builder => $query->whereNotNull('company_id'),
DataSourceKind::Eloquent,
),
];
}
}

List OrganizationFilter::make() in a table’s filters(); OrganizationFilter::make('organization') gives it another key. A filter without a key, or without clauses, fails when the table is prepared, with an error that says what to add.

Give a clause a custom value type, then register an input for that value type in React. This filter takes a role through a custom control:

Laravel

Filter::make('role', 'Role')
->addClause(
Clause::make('acme/has-role', 'Has role')
->valueType('acme/role')
->validateUsing(
fn (mixed $value): bool => in_array($value, ['admin', 'engineer'], true),
)
->applyUsing(
fn (Builder $query, string $column, string $value): Builder =>
$query->where($column, $value),
DataSourceKind::Eloquent,
),
);

A custom value type accepts any value except null, so validate the values you expect.

React

import {
register,
type TableCustomizationOptions,
type TableFilterInputRendererProps,
} from '@inertiax/react-table';
import { AppTable } from '@/components/app-table';
function RoleInput({ ariaLabel, id, value, onChange }: TableFilterInputRendererProps) {
return (
<select
aria-label={ariaLabel}
id={id}
value={typeof value === 'string' ? value : ''}
onChange={(event) => onChange(event.target.value || undefined)}
>
<option value="">Any role</option>
<option value="admin">Administrator</option>
<option value="engineer">Engineer</option>
</select>
);
}
const customization = {
filterInputs: [register('acme/role', RoleInput)],
} satisfies TableCustomizationOptions;
export function UsersIndex() {
return <AppTable prop="users" customization={customization} />;
}

Emit undefined to clear the draft value. The built-in editor commits with Add filter, Save, or Apply filters. Users remove applied conditions with the remove button.

Preserve the server’s value types: string "1", number 1, and Boolean true are distinct. The input also receives the filter definition and the selected clause. clause.input.valueType names the value type, and a select input lists its choices in clause.input.options. The input mounts while a condition is edited.

Add formatFilterValue to show a readable value in Basic filter chips:

React

const customizationWithLabels = {
...customization,
formatFilterValue: ({ clause, value }) => {
if (clause.input?.valueType !== 'acme/role') return undefined;
if (value === 'admin') return 'Administrator';
if (value === 'engineer') return 'Engineer';
return undefined;
},
} satisfies TableCustomizationOptions;
<AppTable prop="users" customization={customizationWithLabels} />;

Return a string to change the label or undefined to use the next formatter. The submitted value stays unchanged. See Filter value summaries for fallbacks and callback precedence.

To query another column, use column(); see Declare a filter. Use applyClauseUsing() when every comparison of a filter needs the same wrapper, such as a has-many relation:

Laravel

TextFilter::make('tag', 'Tag')
->setClauses(['equals', 'contains'])
->applyClauseUsing(
fn (Builder $query, mixed $value, \Closure $applyClause): Builder => $query->whereHas(
'tags',
fn (Builder $tags) => $applyClause($tags, 'name', $value),
),
DataSourceKind::Eloquent,
);

The model needs a tags() relation whose records have a name field. The selected clause still validates the value and performs the comparison, here on each tag’s name. Inside whereHas(), a negated comparison would match records with any other tag, so this filter offers only positive ones. See Filter API for callback context.

For a stock status dropdown, call pinned(); see Filtering.

View Markdown