# FilterBuilder

> Questions about a list, such as category is Groceries and amount over $50, each one an applied FilterPill you can edit or remove. + Filter asks for the field, then the condition and the value, with the control that fits the type: rows or a Combobox for options, an Input for text, a MoneyInput, a Calendar, yes or no. Keep the filters in the URL with serializeFilters and parseFilters. The URL is input, so parseFilters drops anything that does not match a field.

Section: Data. Docs: https://entrepta.vercel.app/docs/components/filter-builder

## Install

```bash
npx @entrepta/cli@latest add filter-builder
```

Files: `data/filter-builder.tsx`, `lib/format.ts`, `lib/filters.ts`, `hooks/use-url-filter.ts`, `lib/icon.tsx`, `primitives/filter-pill.tsx`, `lib/overlay.ts`, `primitives/popover.tsx`, `primitives/segmented-control.tsx`, `hooks/use-command-palette.ts`, `primitives/kbd.tsx`, `feedback/command-palette.tsx`, `primitives/badge.tsx`, `primitives/input.tsx`, `primitives/combobox.tsx`, `hooks/use-format.ts`, `primitives/money-input.tsx`, `primitives/calendar.tsx`, `primitives/button.tsx`, `primitives/button-variants.ts`.
npm packages: `@phosphor-icons/react`, `@radix-ui/react-popover`, `cmdk`, `@radix-ui/react-dialog`, `class-variance-authority`, `react-day-picker`, `@radix-ui/react-slot`.
Comes with: `format-lib`, `filters-lib`, `use-url-filter`, `icon-lib`, `filter-pill`, `overlay-lib`, `popover`, `segmented-control`, `use-command-palette`, `kbd`, `command-palette`, `badge`, `input`, `combobox`, `use-format`, `money-input`, `calendar`, `button`.

## Usage

```tsx
import { FilterBuilder } from "@/components/entrepta/filter-builder"
import { type Filter, matchesFilter, parseFilters, serializeFilters } from "@/lib/filters"

const FIELDS = [
  { id: "category", label: "Category", type: "enum", options: CATEGORIES },
  { id: "note", label: "Note", type: "text" },
  { id: "amount", label: "Amount", type: "amount", currency: "USD" },
  { id: "date", label: "Date", type: "date" },
  { id: "recurring", label: "Recurring", type: "boolean" },
] as const

const [filters, setFilters] = useState<Filter[]>(() =>
  parseFilters(new URLSearchParams(location.search).getAll("filter"), FIELDS)
)

<FilterBuilder fields={FIELDS} value={filters} onValueChange={setFilters} />

// in the browser, or send serializeFilters(filters) to your API
const rows = entries.filter((row) => filters.every((f) => matchesFilter(row[f.field], f)))
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `fields` | `{ id, label, type, options?, currency?, icon? }[]` | - | type is "enum", "text", "amount", "date" or "boolean". An id is letters, digits, - and _ |
| `value` | `{ field, op, value }[]` | - | Amounts in minor units, days as YYYY-MM-DD |
| `onValueChange` | `(filters) => void` | - | Every add, edit and removal |
| `labels` | `Partial<labels>` | - | Every word, the conditions under labels.ops |
| `serializeFilters(filters)` | `string[]` | - | One field:op:value string each, for params.append |
| `parseFilters(values, fields)` | `Filter[]` | - | Back from the URL. Drops unknown fields, wrong conditions, bad values and repeats |
| `matchesFilter(value, filter)` | `boolean` | - | For lists filtered in the browser. Text ignores case and accents |
