Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
2 tasks done
doberkofler opened this issue Jul 25, 2023 · 8 comments
Closed
2 tasks done
Labels
component: data grid This is the name of the generic UI component, not the React module!

Comments

@doberkofler
Copy link

Duplicates

  • I have searched the existing issues

Latest version

  • I have tested the 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:

  • a callback when the filter option from the column menu is clicked
  • a callback when the filter icon on column header when there are active filters

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

  1. 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

@doberkofler doberkofler added the status: waiting for maintainer These issues haven't been looked at yet by a maintainer label Jul 25, 2023
@zannager zannager added the component: data grid This is the name of the generic UI component, not the React module! label Jul 25, 2023
@m4theushw m4theushw removed the status: waiting for maintainer These issues haven't been looked at yet by a maintainer label Jul 25, 2023
@m4theushw
Copy link
Member

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?

@m4theushw m4theushw added the status: waiting for author Issue with insufficient information label Jul 25, 2023
@doberkofler
Copy link
Author

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

@github-actions github-actions bot removed the status: waiting for author Issue with insufficient information label Jul 27, 2023
@m4theushw m4theushw added the status: waiting for author Issue with insufficient information label Jul 27, 2023
@doberkofler
Copy link
Author

doberkofler commented Jul 28, 2023

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.

@github-actions github-actions bot removed the status: waiting for author Issue with insufficient information label Jul 28, 2023
@MBilalShafi
Copy link
Member

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?

@MBilalShafi MBilalShafi added the status: waiting for author Issue with insufficient information label Aug 29, 2023
@doberkofler
Copy link
Author

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.

@github-actions github-actions bot removed the status: waiting for author Issue with insufficient information label Aug 29, 2023
@MBilalShafi
Copy link
Member

MBilalShafi commented Sep 4, 2023

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?

@MBilalShafi MBilalShafi added the status: waiting for author Issue with insufficient information label Sep 4, 2023
@doberkofler
Copy link
Author

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.

@github-actions github-actions bot removed the status: waiting for author Issue with insufficient information label Sep 5, 2023
@MBilalShafi
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: data grid This is the name of the generic UI component, not the React module!
Projects
None yet
Development

No branches or pull requests

4 participants