Filters

Intuitive data filtering components.

Import#

  • FiltersProvider: Context provider that manages active filters.
  • FiltersAddButton: Menu with available filters.
  • ActiveFilter: Active filter component.
  • ActiveFiltersList: A list of active filters.
  • NoFilteredResults: EmptyState used incombination with DataGrid.
  • getDataGridFilter: Utility to enable DataGrid filtering.
import {
FiltersProvider,
FiltersAddButton,
ActiveFilter,
ActiveFilters,
FiltersProvider,
NoFilteredResults,
getDataGridFilter,
} from '@saas-ui-pro/react'

Usage#

The FiltersProvider can be used together with DataGrid to create intuitive data heavy list pages.

Basic#

Filters can be added to the FiltersProvider component. The FiltersAddButton component will render a menu with available filters. The ActiveFiltersList component is best used together with the Page component. It will render a list of active filters.

In this example we have a filter on the type, with two possible values: lead and customer. Use the activeLabel property to render a different label when the filter is active.

import { Spacer, Badge } from '@chakra-ui/react'
import {
Page,
PageHeader,
PageBody,
Toolbar,
FiltersProvider,
FiltersAddButton,
ActiveFiltersList,
} from '@saas-ui-pro/react'
import { FiCircle } from 'react-icons/fi'
export default function ListPage() {
const filters = React.useMemo(
() => [
{
id: 'type',
label: 'Contact is lead',
activeLabel: 'Contact',
icon: <FiUser />,
value: 'lead',
},
{
id: 'type',
label: 'Contact is customer',
activeLabel: 'Contact',
icon: <FiUser />,
value: 'customer',
},
],
[]
)
return (
<FiltersProvider filters={filters}>
<Page>
<PageHeader
title="Contacts"
toolbar={
<Toolbar variant="outline">
<FiltersAddButton />
<Spacer />
</Toolbar>
}
/>
<ActiveFiltersList />
<PageBody></PageBody>
</Page>
</FiltersProvider>
)
}

Nested values#

Filters support multiple levels of nesting. This is a useful pattern where users can select a property, eg status and then can choose from a list of available values, eg New, Active, etc.

Here's a basic example of how that works:

import { Spacer, Badge } from '@chakra-ui/react'
import {
Page,
PageHeader,
PageBody,
Toolbar,
FiltersProvider,
FiltersAddButton,
ActiveFiltersList,
} from '@saas-ui-pro/react'
import { FiCircle } from 'react-icons/fi'
export default function ListPage() {
const filters = React.useMemo(
() => [
{
id: 'status',
label: 'Status',
icon: <FiCircle />,
type: 'enum',
items: [
{
id: 'new',
label: 'New',
icon: (
<Badge boxSize="8px" mx="2px" borderRadius="full" bg="blue.400" />
),
},
{
id: 'active',
label: 'Active',
icon: (
<Badge
boxSize="8px"
mx="2px"
borderRadius="full"
bg="green.400"
/>
),
},
],
},
],
[]
)
return (
<FiltersProvider filters={filters}>
<Page>
<PageHeader
title="Contacts"
toolbar={
<Toolbar variant="outline">
<FiltersAddButton />
<Spacer />
</Toolbar>
}
/>
<ActiveFiltersList />
<PageBody></PageBody>
</Page>
</FiltersProvider>
)
}

Custom user input#

There are cases where you want to allow users to enter custom values, for example when filtering by an amount or date. Instead of using the enum type, you can use the string type and return a custom Input component in the renderValue function for a specific filter.

