# useTableFiltering

## Usage

Table plugin that adds column filters with popover or inline controls. A column gets a filter when its `filter` names a field in `fields`. In the `inline` variant the control sits in the header cell under the column label; `inline-compact` puts the same control in a tighter frame when the table is short on space; in the `popover` variant it hides behind a funnel button and applies on confirm. Pairs with `useTableFilterState` for managed state.

```ts
import { useTableFiltering, useTableFilterState } from '@astryx-vue/core/Table'
```

Five field types are supported, and each one gets a matching control:

| `type` | Control | Matches |
| --- | --- | --- |
| `string` | text input | a substring of the cell text |
| `enum` | select | one option, exactly |
| `multi-select` | multi-select list | any one of the picked options |
| `number` | number input | the same number |
| `date` | date picker | a cell date later than the picked day |

```ts
const fields: TableFilterField[] = [
  { key: 'name', label: 'Name', type: 'string' },
  { key: 'role', label: 'Role', type: 'enum', options: roleOptions },
  { key: 'teams', label: 'Teams', type: 'multi-select', options: teamOptions },
  { key: 'age', label: 'Age', type: 'number', min: 18, max: 70, isIntegerOnly: true },
  { key: 'hired', label: 'Hired', type: 'date' },
]
```

`applyTableFilters()` returns the rows that match every active filter, so the plugin never touches `data` itself.

```ts
const { filters, onFilterChange } = useTableFilterState()
const filterPlugin = useTableFiltering(() => ({
  filters: filters.value,
  onFilterChange,
  fields,
  variant: 'inline',
}))
const rows = computed(() => applyTableFilters(employees, filters.value, fields))
```

The config is read again on every access, so pass a getter when the filters live in a `ref`. A plain object works too, but the table keeps the filters it saw first.

In the inline variants a filtered column that declares no `width` gets `proportional(1)`, so the control has room to sit under the label.

This filtering is separate from Power Search. A column without `filter` is never filtered, and the plugin renders nothing for it.

## Examples

### Table — Inline Filters

Table with inline filter controls rendered directly below each column header.

```vue
<script setup lang="ts">
import type { TableColumn, TableFilterField } from '@astryx-vue/core/Table'
import { applyTableFilters, proportional, Table, useTableFiltering, useTableFilterState } from '@astryx-vue/core/Table'
import { computed } from 'vue'

interface Employee extends Record<string, unknown> {
  id: string
  name: string
  email: string
  role: string
  department: string
}

const employees: Employee[] = [
  { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Engineer', department: 'Platform' },
  { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'Designer', department: 'Product' },
  { id: '3', name: 'Charlie Brown', email: 'charlie@example.com', role: 'Manager', department: 'Platform' },
  { id: '4', name: 'Diana Prince', email: 'diana@example.com', role: 'Engineer', department: 'Infra' },
  { id: '5', name: 'Eve Davis', email: 'eve@example.com', role: 'Admin', department: 'Operations' },
]

const fields: TableFilterField[] = [
  { key: 'name', type: 'string', label: 'Name' },
  { key: 'email', type: 'string', label: 'Email' },
  {
    key: 'role',
    type: 'enum',
    label: 'Role',
    options: [
      { value: 'Engineer', label: 'Engineer' },
      { value: 'Designer', label: 'Designer' },
      { value: 'Manager', label: 'Manager' },
      { value: 'Admin', label: 'Admin' },
    ],
  },
]

const columns: TableColumn<Employee>[] = [
  { key: 'name', header: 'Name', width: proportional(1), filter: 'name' },
  { key: 'email', header: 'Email', width: proportional(2), filter: 'email' },
  { key: 'role', header: 'Role', width: proportional(1), filter: 'role' },
  { key: 'department', header: 'Department', width: proportional(1) },
]

const { filters, onFilterChange } = useTableFilterState()
const filterPlugin = useTableFiltering<Employee>(() => ({
  filters: filters.value,
  onFilterChange,
  fields,
  variant: 'inline',
}))
const rows = computed(() => applyTableFilters(employees, filters.value, fields))
</script>

<template>
  <Table
    :data="rows"
    :columns="columns"
    id-key="id"
    :plugins="{ filter: filterPlugin }"
  />
</template>
```

### Table — Popover Filters

Table with popover filter controls triggered by icons in column headers.

