mui / mui-x

MUI X: Build complex and data-rich applications using a growing list of advanced React components, like the Data Grid, Date and Time Pickers, Charts, and more!
https://mui.com/x/
4.12k stars 1.28k forks source link

[DataGrid] Completely customised UI for (server-based) filterering #9782

Closed doberkofler closed 1 year ago

doberkofler commented 1 year ago

Duplicates

Latest version

Summary 💡

The DataGrid offers an impressive list of options to customise filtering, but unfortunately sometimes a completely customised UI is needed. I'm looking for a way to intercept the actual entry points into the filtering to use my existing filtering solution.

I have a use case where the filtering is done on the server using a custom filter UI for each individual data set. The filter UI is completely externalised and does already exist and I would need to make the DataGrid use the existing filter dialogs.

Assuming that only the UI to invoke filtering offered by the DataGrid is used and the rest is custom implemented , only two callbacks (that I cannot find) would be needed:

Additionally it would only be needed to set the number of active filters for each column, so the UI can be initialised whin the predefined filter definition.

Assuming that the DataGrid would allow me call my custom code when the user invokes the above functions, it should be possible to add a customised filtering logic in a very generic way.

Examples 🌈

1) Allow to intercept when the user wants to set a new filter for a column:

image

2) Allow to intercept when to user wants to modify an existing filter:

image

Motivation 🔦

I have spend quite some time investigating all the available customisation options in DataGrid and am quite impressed. Unfortunately when a completely customised filter UI is needed this currently does not seem to be possible. As far as I understand it should be possible to completely customise the filtering by simply hooking into the entry points when setting or changing a filter.

Order ID 💳 (optional)

No response

m4theushw commented 1 year ago

You need to override the filter menu item and the column header filter icon. In https://mui.com/x/react-data-grid/column-menu/#overriding-default-menu-items there's an example for the first customization. Additionally, you need to enable the server-side filtering mode and pass some filterModel to force the header icon to be rendered, at the same time not trigger the client-side filtering logic. Since there's a lot of components to override I created an example: https://codesandbox.io/s/interesting-browser-w28tsm?file=/demo.tsx This logic could be simplified if we cut some things that the default buttons have, e.g. tooltip.

import * as React from "react";
import {
  unstable_composeClasses as composeClasses,
  unstable_useId as useId
} from "@mui/utils";
import MenuItem from "@mui/material/MenuItem";
import Badge from "@mui/material/Badge";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import IconFilter from "@mui/icons-material/FilterAlt";
import { styled } from "@mui/system";
import {
  DataGrid,
  GridColumnMenu,
  GridColumnMenuProps,
  GridColumnMenuItemProps,
  useGridApiContext,
  ColumnHeaderFilterIconButtonProps,
  useGridRootProps
} from "@mui/x-data-grid";
import { useDemoData } from "@mui/x-data-grid-generator";

function CustomFilterItem(props: GridColumnMenuItemProps) {
  const { onClick, colDef } = props;
  const apiRef = useGridApiContext();

  const handleClick = React.useCallback(
    (event: React.MouseEvent<HTMLElement>) => {
      onClick(event);
      apiRef.current.toggleColumnMenu(colDef.field);
    },
    [apiRef, colDef.field, onClick]
  );

  return (
    <MenuItem onClick={handleClick}>
      <ListItemIcon>
        <IconFilter fontSize="small" />
      </ListItemIcon>
      <ListItemText>Show Filters</ListItemText>
    </MenuItem>
  );
}

function CustomColumnMenu(
  props: GridColumnMenuProps & { showFilterPanel: () => void }
) {
  const { showFilterPanel, ...other } = props;

  return (
    <GridColumnMenu
      {...other}
      slots={{
        // Override slot for `columnMenuFilterItem`
        columnMenuFilterItem: CustomFilterItem
      }}
      slotProps={{
        columnMenuFilterItem: {
          onClick: props.showFilterPanel
        }
      }}
    />
  );
}

declare module "@mui/x-data-grid" {
  interface ColumnMenuPropsOverrides {
    showFilterPanel: () => void;
  }
  interface ColumnHeaderFilterIconButtonPropsOverrides {
    showFilterPanel: () => void;
  }
}

const GridIconButtonContainerRoot = styled("div", {
  name: "MuiDataGrid",
  slot: "IconButtonContainer"
})(() => ({
  display: "flex",
  visibility: "hidden",
  width: 0
}));

