Skip to main content
Version: 3.xx.xx
    Current Framework
    Headless

    6. Adding Sort and Filters

    In the previous Adding List Page section, we have displayed blog posts data in a table. Now we will learn how to add sorting and filtering to the table to user can have more control over the data.

    Sort and Filters

    The @pankod/refine-react-table package based on the Tanstack Table package. So, we can add sorting and filtering features to our table as suggested in the Tanstack Table documentation.

    Refer to the @pankod/refine-react-table useTable documentation for more information

    Tanstack Table keeps the sorting and filters states in the useTable hook. When we change the these states, the useTable hook will automatically fetch the data and update the table with the new data.

    INFORMATION

    Under the hood, sorting and filters states of Tanstack Table are converted to the CrudSorting and CrudFilter types of refine. So, when you change the Tanstack Table's sorting or filters state, useTable hook will pass the converted params to the getList method of the dataProvider.

    Since @pankod/refine-react-table provides a headless solution, there are many ways to handle filtering and sorting. In this tutorial, we will show basic examples of how to add sorting and filtering to the table.

    Adding Sorting

    Let's create a <ColumnSorter/> component to use in our table header. This component will be responsible for changing the sorting state of the table.

    src/components/table/ColumnSorter.tsx
    import { ActionIcon } from "@pankod/refine-mantine";
    import { IconChevronDown, IconSelector, IconChevronUp } from "@tabler/icons";
    import type { Column } from "@pankod/refine-react-table";

    export const ColumnSorter: React.FC<{ column: Column<any, any> }> = ({
    column,
    }) => {
    if (!column.getCanSort()) {
    return null;
    }

    const sorted = column.getIsSorted();

    return (
    <ActionIcon size="xs" onClick={column.getToggleSortingHandler()}>
    {!sorted && <IconSelector size={18} />}
    {sorted === "asc" && <IconChevronDown size={18} />}
    {sorted === "desc" && <IconChevronUp size={18} />}
    </ActionIcon>
    );
    };

    <ColumnSorter/> is a simple component that renders a button. When the user clicks on the button, the column.getToggleSortingHandler() method will be called. This method will change the sorting state of the table.

    In addition, we are using the column.getCanSort() method to check if the column is sortable. If the column is not sortable, we will not render the <ColumnSorter/> component.

    Lastly, If the column is sorted, we will render the IconChevronDown icon. Otherwise, we will render the IconSelector icon.

    TIP

    In this example, we are using the @tabler/icons package for icons. You can use any icon library you want.


    Now, we can use <ColumnSorter/> in our table header.

    1. Import the <ColumnSorter/> component.

      src/pages/blog-posts/list.tsx
      import { ColumnSorter } from "../../components/table/ColumnSorter";
    2. Add the <ColumnSorter/> component to the <th/> as a child like below.

      src/pages/blog-posts/list.tsx
      <thead>
      {getHeaderGroups().map((headerGroup) => (
      <tr key={headerGroup.id}>
      {headerGroup.headers.map((header) => (
      <th key={header.id}>
      {!header.isPlaceholder &&
      flexRender(header.column.columnDef.header, header.getContext())}
      <ColumnSorter column={header.column} />
      </th>
      ))}
      </tr>
      ))}
      </thead>
    3. Disable sorting for the actions column by setting the enableSorting property of the column to false in the column definition like below:

      src/pages/blog-posts/list.tsx
      {
      id: "actions",
      accessorKey: "id",
      header: "Actions",
      enableSorting: false,
      },

    Now, we can sort the table by clicking on the sort button in the table header.

    How can I disable sorting for a specific column?

    You can disable sorting for a specific column by setting the enableSorting property of the column to false in the column definition like below.

    {
    id: "name",
    accessorKey: "name",
    header: "Name",
    enableSorting: false,
    },

    Adding Filters

    Let's create a <ColumnFilter/> component to use in our table header. This component will be responsible for changing the filters state of the table.

    src/components/table/ColumnFilter.tsx
    import React, { useState } from "react";
    import {
    TextInput,
    Menu,
    ActionIcon,
    Stack,
    Group,
    } from "@pankod/refine-mantine";
    import { IconFilter, IconX, IconCheck } from "@tabler/icons";
    import type { Column } from "@pankod/refine-react-table";

    export const ColumnFilter: React.FC<{ column: Column<any, any> }> = ({
    column,
    }) => {
    const [state, setState] = useState(null as null | { value: any });

    if (!column.getCanFilter()) {
    return null;
    }

    const open = () =>
    setState({
    value: column.getFilterValue(),
    });

    const close = () => setState(null);

    const change = (value: any) => setState({ value });

    const clear = () => {
    column.setFilterValue(undefined);
    close();
    };

    const save = () => {
    if (!state) return;
    column.setFilterValue(state.value);
    close();
    };

    const renderFilterElement = () => {
    const FilterComponent = (column.columnDef?.meta as any)?.filterElement;

    if (!FilterComponent && !!state) {
    return (
    <TextInput
    autoComplete="off"
    value={state.value}
    onChange={(e) => change(e.target.value)}
    />
    );
    }

    return <FilterComponent value={state?.value} onChange={change} />;
    };

    return (
    <Menu
    opened={!!state}
    position="bottom"
    withArrow
    transition="scale-y"
    shadow="xl"
    onClose={close}
    width="256px"
    withinPortal
    >
    <Menu.Target>
    <ActionIcon
    size="xs"
    onClick={open}
    variant={column.getIsFiltered() ? "light" : "transparent"}
    color={column.getIsFiltered() ? "primary" : "gray"}
    >
    <IconFilter size={18} />
    </ActionIcon>
    </Menu.Target>
    <Menu.Dropdown>
    {!!state && (
    <Stack p="xs" spacing="xs">
    {renderFilterElement()}
    <Group position="right" spacing={6} noWrap>
    <ActionIcon
    size="md"
    color="gray"
    variant="outline"
    onClick={clear}
    >
    <IconX size={18} />
    </ActionIcon>
    <ActionIcon
    size="md"
    onClick={save}
    color="primary"
    variant="outline"
    >
    <IconCheck size={18} />
    </ActionIcon>
    </Group>
    </Stack>
    )}
    </Menu.Dropdown>
    </Menu>
    );
    };

    <ColumnFilter/> is a component that renders a button. When the user clicks on the button, the menu will be opened. In the menu, we are rendering the filter element of the column. By default, we are rendering an <Input/> component. However, you can render any component you want.

    Filter element is a component that renders an input element. When the user changes the value of the input element, the filter value of the column will be changed.

    <ColumnFilter/> also contains a clear and apply button. When the user clicks on the clear button, the filter value of the column will be cleared. When the user clicks on the apply button, the filter value of the column will be set.

    Now, we can use <ColumnFilter/> in our table header.

    1. Import the <ColumnFilter/> component.

      src/pages/blog-posts/list.tsx
      import { ColumnFilter } from "../../components/table/ColumnFilter";
    2. Add the <ColumnFilter/> component to the <th/> as a child like below.

      src/pages/blog-posts/list.tsx
      <thead>
      {getHeaderGroups().map((headerGroup) => (
      <tr key={headerGroup.id}>
      {headerGroup.headers.map((header) => (
      <th key={header.id}>
      {!header.isPlaceholder &&
      flexRender(header.column.columnDef.header, header.getContext())}
      <ColumnSorter column={header.column} />
      <ColumnFilter column={header.column} />
      </th>
      ))}
      </tr>
      ))}
      </thead>
    3. Change the filter operator for columns to "contains" by changing the meta property of the column definition like below:

      {
      id: "title",
      accessorKey: "title",
      header: "Title",
      meta: {
      filterOperator: "contains",
      },
      },
      {
      id: "content",
      accessorKey: "content",
      header: "Content",
      meta: {
      filterOperator: "contains",
      },
      },

      By default, the filterOperator is set to "eq". So, we have changed the filterOperator to "contains" for specific columns.

    4. Disable filtering for the "actions" column by setting the enableColumnFilter property of the column to false in the column definition like below:

      {
      id: "actions",
      accessorKey: "id",
      header: "Actions",
      enableColumnFilter: false,
      ...
      },

    Now, we can filter the table by clicking on the filter button in the table header.


    How can I use custom filter element?

    If you want to use a custom filter element, you can pass it to the filterElement property of the meta in column definition. For example, you can pass a <Select/> component like below:

    {
    id: "category",
    header: "Category",
    accessorKey: "category.id",
    meta: {
    filterElement: (props) => <Select {...props} />,
    },
    },

    In the props, the filter element will receive the value and onChange props. You can use these props to change the filter value of the column.

    How can I change the filter operator?

    By default, filter operator is "eq" for columns. You can change the filter operator by passing the filterOperator property to the meta in column definition. For example, you can change the filter operator to "contains" like below:

    {
    id: "description",
    header: "Description",
    accessorKey: "description",
    meta: {
    filterOperator: "contains",
    },
    },
    How can I disable filtering for a specific column?

    You can disable filtering for a specific column by setting the enableColumnFilter property of the column to false in the column definition like below:

    {
    id: "category",
    header: "Category",
    accessorKey: "category.id",
    enableColumnFilter: false,
    },


    Checklist