diff --git a/docs/List.md b/docs/List.md
index 62fead24bac..f0259e6eaa2 100644
--- a/docs/List.md
+++ b/docs/List.md
@@ -2016,6 +2016,7 @@ The `Datagrid` component renders a list of records as a table. It is usually use
Here are all the props accepted by the component:
* [`body`](#body-element)
+* [`header`](#header-element)
* [`rowStyle`](#row-style-function)
* [`rowClick`](#rowclick)
* [`expand`](#expand)
@@ -2101,6 +2102,39 @@ const PostList = props => (
export default PostList;
```
+### Header Element
+
+By default, `` renders its header using ``, an internal react-admin component. You can pass a custom component as the `header` prop to override that default. This can be useful e.g. to add a second header row, or to create headers spanning multiple columns.
+
+For instance, here is a simple datagrid header that displays column names with no sort and no "select all" button:
+
+```jsx
+import { TableHead, TableRow, TableCell } from '@material-ui/core';
+
+const DatagridHeader = ({ children }) => (
+
+
+ {/* empty cell to account for the select row checkbox in the body */}
+ {Children.map(children, child => (
+
+ {child.props.source}
+
+ ))}
+
+
+);
+
+const PostList = props => (
+
+ }>
+ {/* ... */}
+
+
+);
+```
+
+**Tip**: To handle sorting in your custom Datagrid header component, check out the [Building a custom sort control](#building-a-custom-sort-control) section.
+
### Row Style Function
You can customize the `` row style (applied to the `` element) based on the record, thanks to the `rowStyle` prop, which expects a function. React-admin calls this function for each row, passing the current record and index as arguments. The function should return a style object, which react-admin uses as a `
` prop.
diff --git a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx
index 9d8f499f187..b932f953edb 100644
--- a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx
+++ b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx
@@ -1,12 +1,13 @@
import * as React from 'react';
import {
- isValidElement,
- Children,
cloneElement,
+ createElement,
+ isValidElement,
useCallback,
useRef,
useEffect,
FC,
+ ComponentType,
ReactElement,
useMemo,
} from 'react';
@@ -20,19 +21,12 @@ import {
RecordMap,
SortPayload,
} from 'ra-core';
-import {
- Checkbox,
- Table,
- TableProps,
- TableCell,
- TableHead,
- TableRow,
-} from '@material-ui/core';
+import { Table, TableProps } from '@material-ui/core';
import classnames from 'classnames';
import union from 'lodash/union';
import difference from 'lodash/difference';
-import DatagridHeaderCell from './DatagridHeaderCell';
+import { DatagridHeader } from './DatagridHeader';
import DatagridLoading from './DatagridLoading';
import DatagridBody, { PureDatagridBody } from './DatagridBody';
import useDatagridStyles from './useDatagridStyles';
@@ -113,7 +107,8 @@ const Datagrid: FC = React.forwardRef((props, ref) => {
const classes = useDatagridStyles(props);
const {
optimized = false,
- body = optimized ? : ,
+ body = optimized ? PureDatagridBody : DatagridBody,
+ header = DatagridHeader,
children,
classes: classesOverride,
className,
@@ -148,42 +143,6 @@ const Datagrid: FC = React.forwardRef((props, ref) => {
isRowExpandable,
]);
- const updateSortCallback = useCallback(
- event => {
- event.stopPropagation();
- const newField = event.currentTarget.dataset.field;
- const newOrder =
- currentSort.field === newField
- ? currentSort.order === 'ASC'
- ? 'DESC'
- : 'ASC'
- : event.currentTarget.dataset.order;
-
- setSort(newField, newOrder);
- },
- [currentSort.field, currentSort.order, setSort]
- );
-
- const updateSort = setSort ? updateSortCallback : null;
-
- const handleSelectAll = useCallback(
- event => {
- if (event.target.checked) {
- const all = ids.concat(
- selectedIds.filter(id => !ids.includes(id))
- );
- onSelect(
- isRowSelectable
- ? all.filter(id => isRowSelectable(data[id]))
- : all
- );
- } else {
- onSelect([]);
- }
- },
- [data, ids, onSelect, isRowSelectable, selectedIds]
- );
-
const lastSelected = useRef(null);
useEffect(() => {
@@ -253,10 +212,6 @@ const Datagrid: FC = React.forwardRef((props, ref) => {
return null;
}
- const all = isRowSelectable
- ? ids.filter(id => isRowSelectable(data[id]))
- : ids;
-
/**
* After the initial load, if the data for the list isn't empty,
* and even if the data is refreshing (e.g. after a filter change),
@@ -270,58 +225,26 @@ const Datagrid: FC = React.forwardRef((props, ref) => {
size={size}
{...sanitizeListRestProps(rest)}
>
-
-
- {expand && (
-
- )}
- {hasBulkActions && selectedIds && (
-
- 0 &&
- all.length > 0 &&
- all.every(id =>
- selectedIds.includes(id)
- )
- }
- onChange={handleSelectAll}
- />
-
- )}
- {Children.map(children, (field, index) =>
- isValidElement(field) ? (
-
- ) : null
- )}
-
-
- {cloneElement(
+ {createOrCloneElement(
+ header,
+ {
+ children,
+ classes,
+ className,
+ currentSort,
+ data,
+ hasExpand: !!expand,
+ hasBulkActions,
+ ids,
+ isRowSelectable,
+ onSelect,
+ resource,
+ selectedIds,
+ setSort,
+ },
+ children
+ )}
+ {createOrCloneElement(
body,
{
basePath,
@@ -347,9 +270,15 @@ const Datagrid: FC = React.forwardRef((props, ref) => {
);
});
+const createOrCloneElement = (element, props, children) =>
+ isValidElement(element)
+ ? cloneElement(element, props, children)
+ : createElement(element, props, children);
+
Datagrid.propTypes = {
basePath: PropTypes.string,
- body: PropTypes.element,
+ // @ts-ignore
+ body: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType]),
children: PropTypes.node.isRequired,
classes: PropTypes.object,
className: PropTypes.string,
@@ -362,6 +291,8 @@ Datagrid.propTypes = {
// @ts-ignore
expand: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType]),
hasBulkActions: PropTypes.bool,
+ // @ts-ignore
+ header: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType]),
hover: PropTypes.bool,
ids: PropTypes.arrayOf(PropTypes.any),
loading: PropTypes.bool,
@@ -380,7 +311,7 @@ Datagrid.propTypes = {
export interface DatagridProps
extends Omit {
- body?: ReactElement;
+ body?: ReactElement | ComponentType;
classes?: ClassesOverride;
className?: string;
expand?:
@@ -392,6 +323,7 @@ export interface DatagridProps
resource: string;
}>;
hasBulkActions?: boolean;
+ header?: ReactElement | ComponentType;
hover?: boolean;
empty?: ReactElement;
isRowSelectable?: (record: Record) => boolean;
diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridHeader.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridHeader.tsx
new file mode 100644
index 00000000000..db6f3994e0c
--- /dev/null
+++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridHeader.tsx
@@ -0,0 +1,177 @@
+import * as React from 'react';
+import { Children, isValidElement, useCallback } from 'react';
+import PropTypes from 'prop-types';
+import {
+ useListContext,
+ useResourceContext,
+ Identifier,
+ Record,
+ RecordMap,
+ SortPayload,
+} from 'ra-core';
+import { Checkbox, TableCell, TableHead, TableRow } from '@material-ui/core';
+import classnames from 'classnames';
+
+import DatagridHeaderCell from './DatagridHeaderCell';
+import useDatagridStyles from './useDatagridStyles';
+import { ClassesOverride } from '../../types';
+
+/**
+ * The default Datagrid Header component.
+ *
+ * Renders select all checkbox as well as column header buttons used for sorting.
+ */
+export const DatagridHeader = (props: DatagridHeaderProps) => {
+ const {
+ children,
+ classes,
+ className,
+ hasExpand = false,
+ hasBulkActions = false,
+ isRowSelectable,
+ } = props;
+ const resource = useResourceContext(props);
+ const {
+ currentSort,
+ data,
+ ids,
+ onSelect,
+ selectedIds,
+ setSort,
+ } = useListContext(props);
+
+ const updateSortCallback = useCallback(
+ event => {
+ event.stopPropagation();
+ const newField = event.currentTarget.dataset.field;
+ const newOrder =
+ currentSort.field === newField
+ ? currentSort.order === 'ASC'
+ ? 'DESC'
+ : 'ASC'
+ : event.currentTarget.dataset.order;
+
+ setSort(newField, newOrder);
+ },
+ [currentSort.field, currentSort.order, setSort]
+ );
+
+ const updateSort = setSort ? updateSortCallback : null;
+
+ const handleSelectAll = useCallback(
+ event => {
+ if (event.target.checked) {
+ const all = ids.concat(
+ selectedIds.filter(id => !ids.includes(id))
+ );
+ onSelect(
+ isRowSelectable
+ ? all.filter(id => isRowSelectable(data[id]))
+ : all
+ );
+ } else {
+ onSelect([]);
+ }
+ },
+ [data, ids, onSelect, isRowSelectable, selectedIds]
+ );
+
+ const selectableIds = isRowSelectable
+ ? ids.filter(id => isRowSelectable(data[id]))
+ : ids;
+
+ return (
+
+
+ {hasExpand && (
+
+ )}
+ {hasBulkActions && selectedIds && (
+
+ 0 &&
+ selectableIds.length > 0 &&
+ selectableIds.every(id =>
+ selectedIds.includes(id)
+ )
+ }
+ onChange={handleSelectAll}
+ />
+
+ )}
+ {Children.map(children, (field, index) =>
+ isValidElement(field) ? (
+
+ ) : null
+ )}
+
+
+ );
+};
+
+DatagridHeader.propTypes = {
+ children: PropTypes.node,
+ classes: PropTypes.object,
+ className: PropTypes.string,
+ currentSort: PropTypes.exact({
+ field: PropTypes.string,
+ order: PropTypes.string,
+ }),
+ data: PropTypes.any,
+ hasExpand: PropTypes.bool,
+ hasBulkActions: PropTypes.bool,
+ ids: PropTypes.arrayOf(PropTypes.any),
+ isRowSelectable: PropTypes.func,
+ isRowExpandable: PropTypes.func,
+ onSelect: PropTypes.func,
+ onToggleItem: PropTypes.func,
+ resource: PropTypes.string,
+ selectedIds: PropTypes.arrayOf(PropTypes.any),
+ setSort: PropTypes.func,
+};
+
+export interface DatagridHeaderProps {
+ children?: React.ReactNode;
+ classes?: ClassesOverride;
+ className?: string;
+ hasExpand?: boolean;
+ hasBulkActions?: boolean;
+ isRowSelectable?: (record: Record) => boolean;
+ isRowExpandable?: (record: Record) => boolean;
+ size?: 'medium' | 'small';
+ // can be injected when using the component without context
+ currentSort?: SortPayload;
+ data?: RecordMap;
+ ids?: Identifier[];
+ onSelect?: (ids: Identifier[]) => void;
+ onToggleItem?: (id: Identifier) => void;
+ resource?: string;
+ selectedIds?: Identifier[];
+ setSort?: (sort: string, order?: string) => void;
+}
+
+DatagridHeader.displayName = 'DatagridHeader';
diff --git a/packages/ra-ui-materialui/src/list/datagrid/index.ts b/packages/ra-ui-materialui/src/list/datagrid/index.ts
index 3ca2435df39..7eb6e18ce3b 100644
--- a/packages/ra-ui-materialui/src/list/datagrid/index.ts
+++ b/packages/ra-ui-materialui/src/list/datagrid/index.ts
@@ -16,6 +16,8 @@ import DatagridRow, {
import ExpandRowButton, { ExpandRowButtonProps } from './ExpandRowButton';
import useDatagridStyles from './useDatagridStyles';
+export * from './DatagridHeader';
+
export {
Datagrid,
DatagridLoading,