function CustomFilterIcon(
  props: ColumnHeaderFilterIconButtonProps & { showFilterPanel: () => void }
) {
  const { counter, field, onClick, showFilterPanel } = props;
  const apiRef = useGridApiContext();
  const rootProps = useGridRootProps();
  const labelId = useId();
  const panelId = useId();

  const toggleFilter = React.useCallback(
    (event: React.MouseEvent<HTMLButtonElement>) => {
      event.preventDefault();
      event.stopPropagation();
      showFilterPanel();
    },
    [apiRef, field, onClick, panelId, labelId]
  );

  if (!counter) {
    return null;
  }

  const iconButton = (
    <rootProps.slots.baseIconButton
      id={labelId}
      onClick={toggleFilter}
      color="default"
      aria-label={apiRef.current.getLocaleText("columnHeaderFiltersLabel")}
      size="small"
      tabIndex={-1}
      aria-haspopup="menu"
      {...rootProps.slotProps?.baseIconButton}
    >
      <rootProps.slots.columnFilteredIcon fontSize="small" />
    </rootProps.slots.baseIconButton>
  );

  return (
    <rootProps.slots.baseTooltip
      title={
        apiRef.current.getLocaleText("columnHeaderFiltersTooltipActive")(
          counter
        ) as React.ReactElement
      }
      enterDelay={1000}
      {...rootProps.slotProps?.baseTooltip}
    >
      <GridIconButtonContainerRoot className="MuiDataGrid-iconButtonContainer">
        {counter > 1 && (
          <Badge badgeContent={counter} color="default">
            {iconButton}
          </Badge>
        )}

        {counter === 1 && iconButton}
      </GridIconButtonContainerRoot>
    </rootProps.slots.baseTooltip>
  );
}

export default function OverrideColumnMenuGrid() {
  const { data } = useDemoData({
    dataSet: "Commodity",
    rowLength: 20,
    maxColumns: 5
  });

  const showFilterPanel = () => {
    console.log("showFilterPanel");
  };

  return (
    <div style={{ height: 400, width: "100%" }}>
      <DataGrid
        {...data}
        slots={{
          columnMenu: CustomColumnMenu,
          columnHeaderFilterIconButton: CustomFilterIcon
        }}
        slotProps={{
          columnMenu: {
            showFilterPanel
          },
          columnHeaderFilterIconButton: {
            showFilterPanel
          }
        }}
        filterModel={{
          items: [
            { id: 0, field: "commodity", value: "a", operator: "contains" }
          ]
        }}
        filterMode="server"
      />
    </div>
  );
}

Is this what you're looking for?

doberkofler commented 1 year ago

Thank you very much for the example. I will test this for my use cases and post the results.

doberkofler commented 1 year ago

Sorry, I must somehow have overlooked the concept of "Overwriting components". I now tested your example for my use cases and it works very well.

Given that this customisation is not trivial, it might still be worth exploring a simpler API to fully customise the filter panel. I imagine a single optional onFilterPanel property on the Grid allowing a callback like (colDef: GridColDef) => Promise<void> could be an interesting customisation feature.

MBilalShafi commented 1 year ago

Glad it worked for you.

Thanks for the suggestion about the API. What would you think the purpose would be for the onFilterPanel property you mentioned and how could it address your specific use case?

doberkofler commented 1 year ago

Although the customization of column filters in the ˋDataGridˋ is quite extensive, there are situation when a completely customized UI for filtering is needed. In my use case we only filter on the backend and allow complex column specific filtering including boolean operators, ranges, validations and so on. A ´onFilterPanel´ property should allow to simply interface with an externally designed filter dialog when setting or changing a column filter.

MBilalShafi commented 1 year ago

If you are looking for something like this, I believe we don't need to introduce a dedicated prop for this, it's achievable without it. You could have a filter panel and use the grid API methods to control how it interacts with the Grid.

You could also customize how default filter UI options work.

  1. Customize the default column menu item for Filter by updating the ColumnMenu slot: https://mui.com/x/react-data-grid/column-menu/#overriding-default-menu-items
  2. Provide custom slot for the header icon button (columnHeaderFilterIconButton) for defined filters: https://mui.com/x/react-data-grid/components/#component-slots
  3. Customize the header filters component used: https://mui.com/x/react-data-grid/filtering/header-filters/#customize-header-filters

Would this work for you?

doberkofler commented 1 year ago

Thank you for the feedback! The solution provided by m4theushw did work for me but requires quite a bit of custom code and I just wanted to suggest that there might be an clean and easy solution when someone needs full control over the filtering.

MBilalShafi commented 1 year ago

Thank you for the clarification. Yes we do recognize that customizing the current filter panel isn't a piece of cake right now. We have this in our roadmap to make it possibly make it a bit easier to do so.