# Custom filters

> Write your own comparisons and filters, or replace a filter input.

Source: https://docs.inertiax.dev/table/guide/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`.

## Add a custom comparison

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

**Laravel**

```php
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**

```php
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](https://docs.inertiax.dev/table/reference/clause/#inputs) for every input and value type.

## Build a filter from scratch

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

**Laravel**

```php
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**

```php
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.

## Replace the input

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**

```php
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**

```tsx
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.

## Format applied values

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

**React**

```tsx
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](https://docs.inertiax.dev/table/reference/react/#filter-value-summaries) for fallbacks and callback precedence.

## Wrap clause application

To query another column, use `column()`; see [Declare a filter](https://docs.inertiax.dev/table/guide/filters/#declare-a-filter). Use `applyClauseUsing()` when every comparison of a filter needs the same wrapper, such as a has-many relation:

**Laravel**

```php
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](https://docs.inertiax.dev/table/reference/filter/#shared-clause-routing) for callback context.

## Keep a select filter in the toolbar

For a stock status dropdown, call `pinned()`; see [Filtering](https://docs.inertiax.dev/table/guide/filters/#keep-a-select-filter-in-the-toolbar).