import { Spacer, Badge } from '@chakra-ui/react'
import {
Page,
PageHeader,
PageBody,
Toolbar,
FiltersProvider,
FiltersAddButton,
ActiveFiltersList,
ActiveFilterValueInput
} from '@saas-ui-pro/react'
import { FiTag } from 'react-icons/fi'
const getTags = async (query: string) => {
const tags = ['new', 'active', 'lead']
return new Promise((resolve) => {
setTimeout(() => {
resolve(tags.filter((tag) => tag.match(query)))
}, 1000)
})
}
export default function ListPage() {
const filters = React.useMemo(
() => [
{
id: 'likes',
label: 'Likes',
icon: <FiHeart />,
type: 'number',
defaultOperator: 'moreThan',
},
],
[]
)
const renderValue: FilterRenderFn = React.useCallback((context) => {
if (context.id === 'likes') {
return <ActiveFilterValueInput bg='none' />
}
return context.value?.toLocaleString()
}, [])
return (
<FiltersProvider filters={filters}>
<Page>
<PageHeader
title="Contacts"
toolbar={
<Toolbar variant="outline">
<FiltersAddButton />
<Spacer />
</Toolbar>
}
/>
<ActiveFiltersList renderValue={renderValue} />
<PageBody></PageBody>
</Page>
</FiltersProvider>
)
}

Input from modal#

Another way to collect user input is to use a modal. This is useful when you want to select dates with a DatePicker. This can be achieved using the onBeforeEnableFilter callback. In this example we have a filter for createdAt and we want to allow users to select a date from a list of predefined options or a custom date.

import { Spacer } from '@chakra-ui/react'
import {
useModals
} from '@saas-ui/react'
import {
DatePickerModal,
DateValue,
} from '@saas-ui/date-picker'
import {
Page,
PageHeader,
PageBody,
Toolbar,
Filter,
FilterItem,
FiltersProvider,
FiltersAddButton,
ActiveFiltersList,
ActiveFilterValueInput
} from '@saas-ui-pro/react'
import { FiCalendar, FiTag } from 'react-icons/fi'
import {startOfDay, subDays, formatDistanceToNowStrict} from 'date-fns'
const getTags = async (query: string) => {
const tags = ['new', 'active', 'lead']
return new Promise((resolve) => {
setTimeout(() => {
resolve(tags.filter((tag) => tag.match(query)))
}, 1000)
})
}
const days = [1, 2, 3, 7, 14, 21, 31, 60]
export default function ListPage() {
const modals = useModals()
const filters = React.useMemo(
() => [
{
id: 'createdAt',
label: 'Created at',
icon: <FiCalendar />,
type: 'date',
operators: ['after', 'before'],
defaultOperator: 'after',
items: days
.map((day): FilterItem => {
const date = startOfDay(subDays(new Date(), day))
return {
id: `${day}days`,
label: formatDistanceToNowStrict(date, { addSuffix: true }),
value: date,
}
})
.concat([{ id: 'custom', label: 'Custom' }]),
},
],
[]
)
const onBeforeEnableFilter = React.useCallback(
(activeFilter: Filter, filter: FilterItem): Promise<Filter> => {
return new Promise((resolve, reject) => {
const { key, id, value } = activeFilter
const { type, label } = filter
if (type === 'date' && value === 'custom') {
return modals.open({
title: label,
date: new Date(),
onSubmit: (date: DateValue) => {
resolve({
key,
id,
value: date.toDate(getLocalTimeZone()),
operator: 'after',
})
},
onClose: () => reject(),
component: DatePickerModal,
})
}
resolve(activeFilter)
})
},
[],
)
return (
<FiltersProvider filters={filters} onBeforeEnableFilter={onBeforeEnableFilter}>
<Page>
<PageHeader
title="Contacts"
toolbar={
<Toolbar variant="outline">
<FiltersAddButton />
<Spacer />
</Toolbar>
}
/>
<ActiveFiltersList />
<PageBody></PageBody>
</Page>
</FiltersProvider>
)
}

Async filters#

Filter items can be async. This is useful when you need to fetch data from an API.

The items handler receives the current query, filter id and value as arguments.

Values returned from the items handler will be cached, but the handler will execute on every change. It's up to you to make sure any requests get deduped, using React Query or similar.

