Filters
Intuitive data filtering components.
- Beta
Buy Pro
- 0.35.5 (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.
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) => {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?