Custom filter functions

Filter logic that the built-in filterFns (includesString, equals, between, ...) doesn't cover - semver ranges, CIDR / IP matching, fuzzy search, regex. The library doesn't expose a per-column filterFn field; the pattern is pre-filter the data array and hand the result to the grid.

When

You need any of:

For exact-match, contains, range, and "in [list]" - prefer api.setFilter with a built-in operator.

The pattern

Each filter is a pure (value, spec) => boolean function. Compose them in a $derived that filters the data array before it reaches <SvGrid data={...}>.

The examples on this page run against these rows:

<script lang="ts">
  import { SvGrid, type GridColumns, type SvGridApi } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    department: string
    city: string
    age: number
    salary: number
  }

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace',   department: 'Engineering', city: 'London',   age: 36, salary: 142000 },
    { id: 2, name: 'Grace Hopper',   department: 'Engineering', city: 'New York', age: 45, salary: 168000 },
    { id: 3, name: 'Linus Torvalds', department: 'Platform',    city: 'Portland', age: 54, salary: 155000 },
    { id: 4, name: 'Radia Perlman',  department: 'Networking',  city: 'Seattle',  age: 49, salary: 161000 },
    { id: 5, name: 'Barbara Liskov', department: 'Platform',    city: 'Boston',   age: 52, salary: 172000 },
  ]

  let rows = $state<Person[]>(people)

  const columns: GridColumns<Person> = [
    { field: 'name',       header: 'Name',       width: 190, editorType: 'text' },
    { field: 'department', header: 'Department', width: 150, editorType: 'text' },
    { field: 'city',       header: 'City',       width: 130, editorType: 'text' },
    { field: 'age',        header: 'Age',        width: 80,  editorType: 'number' },
    { field: 'salary',     header: 'Salary',     width: 130, editorType: 'number', format: { type: 'currency', currency: 'USD' } },
  ]
</script>
<script lang="ts">
  import { SvGrid, tableFeatures, rowSortingFeature, columnFilteringFeature } from '@svgrid/grid'

  type FilterFn = (value: unknown, filterValue: string) => boolean

  const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })

  let services: Service[] = [/* ... */]
  let semverSpec = $state('')
  let cidrSpec   = $state('')
  let fuzzySpec  = $state('')
  let regexSpec  = $state('')

  const filtered = $derived(
    services.filter((s) =>
      semverInRange(s.semver, semverSpec)
      && cidrContainsIp(s.ip, cidrSpec)
      && fuzzySubstring(s.id, fuzzySpec)
      && regexMatch(s.tag, regexSpec)
    ),
  )
</script>

<SvGrid data={filtered} {columns} {features} />

The grid stays a plain consumer - all filters compose naturally and empty inputs are no-ops.

The four filter implementations

Semver in range

/** Matches simple "^x.y.z" (compatible) and "~x.y.z" (patch-only). */
const semverInRange: FilterFn = (value, filterValue) => {
  const v = String(value ?? '')
  const spec = String(filterValue ?? '').trim()
  if (!spec) return true
  const parse = (s: string) =>
    s.split('.').map((n) => parseInt(n, 10) || 0) as [number, number, number]
  const op = spec[0]
  const base = parse(spec.slice(op === '^' || op === '~' ? 1 : 0))
  const cur  = parse(v)
  if (op === '^') return cur[0] === base[0] &&
    (cur[1] > base[1] || (cur[1] === base[1] && cur[2] >= base[2]))
  if (op === '~') return cur[0] === base[0] && cur[1] === base[1] && cur[2] >= base[2]
  return v === spec
}

CIDR contains IP

const cidrContainsIp: FilterFn = (value, filterValue) => {
  const ip = String(value ?? '')
  const cidr = String(filterValue ?? '').trim()
  if (!cidr) return true
  const [net, bitsStr] = cidr.split('/')
  if (!net || !bitsStr) return false
  const bits = parseInt(bitsStr, 10)
  const toInt = (s: string) =>
    s.split('.').reduce((a, b) => (a << 8) + (parseInt(b, 10) || 0), 0) >>> 0
  const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0
  return (toInt(ip) & mask) === (toInt(net) & mask)
}

Fuzzy substring (acronym OR contiguous)

const fuzzySubstring: FilterFn = (value, filterValue) => {
  const hay = String(value ?? '').toLowerCase()
  const needle = String(filterValue ?? '').toLowerCase().trim()
  if (!needle) return true
  if (hay.includes(needle)) return true
  // Acronym: every needle char appears in hay in order.
  let i = 0
  for (const ch of hay) {
    if (ch === needle[i]) i += 1
    if (i === needle.length) return true
  }
  return false
}

Regex match

const regexMatch: FilterFn = (value, filterValue) => {
  const v = String(value ?? '')
  const pattern = String(filterValue ?? '').trim()
  if (!pattern) return true
  try { return new RegExp(pattern).test(v) }
  catch { return false }
}

Performance notes

Try it

There is no per-column filterFn, so a custom predicate lives outside the grid: filter the array, hand the result to data. Everything downstream - sort, selection, the row count - follows the filtered set.

<script lang="ts">
  let query = $state('')

  // A fuzzy subsequence match: "gh" finds "Grace Hopper". Nothing the built-in
  // string operators express, and about six lines to write.
  function fuzzy(needle: string, haystack: string) {
    const n = needle.toLowerCase()
    const h = haystack.toLowerCase()
    let i = 0
    for (const ch of h) if (ch === n[i]) i += 1
    return i === n.length
  }

  const shown = $derived(
    query.trim() ? rows.filter((r) => fuzzy(query.trim(), r.name)) : rows,
  )
</script>

<input placeholder="Try: gh" bind:value={query} />

<SvGrid data={shown} {columns} sortable />

See also

Related articles