import { Spacer, Badge } from '@chakra-ui/react'
import {
Page,
PageHeader,
PageBody,
Toolbar,
FiltersProvider,
FiltersAddButton,
ActiveFiltersList,
} from '@saas-ui-pro/react'
import { FiTag } from 'react-icons/fi'
const getTags = async (query: string) => {
const tags = ['new', 'active', 'lead']
return new Promise((resolve) => {
setTimeout(() => {
resolve(tags.filter((tag) => tag.match(query)))
}, 1000)
})
}
export default function ListPage() {
const filters = React.useMemo(
() => [
{
id: 'tags',
label: 'Tag',
icon: <FiTag />,
type: 'enum',
items: async ({ query, id, value }) => {
console.log('query', query)
const tags = await getTags(query)
return tags.map((tag) => ({
id: tag,
label: tag,
}))
},
},
],
[]
)
return (
<FiltersProvider filters={filters}>
<Page>
<PageHeader
title="Contacts"
toolbar={
<Toolbar variant="outline">
<FiltersAddButton />
<Spacer />
</Toolbar>
}
/>
<ActiveFiltersList />
<PageBody></PageBody>
</Page>
</FiltersProvider>
)
}

Usage with DataGrid#

Filters can be used together with DataGrid or Tanstack Table using the getDataGridFilter helper function.

import { Spacer, Badge } from '@chakra-ui/react'
import {
Page,
PageHeader,
PageBody,
Toolbar,
FiltersProvider,
FiltersAddButton,
ActiveFiltersList,
getDataGridFilter,
} from '@saas-ui-pro/react'
import { FiCircle, FiUser } from 'react-icons/fi'
export default function ListPage() {
const gridRef = useRef()
const filters = React.useMemo(
() => [
{
id: 'status',
label: 'Status',
icon: <FiCircle />,
type: 'enum',
items: [
{
id: 'new',
label: 'New',
icon: (
<Badge boxSize="8px" mx="2px" borderRadius="full" bg="blue.400" />
),
},
{
id: 'active',
label: 'Active',
icon: (
<Badge
boxSize="8px"
mx="2px"
borderRadius="full"
bg="green.400"
/>
),
},
],
},
{
id: 'isLead',
label: 'Is lead',
type: 'boolean',
icon: <FiUser />,
value: true,
},
],
[]
)
const columns = React.useMemo(() => {
return [
{
accessorKey: 'name',
header: 'Name',
size: 200,
meta: {
href: ({ id }) => `#customers/${id}`,
},
filterFn: getDataGridFilter('string'),
},
{
accessorKey: 'email',
header: 'Email',
filterFn: getDataGridFilter('string'),
},
{
accessorKey: 'company',
header: 'Company',
filterFn: getDataGridFilter('string'),
},
{
accessorKey: 'status',
header: 'Status',
cell: (cell) => {
const value = cell.getValue()
return (
<Tag
colorScheme={value === 'new' ? 'blue' : 'green'}
size="sm"
variant="outline"
>
{value}
</Tag>
)
},
filterFn: getDataGridFilter('string'),
},
{
accessorKey: 'isLead',
header: 'Lead',
hidden: true,
filterFn: getDataGridFilter('boolean'),
},
{
accessorKey: 'employees',
header: 'Employees',
meta: {
isNumeric: true,
},
},
{
id: 'action',
disableSortBy: true,
disableGlobaFilter: true,
header: '',
cell: () => (
<>
<Button size="xs">Edit</Button>
</>
),
size: 100,
},
]
}, [])
const onFilter = React.useCallback((filters) => {
gridRef.current.setColumnFilters(
filters.map((filter) => {
return {
id: filter.id,
value: {
value: filter.value,
operator: filter.operator || 'is',
},
}
})
)
}, [])
const data = React.useMemo(
() =>
dataTable.data.map((item) => {
return {
...item,
status: 'new',
isLead: true,
}
}),
[]
)
return (
<FiltersProvider filters={filters} onChange={onFilter}>
<Page height="400px">
<PageHeader
title="Contacts"
toolbar={
<Toolbar variant="outline">
<FiltersAddButton />
<Spacer />
</Toolbar>
}
/>
<ActiveFiltersList />
<PageBody>
<DataGrid
instanceRef={gridRef}
columns={columns}
data={data}
noResults={NoFilteredResults}
initialState={{
columnVisibility: {
isLead: false,
},
}}
/>
</PageBody>
</Page>
</FiltersProvider>
)
}

