Filters
Intuitive data filtering components.
- Beta
Buy Pro
- 0.40.0 (latest)
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><PageHeadertitle="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: (<BadgeboxSize="8px"mx="2px"borderRadius="full"bg="green.400"/>),},],},],[])return (<FiltersProvider filters={filters}><Page><PageHeadertitle="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><PageHeadertitle="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 } = activeFilterconst { type, label } = filterif (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><PageHeadertitle="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><PageHeadertitle="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: (<BadgeboxSize="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 (<TagcolorScheme={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"><PageHeadertitle="Contacts"toolbar={<Toolbar variant="outline"><FiltersAddButton /><Spacer /></Toolbar>}/><ActiveFiltersList /><PageBody><DataGridinstanceRef={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: (<BadgeboxSize="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 (<FiltersProviderfilters={filters}onChange={onFilter}defaultFilters={defaultFilters}><Page height="400px"><PageHeadertitle="Contacts"toolbar={<Toolbar variant="outline"><FiltersAddButton /><Spacer /></Toolbar>}/><ActiveFiltersList /><PageBody><DataGridinstanceRef={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?