Filtering
You have 3,000 support tickets and need to find the ones assigned to Alice that were opened last week. Click the filter icon on "Assignee", pick Alice. Click the filter on "Created Date", set the range. Done — two clicks, no code.
The demo uses React Radix styling. Each framework renders filter UI with its native components — dropdowns, date pickers, and inputs that match your design system.
Add filters to any column
Set filterable on a column definition. The type field picks the filter UI:
- React
- Angular
- Vue
- Vanilla JS
import { OGrid } from '@alaarab/ogrid-react-radix';
const columns = [
{
columnId: 'name',
name: 'Name',
filterable: { type: 'text' },
},
{
columnId: 'department',
name: 'Department',
filterable: { type: 'multiSelect', filterField: 'department' },
},
{
columnId: 'status',
name: 'Status',
filterable: { type: 'multiSelect', filterField: 'status' },
},
{
columnId: 'salary',
name: 'Salary',
type: 'numeric' as const,
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
},
];
function App() {
return (
<OGrid
columns={columns}
data={people}
getRowId={(item) => item.id}
defaultPageSize={10}
/>
);
}
Same props across all React packages — just change the import:
- Radix (lightweight, default):
from '@alaarab/ogrid-react-radix' - Fluent UI (Microsoft 365 / SPFx):
from '@alaarab/ogrid-react-fluent'- wrap in<FluentProvider> - Material UI (MUI v7):
from '@alaarab/ogrid-react-material'- wrap in<ThemeProvider>
import { Component } from '@angular/core';
import { OGridComponent, type IColumnDef } from '@alaarab/ogrid-angular-material';
@Component({
standalone: true,
imports: [OGridComponent],
template: `<ogrid [props]="gridProps" />`
})
export class GridComponent {
gridProps = {
columns: [
{ columnId: 'name', name: 'Name', filterable: { type: 'text' } },
{
columnId: 'department', name: 'Department',
filterable: { type: 'multiSelect', filterField: 'department' },
},
{
columnId: 'status', name: 'Status',
filterable: { type: 'multiSelect', filterField: 'status' },
},
{
columnId: 'salary', name: 'Salary', type: 'numeric',
valueFormatter: (v: unknown) => `$${Number(v).toLocaleString()}`,
},
] as IColumnDef<Person>[],
data: people,
getRowId: (item: Person) => item.id,
defaultPageSize: 10,
};
}
Same API across Angular packages. Change the import:
- Radix (CDK):
from '@alaarab/ogrid-angular-radix'(default, lightweight) - Angular Material:
from '@alaarab/ogrid-angular-material' - PrimeNG:
from '@alaarab/ogrid-angular-primeng'
All components are standalone — no NgModule required.
<script setup lang="ts">
import { OGrid, type IColumnDef } from '@alaarab/ogrid-vue-vuetify';
const columns: IColumnDef<Person>[] = [
{ columnId: 'name', name: 'Name', filterable: { type: 'text' } },
{
columnId: 'department', name: 'Department',
filterable: { type: 'multiSelect', filterField: 'department' },
},
{
columnId: 'status', name: 'Status',
filterable: { type: 'multiSelect', filterField: 'status' },
},
{
columnId: 'salary', name: 'Salary', type: 'numeric',
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
},
];
const gridProps = {
columns,
data: people,
getRowId: (item) => item.id,
defaultPageSize: 10,
};
</script>
<template>
<OGrid :gridProps="gridProps" />
</template>
Same API across Vue packages. Change the import:
- Radix (Headless UI):
from '@alaarab/ogrid-vue-radix'(default, lightweight) - Vuetify:
from '@alaarab/ogrid-vue-vuetify'- wrap in<v-app>for theming - PrimeVue:
from '@alaarab/ogrid-vue-primevue'
import { OGrid } from '@alaarab/ogrid-js';
import '@alaarab/ogrid-js/styles';
const grid = new OGrid(document.getElementById('grid'), {
columns: [
{ columnId: 'name', name: 'Name', filterable: { type: 'text' } },
{
columnId: 'department',
name: 'Department',
filterable: { type: 'multiSelect', filterField: 'department' },
},
{
columnId: 'status',
name: 'Status',
filterable: { type: 'multiSelect', filterField: 'status' },
},
{
columnId: 'salary',
name: 'Salary',
type: 'numeric',
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
},
],
data: people,
getRowId: (item) => item.id,
pageSize: 10,
});
Filter types
Text filter
A case-insensitive substring search. Good for names, descriptions, anything freeform:
{ columnId: 'name', name: 'Name', filterable: { type: 'text' } }
Type "john" and it matches "John Smith", "Johnny", and "Johnathan".
Multi-select filter
A checkbox list — users pick one or more values, the grid shows only matching rows. Has built-in "Select All" and "Clear All" buttons.
{
columnId: 'status',
name: 'Status',
filterable: {
type: 'multiSelect',
filterField: 'status', // field to filter on (defaults to columnId)
optionsSource: 'static', // where options come from
options: ['Active', 'Draft', 'Archived'],
},
}
Where do the options come from?
optionsSource | How it works |
|---|---|
| (omitted) | Client-side: unique values extracted from your data. Server-side: calls your API. |
'static' | Uses the options array you provide — great for known enums. |
'api' | Calls dataSource.fetchFilterOptions(field) — for dynamic option lists. |
'years' | Auto-generates a year list from the current year down. |
Date filter
A date range picker with "From" and "To" inputs. Shows only rows where the column value falls within the range:
{
columnId: 'createdDate',
name: 'Created',
type: 'date',
filterable: { type: 'date' },
}
A column with type: 'date' automatically gets date formatting, chronological sorting, and a native date input editor — you don't need to configure those separately.
People filter
A search-as-you-type lookup that hits your directory API. Designed for Microsoft Graph, LDAP, or any endpoint that returns users:
{
columnId: 'assignee',
name: 'Assignee',
filterable: { type: 'people' },
}
The people filter requires a dataSource with a searchPeople method that returns UserLike objects (name, email, optional photo).
Keeping filter state outside the grid
By default the grid owns its filter state — filters reset when the component unmounts. To persist them (URL sync, page reload survival), pass filters and onFiltersChange:
function App() {
const [filters, setFilters] = useState<IFilters>({
status: { type: 'multiSelect', value: ['Active'] },
name: { type: 'text', value: 'John' },
});
return (
<OGrid
columns={columns}
data={tasks}
getRowId={(item) => item.id}
filters={filters}
onFiltersChange={setFilters}
/>
);
}
Now you can serialize filters to the URL, localStorage, or a server — and the grid will restore the exact filter state when the user comes back.
Server-side filtering
When you're using a dataSource, the grid hands off filter values to your fetchPage() function. You decide what to do with them — query params, request body, whatever your API expects:
const dataSource: IDataSource<Task> = {
fetchPage: async ({ filters, page, pageSize, sort }) => {
const params = new URLSearchParams({
page: String(page),
pageSize: String(pageSize),
});
if (filters.status?.type === 'multiSelect') {
params.set('status', filters.status.value.join(','));
}
if (filters.name?.type === 'text') {
params.set('search', filters.name.value);
}
const res = await fetch(`/api/tasks?${params}`);
return res.json(); // { items: Task[], totalCount: number }
},
// For 'api' option source on multiSelect filters:
fetchFilterOptions: async (field) => {
const res = await fetch(`/api/tasks/filter-options/${field}`);
return res.json(); // string[]
},
// For people filters:
searchPeople: async (query) => {
const res = await fetch(`/api/users/search?q=${query}`);
return res.json(); // UserLike[]
},
};
Server-side filtering pairs naturally with pagination. The totalCount in your fetchPage response tells the grid how many pages there are — so pagination stays accurate even when filters narrow the result set.
Props reference
| Prop | Type | Default | Description |
|---|---|---|---|
filterable | IColumnFilterDef | — | Set on IColumnDef. Enables the filter icon and defines the filter UI. |
filterable.type | 'text' | 'multiSelect' | 'people' | 'date' | — | Which filter UI to show. |
filterable.filterField | string | columnId | Data field to filter on (useful when column key differs from data key). |
filterable.optionsSource | 'api' | 'static' | 'years' | auto | Where multi-select options come from. |
filterable.options | string[] | — | Static option list for 'static' source. |
filterable.yearsCount | number | — | How many years to generate for 'years' source. |
filters | IFilters | — | Controlled filter state (pass to own state externally). |
onFiltersChange | (filters: IFilters) => void | — | Called whenever filters change. |
Next steps
- Sorting — sort filtered results by any column
- Pagination — paginate filtered results server-side or client-side
- Server-Side Data — move all filtering, sorting, and paging to the server