```vue
<script setup lang="ts">
import type { TableColumn, TableFilterField } from '@astryx-vue/core/Table'
import { applyTableFilters, proportional, Table, useTableFiltering, useTableFilterState } from '@astryx-vue/core/Table'
import { computed } from 'vue'

interface Employee extends Record<string, unknown> {
  id: string
  name: string
  email: string
  role: string
  department: string
}

const employees: Employee[] = [
  { id: '1', name: 'Alice', email: 'alice@example.com', role: 'Engineer', department: 'Platform' },
  { id: '2', name: 'Bob', email: 'bob@example.com', role: 'Designer', department: 'Product' },
  { id: '3', name: 'Charlie', email: 'charlie@example.com', role: 'Manager', department: 'Platform' },
  { id: '4', name: 'Diana', email: 'diana@example.com', role: 'Engineer', department: 'Infrastructure' },
  { id: '5', name: 'Eve', email: 'eve@example.com', role: 'Admin', department: 'Operations' },
]

const fields: TableFilterField[] = [
  { key: 'name', type: 'string', label: 'Name' },
  { key: 'email', type: 'string', label: 'Email' },
  {
    key: 'role',
    type: 'enum',
    label: 'Role',
    options: [
      { value: 'Engineer', label: 'Engineer' },
      { value: 'Designer', label: 'Designer' },
      { value: 'Manager', label: 'Manager' },
      { value: 'Admin', label: 'Admin' },
    ],
  },
]

const columns: TableColumn<Employee>[] = [
  { key: 'name', header: 'Name', width: proportional(1), filter: 'name' },
  { key: 'email', header: 'Email', width: proportional(2), filter: 'email' },
  { key: 'role', header: 'Role', width: proportional(1), filter: 'role' },
  { key: 'department', header: 'Department', width: proportional(1) },
]

const { filters, onFilterChange } = useTableFilterState()
const filterPlugin = useTableFiltering<Employee>(() => ({
  filters: filters.value,
  onFilterChange,
  fields,
}))
const rows = computed(() => applyTableFilters(employees, filters.value, fields))
</script>

<template>
  <Table
    :data="rows"
    :columns="columns"
    id-key="id"
    :plugins="{ filter: filterPlugin }"
  />
</template>
```

## Parameters

`useTableFiltering()` takes one config object. Wrap it in a getter or a `ref` so the table reads the current filters: `useTableFiltering(() => ({ ... }))`.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `filters` | `TableFilterState` | — | Required. Current filter state: a map from field key to filter value. |
| `onFilterChange` | `(key: string, value: TableFilterValue \| null) => void` | — | Required. Called when the user changes a filter value. `null` clears the filter. |
| `fields` | `readonly TableFilterField[]` | — | Required. The fields a column may filter on. Each entry has `key`, `label`, `type`, and the options or limits that type needs. |
| `variant` | `'inline' \| 'inline-compact' \| 'popover'` | `'popover'` | Where the control sits. `inline` and `inline-compact` render it in the header cell under the label, `inline-compact` in a tighter frame; `popover` keeps it behind a funnel button that applies on confirm. |

## Returns

`useTableFiltering()` returns one `TablePlugin&lt;T&gt;`. Pass it to `Table` in `plugins`. The plugin fills two fields of the plugin contract:

| Name | Type | Description |
| --- | --- | --- |
| `transformColumns` | `(columns: TableColumn[]) => TableColumn[]` | Only set for the inline variants: a filtered column without a `width` gets `proportional(1)` so the control fits under the label. |
| `filterFor` | `(column: TableColumn) => TableFilterContribution \| null` | Builds the control for a column, or `null` when the column has no `filter` or names a field that is not in `fields`. |

### TableFilterContribution

| Name | Type | Description |
| --- | --- | --- |
| `variant` | `'inline' \| 'inline-compact' \| 'popover'` | The resolved variant, after the default is applied. |
| `field` | `TableFilterField` | The field the column named, taken from `fields`. |
| `value` | `TableFilterValue \| null` | The current value from `filters`, or `null` when the field has no filter. |
| `onChange` | `(value: TableFilterValue \| null) => void` | Reports a new value through your `onFilterChange`, already keyed by the field. |

### TableFilterField

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `key` | `string` | — | Field key. A column's `filter` holds this string, and the field reads `row[key]`. |
| `label` | `string` | — | Label and placeholder of the control. It also names the control for screen readers. |
| `type` | `'string' \| 'enum' \| 'multi-select' \| 'number' \| 'date'` | — | How the filter matches. `string` matches a substring, `enum` one option, `multi-select` any of the picked options, `number` the same number, and `date` a cell date later than the picked day. |
| `options` | `readonly { value: string, label: string }[]` | — | Choices for an `enum` or `multi-select` field. Without options the control renders an empty list. |
| `min` | `number` | — | Smallest value a `number` field accepts. |
| `max` | `number` | — | Largest value a `number` field accepts. |
| `step` | `number` | `1` when `isIntegerOnly`, otherwise none | Step of a `number` field. |
| `isIntegerOnly` | `boolean` | `false` | A `number` field that only accepts whole numbers. |

### TableFilterValue

| Name | Type | Description |
| --- | --- | --- |
| `value` | `string \| number \| string[]` | A `number` field stores a number, `multi-select` stores an array of option values, and the rest store a string. A `date` field stores an ISO calendar day such as `2026-03-21`. |
