Documentation
Column Filtering
Let users filter columns by type — or drive filtering from your server.
Enable filtering
Set filterable: true on a column. Operators follow the column type (string, number, date, boolean, or enum).
TypeScript
Copy
{accessor: "name",label: "Full Name",width: "1fr",type: "string",filterable: true,}
TypeScript
Copy
{accessor: "name",label: "Full Name",width: "1fr",type: "string",filterable: true,}
TypeScript
Copy
{accessor: "name",label: "Full Name",width: "1fr",type: "string",filterable: true,}
TypeScript
Copy
{accessor: "name",label: "Full Name",width: "1fr",type: "string",filterable: true,}
TypeScript
Copy
{accessor: "name",label: "Full Name",width: "1fr",type: "string",filterable: true,}
TypeScript
Copy
{accessor: "name",label: "Full Name",width: "1fr",type: "string",filterable: true,}
Limit filter operators
Use filterOperators to show only the comparisons you want, in that order. Invalid operators for the column type are ignored. Has no effect on enum columns.
TypeScript
Copy
{accessor: "name",label: "Full Name",type: "string",filterable: true,filterOperators: ["contains", "equals"],}
TypeScript
Copy
{accessor: "name",label: "Full Name",type: "string",filterable: true,filterOperators: ["contains", "equals"],}
TypeScript
Copy
{accessor: "name",label: "Full Name",type: "string",filterable: true,filterOperators: ["contains", "equals"],}
TypeScript
Copy
{accessor: "name",label: "Full Name",type: "string",filterable: true,filterOperators: ["contains", "equals"],}
TypeScript
Copy
{accessor: "name",label: "Full Name",type: "string",filterable: true,filterOperators: ["contains", "equals"],}
TypeScript
Copy
{accessor: "name",label: "Full Name",type: "string",filterable: true,filterOperators: ["contains", "equals"],}
Enum filters
Enum columns use a checkbox picker from enumOptions. With more than 10 options, a search input appears automatically.
TypeScript
Copy
{accessor: "status",label: "Status",type: "enum",filterable: true,enumOptions: [{ label: "Active", value: "active" },{ label: "Inactive", value: "inactive" },{ label: "Pending", value: "pending" },],}
TypeScript
Copy
{accessor: "status",label: "Status",type: "enum",filterable: true,enumOptions: [{ label: "Active", value: "active" },{ label: "Inactive", value: "inactive" },{ label: "Pending", value: "pending" },],}
TypeScript
Copy
{accessor: "status",label: "Status",type: "enum",filterable: true,enumOptions: [{ label: "Active", value: "active" },{ label: "Inactive", value: "inactive" },{ label: "Pending", value: "pending" },],}
TypeScript
Copy
{accessor: "status",label: "Status",type: "enum",filterable: true,enumOptions: [{ label: "Active", value: "active" },{ label: "Inactive", value: "inactive" },{ label: "Pending", value: "pending" },],}
TypeScript
Copy
{accessor: "status",label: "Status",type: "enum",filterable: true,enumOptions: [{ label: "Active", value: "active" },{ label: "Inactive", value: "inactive" },{ label: "Pending", value: "pending" },],}
TypeScript
Copy
{accessor: "status",label: "Status",type: "enum",filterable: true,enumOptions: [{ label: "Active", value: "active" },{ label: "Inactive", value: "inactive" },{ label: "Pending", value: "pending" },],}
External / server filtering
Set externalFilterHandling and handle onFilterChange — the table keeps filter UI while you supply pre-filtered rows.
React TSX
Copy
<SimpleTableexternalFilterHandlingcolumns={columns}rows={filteredRows}onFilterChange={(filters) => {// fetch or filter filteredRows from filters}}/>
Angular
Copy
<simple-table[externalFilterHandling]="true"[columns]="columns"[rows]="filteredRows"(filterChange)="handleFilterChange($event)"></simple-table>
Vue SFC
Copy
<SimpleTable:external-filter-handling="true":columns="columns":rows="filteredRows":on-filter-change="handleFilterChange"/>
Svelte
Copy
<SimpleTableexternalFilterHandling={true}{columns}rows={filteredRows}onFilterChange={handleFilterChange}/>
Solid TSX
Copy
<SimpleTableexternalFilterHandlingcolumns={columns}rows={filteredRows()}onFilterChange={(filters) => {// fetch or filter filteredRows from filters}}/>
TypeScript
Copy
new SimpleTableVanilla(container, {columns,rows: filteredRows,externalFilterHandling: true,onFilterChange: (filters) => {// fetch or filter filteredRows from filters},});
Example
React TSX
Copy
1import { SimpleTable } from "@simple-table/react";2import type { Theme } from "@simple-table/react";3import {4 columnFilteringConfig,5 type ColumnFilteringEmployee6} from "./column-filtering.demo-data";7import "@simple-table/react/styles.css";89const ColumnFilteringDemo = ({10 height = "400px",11 theme12}: {13 height?: string | number;14 theme?: Theme;15}) => {16 return (17 <SimpleTable18 columns={columnFilteringConfig.headers}19 getRowId={({ row }) => row.id}20 rows={columnFilteringConfig.rows}21 height={height}22 theme={theme}23 />24 );25};2627export default ColumnFilteringDemo;
Angularcolumn-filtering-demo.component.ts
Copy
1import { Component, Input } from "@angular/core";2import {SimpleTableComponent} from "@simple-table/angular";import type { AngularColumnDef, GetRowIdParams, Theme } from "@simple-table/angular";3import { columnFilteringConfig } from "./column-filtering.demo-data";4import "@simple-table/angular/styles.css";5import type { ColumnFilteringEmployee } from "./column-filtering.demo-data";67@Component({8 selector: "column-filtering-demo",9 standalone: true,10 imports: [SimpleTableComponent],11 template: `12 <simple-table13 [getRowId]="getRowId"14 [rows]="rows"15 [columns]="headers"16 [height]="height"17 [theme]="theme"18 ></simple-table>19 `,20})21export class ColumnFilteringDemoComponent {22 @Input() height: string | number = "400px";23 @Input() theme?: Theme;2425 readonly rows: ColumnFilteringEmployee[] = columnFilteringConfig.rows;26 readonly headers: AngularColumnDef<ColumnFilteringEmployee>[] = columnFilteringConfig.headers;2728 getRowId = ({ row }: GetRowIdParams<ColumnFilteringEmployee>) => row.id;29}303132// column-filtering.demo-data.ts33// Self-contained demo table setup for this example.34import type { AngularColumnDef, CellRendererProps } from "@simple-table/angular";3536export interface ColumnFilteringEmployee {37 id: number;38 name: string;39 department: string;40 role: string;41 salary: number;42 startDate: string;43 isActive: boolean;44}4546export const COLUMN_FILTERING_DATA: ColumnFilteringEmployee[] = [47 {48 id: 1,49 name: "Bianca Rossi",50 department: "Editorial",51 role: "Senior Editor",52 salary: 82000,53 startDate: "2020-02-14",54 isActive: true,55 },56 {57 id: 2,58 name: "Axel Chen",59 department: "Production",60 role: "Art Director",61 salary: 75000,62 startDate: "2021-06-18",63 isActive: true,64 },65 {66 id: 3,67 name: "Emilia Nakamura",68 department: "Editorial",69 role: "Managing Editor",70 salary: 95000,71 startDate: "2019-04-22",72 isActive: true,73 },74 {75 id: 4,76 name: "Luca Martinez",77 department: "Marketing",78 role: "Content Strategist",79 salary: 68000,80 startDate: "2022-01-12",81 isActive: false,82 },83 {84 id: 5,85 name: "Delia Kumar",86 department: "Production",87 role: "Layout Designer",88 salary: 72000,89 startDate: "2020-09-07",90 isActive: true,91 },92 {93 id: 6,94 name: "Cian O'Sullivan",95 department: "Sales",96 role: "Sales Representative",97 salary: 65000,98 startDate: "2021-11-03",99 isActive: true,100 },101 {102 id: 7,103 name: "Amara Okafor",104 department: "Human Resources",105 role: "HR Manager",106 salary: 78000,107 startDate: "2019-08-15",108 isActive: true,109 },110 {111 id: 8,112 name: "Rowan Thompson",113 department: "Production",114 role: "Cover Designer",115 salary: 69000,116 startDate: "2021-12-20",117 isActive: false,118 },119 {120 id: 9,121 name: "Celeste Petrov",122 department: "Marketing",123 role: "PR Specialist",124 salary: 63000,125 startDate: "2022-03-08",126 isActive: true,127 },128 {129 id: 10,130 name: "Quinn Hassan",131 department: "Sales",132 role: "Sales Manager",133 salary: 89000,134 startDate: "2020-05-11",135 isActive: true,136 },137 {138 id: 11,139 name: "Isla Williams",140 department: "Editorial",141 role: "Copy Editor",142 salary: 58000,143 startDate: "2021-10-25",144 isActive: true,145 },146 {147 id: 12,148 name: "Dax Silva",149 department: "Finance",150 role: "Financial Analyst",151 salary: 64000,152 startDate: "2022-07-14",153 isActive: false,154 },155 {156 id: 13,157 name: "Maya Patel",158 department: "IT Support",159 role: "Systems Administrator",160 salary: 71000,161 startDate: "2021-03-15",162 isActive: true,163 },164 {165 id: 14,166 name: "Jordan Lee",167 department: "Quality Assurance",168 role: "QA Engineer",169 salary: 67000,170 startDate: "2020-11-08",171 isActive: true,172 },173];174175176export const DEPARTMENT_OPTIONS = [177 { label: "Editorial", value: "Editorial" },178 { label: "Production", value: "Production" },179 { label: "Marketing", value: "Marketing" },180 { label: "Sales", value: "Sales" },181 { label: "Operations", value: "Operations" },182 { label: "Human Resources", value: "Human Resources" },183 { label: "Finance", value: "Finance" },184 { label: "Legal", value: "Legal" },185 { label: "IT Support", value: "IT Support" },186 { label: "Customer Service", value: "Customer Service" },187 { label: "Research & Development", value: "Research & Development" },188 { label: "Quality Assurance", value: "Quality Assurance" },189];190191export const columnFilteringHeaders: AngularColumnDef<ColumnFilteringEmployee>[] = [192 {193 accessor: "id",194 label: "ID",195 width: 80,196 type: "number",197 sortable: true,198 filterable: true,199 },200 {201 accessor: "name",202 label: "Employee Name",203 width: "1fr",204 minWidth: 150,205 type: "string",206 sortable: true,207 filterable: true,208 },209 {210 accessor: "department",211 label: "Department",212 width: "1fr",213 minWidth: 120,214 type: "enum",215 sortable: true,216 filterable: true,217 enumOptions: DEPARTMENT_OPTIONS,218 },219 {220 accessor: "role",221 label: "Role",222 width: 140,223 type: "string",224 sortable: true,225 filterable: true,226 },227 {228 accessor: "salary",229 label: "Salary",230 width: 120,231 align: "right",232 type: "number",233 sortable: true,234 filterable: true,235 cellRenderer: ({ row }: CellRendererProps<ColumnFilteringEmployee>) => {236 return `$${row.salary.toLocaleString()}`;237 },238 },239 {240 accessor: "startDate",241 label: "Start Date",242 width: 130,243 type: "date",244 sortable: true,245 filterable: true,246 },247 {248 accessor: "isActive",249 label: "Active",250 width: 100,251 align: "center",252 type: "boolean",253 sortable: true,254 filterable: true,255 },256];257258export const columnFilteringConfig = {259 headers: columnFilteringHeaders,260 rows: COLUMN_FILTERING_DATA,261};262
Vue SFC
Copy
1<template>2 <SimpleTable3 :columns="columnFilteringConfig.headers"4 :rows="columnFilteringConfig.rows"5 :get-row-id="getRowId"6 :height="height"7 :theme="theme"8 />9</template>1011<script setup lang="ts">12import { SimpleTable } from "@simple-table/vue";13import type { Theme, GetRowIdParams } from "@simple-table/vue";14import { columnFilteringConfig } from "./column-filtering.demo-data";15import type { ColumnFilteringEmployee } from "./column-filtering.demo-data";16import "@simple-table/vue/styles.css";1718withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {19 height: "400px",20});2122const getRowId = ({ row }: GetRowIdParams<ColumnFilteringEmployee>) => row.id;23</script>
Svelte
Copy
1<script lang="ts">2 import { SimpleTable } from "@simple-table/svelte";3 import type { Theme, GetRowIdParams } from "@simple-table/svelte";4 import { columnFilteringConfig } from "./column-filtering.demo-data";5 import type { ColumnFilteringEmployee } from "./column-filtering.demo-data";6 import "@simple-table/svelte/styles.css";78 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();910 const getRowId = ({ row }: GetRowIdParams<ColumnFilteringEmployee>) => row.id;11</script>1213<SimpleTable14 columns={columnFilteringConfig.headers}15 rows={columnFilteringConfig.rows}16 getRowId={getRowId}17 {height}18 {theme}19/>
Solid TSX
Copy
1import {SimpleTable} from "@simple-table/solid";import type { Theme } from "@simple-table/solid";2import { columnFilteringConfig } from "./column-filtering.demo-data";3import "@simple-table/solid/styles.css";45export default function ColumnFilteringDemo(props: {6 height?: string | number;7 theme?: Theme;8}) {9 return (10 <SimpleTable11 columns={columnFilteringConfig.headers}12 getRowId={({ row }) => row.id}13 rows={columnFilteringConfig.rows}14 height={props.height ?? "400px"}15 theme={props.theme}16 />17 );18}
TypeScriptColumnFilteringDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { ColumnFilteringEmployee } from "./column-filtering.demo-data";3import type { Theme, GetRowIdParams } from "simple-table-core";4import { columnFilteringConfig } from "./column-filtering.demo-data";5import "simple-table-core/styles.css";678const getRowId = ({ row }: GetRowIdParams<ColumnFilteringEmployee>) => row.id;9export function renderColumnFilteringDemo(10 container: HTMLElement,11 options?: { height?: string | number; theme?: Theme }12): SimpleTableVanilla<ColumnFilteringEmployee> {13 const table = new SimpleTableVanilla(container, {14 getRowId,15 columns: columnFilteringConfig.headers,16 rows: columnFilteringConfig.rows,17 height: options?.height ?? "400px",18 theme: options?.theme,19 });20 return table;21}222324// column-filtering.demo-data.ts25// Self-contained demo table setup for this example.26import type { ColumnDef, CellRendererProps } from "simple-table-core";2728export interface ColumnFilteringEmployee {29 id: number;30 name: string;31 department: string;32 role: string;33 salary: number;34 startDate: string;35 isActive: boolean;36}3738export const COLUMN_FILTERING_DATA: ColumnFilteringEmployee[] = [39 {40 id: 1,41 name: "Bianca Rossi",42 department: "Editorial",43 role: "Senior Editor",44 salary: 82000,45 startDate: "2020-02-14",46 isActive: true,47 },48 {49 id: 2,50 name: "Axel Chen",51 department: "Production",52 role: "Art Director",53 salary: 75000,54 startDate: "2021-06-18",55 isActive: true,56 },57 {58 id: 3,59 name: "Emilia Nakamura",60 department: "Editorial",61 role: "Managing Editor",62 salary: 95000,63 startDate: "2019-04-22",64 isActive: true,65 },66 {67 id: 4,68 name: "Luca Martinez",69 department: "Marketing",70 role: "Content Strategist",71 salary: 68000,72 startDate: "2022-01-12",73 isActive: false,74 },75 {76 id: 5,77 name: "Delia Kumar",78 department: "Production",79 role: "Layout Designer",80 salary: 72000,81 startDate: "2020-09-07",82 isActive: true,83 },84 {85 id: 6,86 name: "Cian O'Sullivan",87 department: "Sales",88 role: "Sales Representative",89 salary: 65000,90 startDate: "2021-11-03",91 isActive: true,92 },93 {94 id: 7,95 name: "Amara Okafor",96 department: "Human Resources",97 role: "HR Manager",98 salary: 78000,99 startDate: "2019-08-15",100 isActive: true,101 },102 {103 id: 8,104 name: "Rowan Thompson",105 department: "Production",106 role: "Cover Designer",107 salary: 69000,108 startDate: "2021-12-20",109 isActive: false,110 },111 {112 id: 9,113 name: "Celeste Petrov",114 department: "Marketing",115 role: "PR Specialist",116 salary: 63000,117 startDate: "2022-03-08",118 isActive: true,119 },120 {121 id: 10,122 name: "Quinn Hassan",123 department: "Sales",124 role: "Sales Manager",125 salary: 89000,126 startDate: "2020-05-11",127 isActive: true,128 },129 {130 id: 11,131 name: "Isla Williams",132 department: "Editorial",133 role: "Copy Editor",134 salary: 58000,135 startDate: "2021-10-25",136 isActive: true,137 },138 {139 id: 12,140 name: "Dax Silva",141 department: "Finance",142 role: "Financial Analyst",143 salary: 64000,144 startDate: "2022-07-14",145 isActive: false,146 },147 {148 id: 13,149 name: "Maya Patel",150 department: "IT Support",151 role: "Systems Administrator",152 salary: 71000,153 startDate: "2021-03-15",154 isActive: true,155 },156 {157 id: 14,158 name: "Jordan Lee",159 department: "Quality Assurance",160 role: "QA Engineer",161 salary: 67000,162 startDate: "2020-11-08",163 isActive: true,164 },165];166167168export const DEPARTMENT_OPTIONS = [169 { label: "Editorial", value: "Editorial" },170 { label: "Production", value: "Production" },171 { label: "Marketing", value: "Marketing" },172 { label: "Sales", value: "Sales" },173 { label: "Operations", value: "Operations" },174 { label: "Human Resources", value: "Human Resources" },175 { label: "Finance", value: "Finance" },176 { label: "Legal", value: "Legal" },177 { label: "IT Support", value: "IT Support" },178 { label: "Customer Service", value: "Customer Service" },179 { label: "Research & Development", value: "Research & Development" },180 { label: "Quality Assurance", value: "Quality Assurance" },181];182183export const columnFilteringHeaders: ColumnDef<ColumnFilteringEmployee>[] = [184 {185 accessor: "id",186 label: "ID",187 width: 80,188 type: "number",189 sortable: true,190 filterable: true,191 },192 {193 accessor: "name",194 label: "Employee Name",195 width: "1fr",196 minWidth: 150,197 type: "string",198 sortable: true,199 filterable: true,200 },201 {202 accessor: "department",203 label: "Department",204 width: "1fr",205 minWidth: 120,206 type: "enum",207 sortable: true,208 filterable: true,209 enumOptions: DEPARTMENT_OPTIONS,210 },211 {212 accessor: "role",213 label: "Role",214 width: 140,215 type: "string",216 sortable: true,217 filterable: true,218 },219 {220 accessor: "salary",221 label: "Salary",222 width: 120,223 align: "right",224 type: "number",225 sortable: true,226 filterable: true,227 cellRenderer: ({ row }: CellRendererProps<ColumnFilteringEmployee>) => {228 const salary = row.salary;229 return `$${salary.toLocaleString()}`;230 },231 },232 {233 accessor: "startDate",234 label: "Start Date",235 width: 130,236 type: "date",237 sortable: true,238 filterable: true,239 },240 {241 accessor: "isActive",242 label: "Active",243 width: 100,244 align: "center",245 type: "boolean",246 sortable: true,247 filterable: true,248 },249];250251export const columnFilteringConfig = {252 headers: columnFilteringHeaders,253 rows: COLUMN_FILTERING_DATA,254};255
External filtering example
Filtering is handled outside the table; filter controls still update.
External Filter Status: No filters applied
React TSX
Copy
1import { useState, useMemo } from "react";2import { SimpleTable } from "@simple-table/react";3import type { Theme, TableFilterState } from "@simple-table/react";4import {5 externalFilterConfig,6 matchesFilter,7 type FilterableEmployee8} from "./external-filter.demo-data";9import "@simple-table/react/styles.css";1011const ExternalFilterDemo = ({12 height = "400px",13 theme14}: {15 height?: string | number;16 theme?: Theme;17}) => {18 const [filters, setFilters] = useState<TableFilterState<FilterableEmployee>>({});1920 const filteredData = useMemo(() => {21 const filterEntries = Object.entries(filters);22 if (filterEntries.length === 0) return externalFilterConfig.rows;2324 return externalFilterConfig.rows.filter((row) =>25 filterEntries.every(([accessor, filter]) =>26 matchesFilter(row[accessor as keyof FilterableEmployee], filter),27 ),28 );29 }, [filters]);3031 return (32 <SimpleTable33 columns={externalFilterConfig.headers}34 rows={filteredData}35 onFilterChange={setFilters}36 externalFilterHandling37 columnResizing38 height={height}39 theme={theme}40 getRowId={({ row }) => row.id}41 />42 );43};4445export default ExternalFilterDemo;
Angularexternal-filter-demo.component.ts
Copy
1import { Component, Input } from "@angular/core";2import { SimpleTableImports } from "@simple-table/angular";3import type { AngularColumnDef, GetRowIdParams, TableFilterState, Theme } from "@simple-table/angular";4import { externalFilterConfig, isFilterableKey, matchesFilter } from "./external-filter.demo-data";5import "@simple-table/angular/styles.css";6import type { FilterableEmployee } from "./external-filter.demo-data";78@Component({9 selector: "external-filter-demo",10 standalone: true,11 imports: [SimpleTableImports],12 template: `13 <simple-table14 [getRowId]="getRowId"15 [rows]="filteredRows"16 [columns]="headers"17 [externalFilterHandling]="true"18 [columnResizing]="true"19 [height]="height"20 [theme]="theme"21 (filterChange)="handleFilterChange($event)"22 ></simple-table>23 `,24})25export class ExternalFilterDemoComponent {26 @Input() height: string | number = "400px";27 @Input() theme?: Theme;2829 readonly headers: AngularColumnDef<FilterableEmployee>[] = externalFilterConfig.headers;30 private filters: TableFilterState<FilterableEmployee> = {};3132 handleFilterChange = (newFilters: TableFilterState<FilterableEmployee>) => {33 this.filters = newFilters;34 };3536 get filteredRows(): FilterableEmployee[] {37 const entries = Object.entries(this.filters);38 if (entries.length === 0) return externalFilterConfig.rows ;3940 return (externalFilterConfig.rows ).filter((row) =>41 entries.every(([accessor, filter]) => {42 if (!isFilterableKey(accessor)) return true;43 return matchesFilter(row[accessor], filter);44 }),45 );46 }4748 getRowId = ({ row }: GetRowIdParams<FilterableEmployee>) => row.id;49}505152// external-filter.demo-data.ts53// Self-contained demo table setup for this example.54import type { AngularColumnDef, TableFilterState, ValueFormatterProps } from "@simple-table/angular";5556export interface FilterableEmployee {57 id: number;58 name: string;59 age: number;60 email: string;61 salary: number;62 department: string;63 active: boolean;64 location: string;65}6667type FilterableKey = keyof FilterableEmployee;6869export function isFilterableKey(accessor: string): accessor is FilterableKey {70 return (71 accessor === "id" ||72 accessor === "name" ||73 accessor === "age" ||74 accessor === "email" ||75 accessor === "salary" ||76 accessor === "department" ||77 accessor === "active" ||78 accessor === "location"79 );80}8182type CellValue = string | number | boolean | null | undefined;8384export function matchesFilter(85 value: CellValue,86 filter: TableFilterState<FilterableEmployee>[string]87): boolean {88 const { operator } = filter;8990 switch (operator) {91 case "equals":92 return value === filter.value;93 case "notEquals":94 return value !== filter.value;95 case "contains":96 return String(value).toLowerCase().includes(String(filter.value).toLowerCase());97 case "notContains":98 return !String(value).toLowerCase().includes(String(filter.value).toLowerCase());99 case "startsWith":100 return String(value).toLowerCase().startsWith(String(filter.value).toLowerCase());101 case "endsWith":102 return String(value).toLowerCase().endsWith(String(filter.value).toLowerCase());103 case "greaterThan":104 return Number(value) > Number(filter.value);105 case "lessThan":106 return Number(value) < Number(filter.value);107 case "greaterThanOrEqual":108 return Number(value) >= Number(filter.value);109 case "lessThanOrEqual":110 return Number(value) <= Number(filter.value);111 case "between":112 return (113 filter.values != null &&114 Number(value) >= Number(filter.values[0]) &&115 Number(value) <= Number(filter.values[1])116 );117 case "in":118 return filter.values != null && filter.values.includes(value);119 case "notIn":120 return filter.values != null && !filter.values.includes(value);121 case "isEmpty":122 return value == null || value === "";123 case "isNotEmpty":124 return value != null && value !== "";125 default:126 return true;127 }128}129130const DEPARTMENT_OPTIONS = [131 { label: "AI Research", value: "AI Research" },132 { label: "UX Design", value: "UX Design" },133 { label: "DevOps", value: "DevOps" },134 { label: "Marketing", value: "Marketing" },135 { label: "Engineering", value: "Engineering" },136 { label: "Product", value: "Product" },137 { label: "Sales", value: "Sales" },138];139140const LOCATION_OPTIONS = [141 { label: "San Francisco", value: "San Francisco" },142 { label: "Tokyo", value: "Tokyo" },143 { label: "Lagos", value: "Lagos" },144 { label: "Mexico City", value: "Mexico City" },145 { label: "Kolkata", value: "Kolkata" },146 { label: "Stockholm", value: "Stockholm" },147 { label: "Dubai", value: "Dubai" },148 { label: "Milan", value: "Milan" },149 { label: "Seoul", value: "Seoul" },150 { label: "Austin", value: "Austin" },151 { label: "London", value: "London" },152 { label: "Moscow", value: "Moscow" },153];154155export const externalFilterData: FilterableEmployee[] = [156 { id: 1, name: "Dr. Elena Vasquez", age: 42, email: "elena.vasquez@techcorp.com", salary: 145000, department: "AI Research", active: true, location: "San Francisco" },157 { id: 2, name: "Kai Tanaka", age: 29, email: "k.tanaka@techcorp.com", salary: 95000, department: "UX Design", active: true, location: "Tokyo" },158 { id: 3, name: "Amara Okafor", age: 35, email: "amara.okafor@techcorp.com", salary: 125000, department: "DevOps", active: false, location: "Lagos" },159 { id: 4, name: "Santiago Rodriguez", age: 27, email: "s.rodriguez@techcorp.com", salary: 82000, department: "Marketing", active: true, location: "Mexico City" },160 { id: 5, name: "Priya Chakraborty", age: 33, email: "priya.c@techcorp.com", salary: 118000, department: "Engineering", active: true, location: "Kolkata" },161 { id: 6, name: "Magnus Eriksson", age: 38, email: "magnus.erik@techcorp.com", salary: 110000, department: "Product", active: false, location: "Stockholm" },162 { id: 7, name: "Zara Al-Rashid", age: 31, email: "zara.alrashid@techcorp.com", salary: 98000, department: "Sales", active: true, location: "Dubai" },163 { id: 8, name: "Luca Rossi", age: 26, email: "luca.rossi@techcorp.com", salary: 75000, department: "Marketing", active: true, location: "Milan" },164 { id: 9, name: "Dr. Sarah Kim", age: 45, email: "sarah.kim@techcorp.com", salary: 165000, department: "AI Research", active: true, location: "Seoul" },165 { id: 10, name: "Olumide Adebayo", age: 30, email: "olumide.a@techcorp.com", salary: 105000, department: "Engineering", active: false, location: "Austin" },166 { id: 11, name: "Isabella Chen", age: 24, email: "isabella.chen@techcorp.com", salary: 68000, department: "UX Design", active: true, location: "London" },167 { id: 12, name: "Dmitri Volkov", age: 39, email: "dmitri.volkov@techcorp.com", salary: 135000, department: "DevOps", active: true, location: "Moscow" },168];169170export const externalFilterHeaders: AngularColumnDef<FilterableEmployee, any>[] = [171 { accessor: "name", label: "Name", width: "1fr", minWidth: 120, filterable: true, type: "string" },172 { accessor: "age", label: "Age", width: 120, filterable: true, type: "number" },173 {174 accessor: "department",175 label: "Department",176 width: 150,177 filterable: true,178 type: "enum",179 enumOptions: DEPARTMENT_OPTIONS,180 },181 {182 accessor: "location",183 label: "Location",184 width: 150,185 filterable: true,186 type: "enum",187 enumOptions: LOCATION_OPTIONS,188 },189 { accessor: "active", label: "Active", width: 120, filterable: true, type: "boolean" },190 {191 accessor: "salary",192 label: "Salary",193 width: 120,194 filterable: true,195 type: "number",196 align: "right",197 valueFormatter: ({ value }: ValueFormatterProps<FilterableEmployee, number>) => `$${value.toLocaleString()}`,198 },199];200201export const externalFilterConfig = {202 headers: externalFilterHeaders,203 rows: externalFilterData,204 tableProps: { externalFilterHandling: true, columnResizing: true },205};206
Vue SFC
Copy
1<script setup lang="ts">2import { ref, computed } from "vue";3import { SimpleTable } from "@simple-table/vue";4import type { Theme, TableFilterState, GetRowIdParams } from "@simple-table/vue";5import { externalFilterConfig, matchesFilter } from "./external-filter.demo-data";6import type { FilterableEmployee } from "./external-filter.demo-data";7import "@simple-table/vue/styles.css";89const props = withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {10 height: "400px",11});1213const filters = ref<TableFilterState<FilterableEmployee>>({});1415const getRowId = ({ row }: GetRowIdParams<FilterableEmployee>) => row.id;1617const filteredRows = computed(() => {18 const entries = Object.entries(filters.value);19 if (entries.length === 0) return externalFilterConfig.rows;2021 return externalFilterConfig.rows.filter((row) =>22 entries.every(([accessor, filter]) =>23 matchesFilter(row[accessor as keyof FilterableEmployee], filter)24 )25 );26});2728function handleFilterChange(newFilters: TableFilterState<FilterableEmployee>) {29 filters.value = newFilters;30}31</script>3233<template>34 <SimpleTable35 :columns="externalFilterConfig.headers"36 :rows="filteredRows"37 :get-row-id="getRowId"38 :external-filter-handling="true"39 :column-resizing="true"40 :height="props.height"41 :theme="props.theme"42 :on-filter-change="handleFilterChange"43 />44</template>
Svelte
Copy
1<script lang="ts">2 import { SimpleTable } from "@simple-table/svelte";3 import type { Theme, TableFilterState, GetRowIdParams } from "@simple-table/svelte";4 import { externalFilterConfig, matchesFilter } from "./external-filter.demo-data";5 import type { FilterableEmployee } from "./external-filter.demo-data";6 import "@simple-table/svelte/styles.css";78 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();910 let filters = $state<TableFilterState<FilterableEmployee>>({});1112 const getRowId = ({ row }: GetRowIdParams<FilterableEmployee>) => row.id;1314 let filteredRows = $derived.by(() => {15 const entries = Object.entries(filters);16 if (entries.length === 0) return externalFilterConfig.rows;1718 return externalFilterConfig.rows.filter((row) =>19 entries.every(([accessor, filter]) =>20 matchesFilter(row[accessor as keyof FilterableEmployee], filter),21 ),22 );23 });2425 function handleFilterChange(newFilters: TableFilterState<FilterableEmployee>) {26 filters = newFilters;27 }28</script>2930<SimpleTable31 columns={externalFilterConfig.headers}32 rows={filteredRows}33 {getRowId}34 externalFilterHandling={true}35 columnResizing={true}36 onFilterChange={handleFilterChange}37 {height}38 {theme}39/>
Solid TSX
Copy
1import { createSignal, createMemo } from "solid-js";2import { SimpleTable } from "@simple-table/solid";3import type { Theme, TableFilterState } from "@simple-table/solid";4import {5 externalFilterConfig,6 matchesFilter,7 type FilterableEmployee,8} from "./external-filter.demo-data";9import "@simple-table/solid/styles.css";1011export default function ExternalFilterDemo(props: { height?: string | number; theme?: Theme }) {12 const [filters, setFilters] = createSignal<TableFilterState<FilterableEmployee>>({});1314 const filteredData = createMemo(() => {15 const entries = Object.entries(filters());16 if (entries.length === 0) return externalFilterConfig.rows;1718 return externalFilterConfig.rows.filter((row) =>19 entries.every(([accessor, filter]) =>20 matchesFilter(row[accessor as keyof FilterableEmployee], filter),21 ),22 );23 });2425 return (26 <SimpleTable27 columns={externalFilterConfig.headers}28 getRowId={({ row }) => row.id}29 rows={filteredData()}30 onFilterChange={setFilters}31 externalFilterHandling32 columnResizing33 height={props.height ?? "400px"}34 theme={props.theme}35 />36 );37}
TypeScriptExternalFilterDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { FilterableEmployee } from "./external-filter.demo-data";3import type { Theme, TableFilterState, GetRowIdParams } from "simple-table-core";4import { externalFilterConfig, matchesFilter } from "./external-filter.demo-data";5import "simple-table-core/styles.css";678const getRowId = ({ row }: GetRowIdParams<FilterableEmployee>) => row.id;910type FilterableKey = keyof FilterableEmployee;1112function isFilterableKey(accessor: string): accessor is FilterableKey {13 return (14 accessor === "id" ||15 accessor === "name" ||16 accessor === "age" ||17 accessor === "email" ||18 accessor === "salary" ||19 accessor === "department" ||20 accessor === "active" ||21 accessor === "location"22 );23}2425export function renderExternalFilterDemo(26 container: HTMLElement,27 options?: { height?: string | number; theme?: Theme }28): SimpleTableVanilla<FilterableEmployee> {29 let currentFilters: TableFilterState<FilterableEmployee> = {};3031 const applyFilters = () => {32 const entries = Object.entries(currentFilters);33 if (entries.length === 0) {34 table.update({ rows: externalFilterConfig.rows });35 return;36 }37 const filtered = externalFilterConfig.rows.filter((row) =>38 entries.every(([accessor, filter]) =>39 isFilterableKey(accessor) ? matchesFilter(row[accessor], filter) : true40 )41 );42 table.update({ rows: filtered });43 };4445 const table = new SimpleTableVanilla(container, {46 getRowId,47 columns: externalFilterConfig.headers,48 rows: externalFilterConfig.rows,49 externalFilterHandling: true,50 columnResizing: true,51 height: options?.height ?? "400px",52 theme: options?.theme,53 onFilterChange: (newFilters: TableFilterState<FilterableEmployee>) => {54 currentFilters = newFilters;55 applyFilters();56 },57 });5859 return table;60}616263// external-filter.demo-data.ts64// Self-contained demo table setup for this example.65import type { ColumnDef, TableFilterState } from "simple-table-core";6667export interface FilterableEmployee {68 id: number;69 name: string;70 age: number;71 email: string;72 salary: number;73 department: string;74 active: boolean;75 location: string;76}7778type CellValue = string | number | boolean | null | undefined;7980export function matchesFilter(81 value: CellValue,82 filter: TableFilterState<FilterableEmployee>[string]83): boolean {84 const { operator } = filter;8586 switch (operator) {87 case "equals":88 return value === filter.value;89 case "notEquals":90 return value !== filter.value;91 case "contains":92 return String(value).toLowerCase().includes(String(filter.value).toLowerCase());93 case "notContains":94 return !String(value).toLowerCase().includes(String(filter.value).toLowerCase());95 case "startsWith":96 return String(value).toLowerCase().startsWith(String(filter.value).toLowerCase());97 case "endsWith":98 return String(value).toLowerCase().endsWith(String(filter.value).toLowerCase());99 case "greaterThan":100 return Number(value) > Number(filter.value);101 case "lessThan":102 return Number(value) < Number(filter.value);103 case "greaterThanOrEqual":104 return Number(value) >= Number(filter.value);105 case "lessThanOrEqual":106 return Number(value) <= Number(filter.value);107 case "between":108 return (109 filter.values != null &&110 Number(value) >= Number(filter.values[0]) &&111 Number(value) <= Number(filter.values[1])112 );113 case "in":114 return filter.values != null && filter.values.includes(value);115 case "notIn":116 return filter.values != null && !filter.values.includes(value);117 case "isEmpty":118 return value == null || value === "";119 case "isNotEmpty":120 return value != null && value !== "";121 default:122 return true;123 }124}125126const DEPARTMENT_OPTIONS = [127 { label: "AI Research", value: "AI Research" },128 { label: "UX Design", value: "UX Design" },129 { label: "DevOps", value: "DevOps" },130 { label: "Marketing", value: "Marketing" },131 { label: "Engineering", value: "Engineering" },132 { label: "Product", value: "Product" },133 { label: "Sales", value: "Sales" },134];135136const LOCATION_OPTIONS = [137 { label: "San Francisco", value: "San Francisco" },138 { label: "Tokyo", value: "Tokyo" },139 { label: "Lagos", value: "Lagos" },140 { label: "Mexico City", value: "Mexico City" },141 { label: "Kolkata", value: "Kolkata" },142 { label: "Stockholm", value: "Stockholm" },143 { label: "Dubai", value: "Dubai" },144 { label: "Milan", value: "Milan" },145 { label: "Seoul", value: "Seoul" },146 { label: "Austin", value: "Austin" },147 { label: "London", value: "London" },148 { label: "Moscow", value: "Moscow" },149];150151export const externalFilterData: FilterableEmployee[] = [152 { id: 1, name: "Dr. Elena Vasquez", age: 42, email: "elena.vasquez@techcorp.com", salary: 145000, department: "AI Research", active: true, location: "San Francisco" },153 { id: 2, name: "Kai Tanaka", age: 29, email: "k.tanaka@techcorp.com", salary: 95000, department: "UX Design", active: true, location: "Tokyo" },154 { id: 3, name: "Amara Okafor", age: 35, email: "amara.okafor@techcorp.com", salary: 125000, department: "DevOps", active: false, location: "Lagos" },155 { id: 4, name: "Santiago Rodriguez", age: 27, email: "s.rodriguez@techcorp.com", salary: 82000, department: "Marketing", active: true, location: "Mexico City" },156 { id: 5, name: "Priya Chakraborty", age: 33, email: "priya.c@techcorp.com", salary: 118000, department: "Engineering", active: true, location: "Kolkata" },157 { id: 6, name: "Magnus Eriksson", age: 38, email: "magnus.erik@techcorp.com", salary: 110000, department: "Product", active: false, location: "Stockholm" },158 { id: 7, name: "Zara Al-Rashid", age: 31, email: "zara.alrashid@techcorp.com", salary: 98000, department: "Sales", active: true, location: "Dubai" },159 { id: 8, name: "Luca Rossi", age: 26, email: "luca.rossi@techcorp.com", salary: 75000, department: "Marketing", active: true, location: "Milan" },160 { id: 9, name: "Dr. Sarah Kim", age: 45, email: "sarah.kim@techcorp.com", salary: 165000, department: "AI Research", active: true, location: "Seoul" },161 { id: 10, name: "Olumide Adebayo", age: 30, email: "olumide.a@techcorp.com", salary: 105000, department: "Engineering", active: false, location: "Austin" },162 { id: 11, name: "Isabella Chen", age: 24, email: "isabella.chen@techcorp.com", salary: 68000, department: "UX Design", active: true, location: "London" },163 { id: 12, name: "Dmitri Volkov", age: 39, email: "dmitri.volkov@techcorp.com", salary: 135000, department: "DevOps", active: true, location: "Moscow" },164];165166export const externalFilterHeaders: ColumnDef<FilterableEmployee>[] = [167 { accessor: "name", label: "Name", width: "1fr", minWidth: 120, filterable: true, type: "string" },168 { accessor: "age", label: "Age", width: 120, filterable: true, type: "number" },169 {170 accessor: "department",171 label: "Department",172 width: 150,173 filterable: true,174 type: "enum",175 enumOptions: DEPARTMENT_OPTIONS,176 },177 {178 accessor: "location",179 label: "Location",180 width: 150,181 filterable: true,182 type: "enum",183 enumOptions: LOCATION_OPTIONS,184 },185 { accessor: "active", label: "Active", width: 120, filterable: true, type: "boolean" },186 {187 accessor: "salary",188 label: "Salary",189 width: 120,190 filterable: true,191 type: "number",192 align: "right",193 valueFormatter: ({ value }) => `$${Number(value).toLocaleString()}`,194 },195];196197export const externalFilterConfig = {198 headers: externalFilterHeaders,199 rows: externalFilterData,200 tableProps: { externalFilterHandling: true, columnResizing: true },201};202
Props
Column Filtering Configuration
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
ColumnDef.filterableboolean | Optional | Enables the header filter control for the column. Operators depend on column type. | |
ColumnDef.filterOperatorsFilterOperator[] | Optional | Restricts which operators appear (in this order). Only valid for the column type. No effect on enum columns. | |
onFilterChange | Optional | Fires when active filters change. Keys are accessors; values are FilterCondition. | |
externalFilterHandlingboolean | Optional | Disables internal filtering. Provide already-filtered rows (e.g. from your API). |