reactjsarraystypescriptmaterial-uidatagrid

How to enable filtering and sorting for a multi-value array column using MUI's data grid?


I'm using the free version of the MUI Data Grid component and I'm not able to get filtering and sorting to work correctly with a column that renders multiple values in an array. In this example, my multiple-value array column is 'tags', which shows some categories for words in a data grid.

The desired functionality is the following:

Filtering: I want the user to be able to choose a tag from a dropdown list and have the table show only the rows that contain that tag. It would also be acceptable to have the user select several tags and match on all, but it's not necessary. The expected filter menu would be something like: "Column: Tags, Operator: Contains, Value: [One or several selected tags from a list of all possible tags]".

Sorting: This is more open to interpretation, but at a minimum, it should sort all rows containing tags before/after rows that contain no tags.

Below is what I have so far, but I believe it's not working because the 'singleSelect' column type is expecting a single string value to compare against instead of an array of strings. Looking through the MUI documentation, however, I'm not able to find a corresponding column/filter option that would match a predefined value in a dropdown list against any one of the values in a corresponding string array column.

Code Sandbox

import { DataGrid, GridColDef, GridToolbar } from "@mui/x-data-grid"
import { Box, Chip, Stack } from "@mui/material"

export default function App() {
  const rows = [
    { id: 1, german: "der Kuchen", english: "cake", tags: ["food", "baked goods", "kitchen"] },
    { id: 2, german: "rot", english: "red", tags: ["colors"] },
    { id: 3, german: "das Auto", english: "car", tags: ["vehicles", "garage"] },
    { id: 4, german: "fliegend", english: "flying", tags: [] },
    { id: 5, german: "grün", english: "green", tags: ["colors", "nature"] },
    { id: 6, german: "der Hubschrauber", english: "helicopter", tags: ["vehicles"] },
    { id: 7, german: "die Gabel", english: "fork", tags: ["kitchen"] },
    { id: 8, german: "das Hemd", english: "shirt", tags: ["clothes", "closet"] },
    { id: 9, german: "tatsächlich", english: "actual", tags: [] },
    { id: 10, german: "der Bus", english: "bus", tags: ["school", "vehicles"] }
  ]

  const columns: GridColDef[] = [
    { field: "id", headerName: "ID", width: 30, filterable: false },
    { field: "german", headerName: "German", width: 150 },
    { field: "english", headerName: "English", width: 100 },
    {
      field: "tags",
      headerName: "Tags",
      width: 300,
      type: "singleSelect",
      valueOptions: [...new Set(rows.map((o) => o.tags).flat())],
      renderCell: (params) => (
        <Stack direction="row" spacing={0.25}>
          {params.row.tags.map((tag: string) => (
            <Chip label={tag} />
          ))}
        </Stack>
      )
    }
  ]

  return (
    <Box sx={{ height: 500, width: "100%" }}>
      <DataGrid
        rows={rows}
        columns={columns}
        density={"compact"}
        disableSelectionOnClick
        components={{
          Toolbar: GridToolbar
        }}
      />
    </Box>
  )
}

This is what the data grid looks like before trying to filter it: data-grid

This is an example of the filtering not working as expected (the expected behavior is that when the user selects 'vehicles', the table displays only rows 3, 6, & 10): data-grid-no-filter-results

Here you can see sorting has no effect: data-grid-sort-not-working

MUI documentation where I looked for a solution:


Solution

  • You need to set custom sortComparator and filterOperators props to the related column definition like this:

    import {
      DataGrid,
      getGridSingleSelectOperators,
      GridCellParams,
      GridColDef,
      GridComparatorFn,
      GridFilterItem,
      GridToolbar
    } from "@mui/x-data-grid"
    import { Box, Chip, Stack } from "@mui/material"
    
    const tagsSortComparator: GridComparatorFn<any> = (tags1: any, tags2: any) => {
      return tags1.length - tags2.length
    }
    
    const tagsFilterOperators = getGridSingleSelectOperators()
      .filter((operator) => operator.value === "isAnyOf")
      .map((operator) => {
        const newOperator = { ...operator }
        const newGetApplyFilterFn = (filterItem: GridFilterItem, column: GridColDef) => {
          return (params: GridCellParams): boolean => {
            let isOk = true
            filterItem?.value?.forEach((fv: any) => {
              isOk = isOk && params.value.includes(fv)
            })
            return isOk
          }
        }
        newOperator.getApplyFilterFn = newGetApplyFilterFn
        return newOperator
      })
    
    export default function App() {
      const rows = [
        { id: 1, german: "der Kuchen", english: "cake", tags: ["food", "baked goods", "kitchen"] },
        { id: 2, german: "rot", english: "red", tags: ["colors"] },
        { id: 3, german: "das Auto", english: "car", tags: ["vehicles", "garage"] },
        { id: 4, german: "fliegend", english: "flying", tags: [] },
        { id: 5, german: "grün", english: "green", tags: ["colors", "nature"] },
        { id: 6, german: "der Hubschrauber", english: "helicopter", tags: ["vehicles"] },
        { id: 7, german: "die Gabel", english: "fork", tags: ["kitchen"] },
        { id: 8, german: "das Hemd", english: "shirt", tags: ["clothes", "closet"] },
        { id: 9, german: "tatsächlich", english: "actual", tags: [] },
        { id: 10, german: "der Bus", english: "bus", tags: ["school", "vehicles"] }
      ]
    
      const columns: GridColDef[] = [
        { field: "id", headerName: "ID", width: 30, filterable: false },
        { field: "german", headerName: "German", width: 150 },
        { field: "english", headerName: "English", width: 100 },
        {
          field: "tags",
          headerName: "Tags",
          width: 300,
          type: "singleSelect",
          valueOptions: [...new Set(rows.map((o) => o.tags).flat())],
          renderCell: (params) => (
            <Stack direction="row" spacing={0.25}>
              {params.row.tags.map((tag: string) => (
                <Chip label={tag} />
              ))}
            </Stack>
          ),
          sortComparator: tagsSortComparator,
          filterOperators: tagsFilterOperators
        }
      ]
    
      return (
        <Box sx={{ height: 500, width: "100%" }}>
          <DataGrid
            rows={rows}
            columns={columns}
            density={"compact"}
            disableSelectionOnClick
            components={{
              Toolbar: GridToolbar
            }}
          />
        </Box>
      )
    }
    
    

    You can take a look at this stackblitz for a live working example of this solution.