DataGrid with default filters#

import { Spacer, Badge } from '@chakra-ui/react'
import {
Page,
PageHeader,
PageBody,
Toolbar,
FiltersProvider,
FiltersAddButton,
ActiveFiltersList,
getDataGridFilter,
} from '@saas-ui-pro/react'
import { FiCircle, FiUser } from 'react-icons/fi'
export default function ListPage() {
const gridRef = useRef()
const filters = React.useMemo(
() => [
{
id: 'status',
label: 'Status',
icon: <FiCircle />,
type: 'enum',
items: [
{
id: 'new',
label: 'New',
icon: (
<Badge boxSize="8px" mx="2px" borderRadius="full" bg="blue.400" />
),
},
{
id: 'active',
label: 'Active',
icon: (
<Badge
boxSize="8px"
mx="2px"
borderRadius="full"
bg="green.400"
/>
),
},
],
},
{
id: 'isLead',
label: 'Is lead',
type: 'boolean',
icon: <FiUser />,
value: true,
},
],
[]
)
const columns = React.useMemo(() => {
return [
{
accessorKey: 'name',
header: 'Name',
size: 200,
meta: {
href: ({ id }) => `#customers/${id}`,
},
filterFn: getDataGridFilter('string'),
},
{
accessorKey: 'email',
header: 'Email',
filterFn: getDataGridFilter('string'),
},
{
accessorKey: 'company',
header: 'Company',
filterFn: getDataGridFilter('string'),
},
{
accessorKey: 'status',
header: 'Status',
cell: (cell) => {
const value = cell.getValue()
return (
<Tag colorScheme={value === 'new' ? 'orange' : 'green'} size="sm">
{value}
</Tag>
)
},
filterFn: getDataGridFilter('string'),
},
{
accessorKey: 'isLead',
header: 'Lead',
hidden: true,
filterFn: getDataGridFilter('boolean'),
},
{
accessorKey: 'employees',
header: 'Employees',
meta: {
isNumeric: true,
},
},
{
id: 'action',
disableSortBy: true,
disableGlobaFilter: true,
header: '',
cell: () => (
<>
<Button size="xs">Edit</Button>
</>
),
size: 100,
},
]
}, [])
const onFilter = React.useCallback((filters) => {
gridRef.current.setColumnFilters(
filters.map((filter) => {
return {
id: filter.id,
value: {
value: filter.value,
operator: filter.operator || 'is',
},
}
})
)
}, [])
const data = React.useMemo(
() =>
dataTable.data.map((item) => {
return {
...item,
status: 'new',
isLead: true,
}
}),
[]
)
const defaultFilters = React.useMemo(
() => [{ id: 'status', operator: 'isNot', value: 'new' }],
[]
)
return (
<FiltersProvider
filters={filters}
onChange={onFilter}
defaultFilters={defaultFilters}
>
<Page height="400px">
<PageHeader
title="Contacts"
toolbar={
<Toolbar variant="outline">
<FiltersAddButton />
<Spacer />
</Toolbar>
}
/>
<ActiveFiltersList />
<PageBody>
<DataGrid
instanceRef={gridRef}
columns={columns}
data={data}
noResults={NoFilteredResults}
initialState={{
columnVisibility: {
isLead: false,
},
filters: defaultFilters.map(({ id, value, operator }) => ({
id,
value: {
value,
operator,
},
})),
}}
/>
</PageBody>
</Page>
</FiltersProvider>
)
}

Was this helpful?