Custom Filters
Create your own filter types with custom value shapes
Custom Filters
While the built-in filters cover common use cases, you can create custom filters for specialized data types or when you need specific options.
Creating a Custom Filter
Use createFilter to define a new filter type:
import { createFilter, defineFilters } from "@jlnstack/filter"
// 1. Create the filter with an ID and value type
const selectFilter = createFilter("select")
.input<string>()
.options<{
label?: string
options: { value: string; label: string }[]
}>()
// 2. Use it in your schema
const filters = defineFilters({
status: selectFilter({
label: "Status",
options: [
{ value: "active", label: "Active" },
{ value: "pending", label: "Pending" },
{ value: "archived", label: "Archived" },
],
}),
})The createFilter API
createFilter(id)
.input<ValueType>() // or .input(standardSchema)
.options<OptionsType>() // Define custom optionsWith Schema Validation
You can pass a Standard Schema compatible validator:
import { z } from "zod"
import { createFilter, defineFilters } from "@jlnstack/filter"
const rangeSchema = z.object({
min: z.number(),
max: z.number(),
})
const rangeFilter = createFilter("range")
.input(rangeSchema)
.options<{
label?: string
step?: number
}>()
const filters = defineFilters({
priceRange: rangeFilter({
label: "Price Range",
step: 10,
}),
})Examples
Enum Filter
const statusValues = ["active", "pending", "archived"] as const
type Status = (typeof statusValues)[number]
const statusFilter = createFilter("status")
.input<Status>()
.options<{
label?: string
}>()
const filters = defineFilters({
status: statusFilter({ label: "Status" }),
})
// Usage
filter.addCondition({ field: "status", value: "active" }) // ✅ Type-safe
filter.addCondition({ field: "status", value: "invalid" }) // ❌ Type errorMulti-Select Filter
const multiSelectFilter = createFilter("multi-select")
.input<string[]>()
.options<{
label?: string
options: { value: string; label: string }[]
}>()
const filters = defineFilters({
tags: multiSelectFilter({
label: "Tags",
options: [
{ value: "featured", label: "Featured" },
{ value: "sale", label: "On Sale" },
{ value: "new", label: "New Arrival" },
],
}),
})
// Usage in component (condition is a Condition<Schema> for \"tags\")
<filter.Filter
condition={condition}
render={({ value, onValueChange, definition }) => (
<div>
{definition.options.map((opt) => (
<label key={opt.value}>
<input
type="checkbox"
checked={value?.includes(opt.value)}
onChange={(e) => {
const current = value ?? []
onValueChange(
e.target.checked
? [...current, opt.value]
: current.filter((v) => v !== opt.value)
)
}}
/>
{opt.label}
</label>
))}
</div>
)}
/>Date Range Filter
type DateRange = {
from: Date | string
to: Date | string
}
const dateRangeFilter = createFilter("date-range")
.input<DateRange>()
.options<{
label?: string
presets?: { label: string; value: DateRange }[]
}>()
const filters = defineFilters({
dateRange: dateRangeFilter({
label: "Date Range",
presets: [
{
label: "Last 7 days",
value: {
from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
to: new Date(),
},
},
{
label: "Last 30 days",
value: {
from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
to: new Date(),
},
},
],
}),
})Filter with Required Options
If your filter requires certain options, TypeScript will enforce them:
const selectFilter = createFilter("select")
.input<string>()
.options<{
label?: string // Optional
options: { value: string; label: string }[] // Required
}>()
// ❌ Type error - missing required 'options'
const filters = defineFilters({
status: selectFilter({ label: "Status" }),
})
// ✅ Correct
const filters = defineFilters({
status: selectFilter({
label: "Status",
options: [{ value: "active", label: "Active" }],
}),
})Filter with No Options
If your filter doesn't need options, call .options() with no type parameter:
const timestampFilter = createFilter("timestamp")
.input<number>()
.options()
const filters = defineFilters({
lastLogin: timestampFilter(), // No options needed
})Accessing Definition in Render
The definition prop in the render function gives you access to all options:
// condition is a Condition<Schema> for the \"status\" field
<filter.Filter
condition={condition}
render={({ value, onValueChange, onRemove, definition }) => (
<select
value={value ?? ""}
onChange={(e) => (e.target.value ? onValueChange(e.target.value) : onRemove())}
>
<option value="">All</option>
{definition.options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
)}
/>Using Filter Type in Components
You can infer the filter definition type for component props:
import type { AvailableFilter } from "@jlnstack/filter/react"
type FilterItemProps = {
filter: AvailableFilter<typeof filters>
}
function FilterItem({ filter }: FilterItemProps) {
// filter.name, filter.id, and all options are typed
return (
<div>
<span>{filter.label ?? filter.name}</span>
{filter.id === "select" && (
<span>({filter.options.length} options)</span>
)}
</div>
)
}