-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
[DataGridPro] Add doc example for tree data children lazy loading #3657
Merged
flaviendelangle
merged 4 commits into
mui:master
from
flaviendelangle:tree-data-lazy-loading
Jan 18, 2022
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
d50b4d4
[DataGridPro] Add doc example for tree data children lazy loading
flaviendelangle b4fa53f
Regen doc ts
flaviendelangle 573608a
Update docs/src/pages/components/data-grid/group-pivot/group-pivot.md
flaviendelangle 8ee0cb0
Code review: Alexandre
flaviendelangle File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
363 changes: 363 additions & 0 deletions
363
docs/src/pages/components/data-grid/group-pivot/TreeDataLazyLoading.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,363 @@ | ||
import * as React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { | ||
DataGridPro, | ||
getDataGridUtilityClass, | ||
GridEvents, | ||
useGridApiContext, | ||
useGridApiRef, | ||
useGridRootProps, | ||
} from '@mui/x-data-grid-pro'; | ||
import { unstable_composeClasses as composeClasses } from '@mui/base'; | ||
import Box from '@mui/material/Box'; | ||
import IconButton from '@mui/material/IconButton'; | ||
|
||
export const isNavigationKey = (key) => | ||
key === 'Home' || | ||
key === 'End' || | ||
key.indexOf('Arrow') === 0 || | ||
key.indexOf('Page') === 0 || | ||
key === ' '; | ||
|
||
const ALL_ROWS = [ | ||
{ | ||
hierarchy: ['Sarah'], | ||
jobTitle: 'Head of Human Resources', | ||
recruitmentDate: new Date(2020, 8, 12), | ||
id: 0, | ||
}, | ||
{ | ||
hierarchy: ['Thomas'], | ||
jobTitle: 'Head of Sales', | ||
recruitmentDate: new Date(2017, 3, 4), | ||
id: 1, | ||
}, | ||
{ | ||
hierarchy: ['Thomas', 'Robert'], | ||
jobTitle: 'Sales Person', | ||
recruitmentDate: new Date(2020, 11, 20), | ||
id: 2, | ||
}, | ||
{ | ||
hierarchy: ['Thomas', 'Karen'], | ||
jobTitle: 'Sales Person', | ||
recruitmentDate: new Date(2020, 10, 14), | ||
id: 3, | ||
}, | ||
{ | ||
hierarchy: ['Thomas', 'Nancy'], | ||
jobTitle: 'Sales Person', | ||
recruitmentDate: new Date(2017, 10, 29), | ||
id: 4, | ||
}, | ||
{ | ||
hierarchy: ['Thomas', 'Daniel'], | ||
jobTitle: 'Sales Person', | ||
recruitmentDate: new Date(2020, 7, 21), | ||
id: 5, | ||
}, | ||
{ | ||
hierarchy: ['Thomas', 'Christopher'], | ||
jobTitle: 'Sales Person', | ||
recruitmentDate: new Date(2020, 7, 20), | ||
id: 6, | ||
}, | ||
{ | ||
hierarchy: ['Thomas', 'Donald'], | ||
jobTitle: 'Sales Person', | ||
recruitmentDate: new Date(2019, 6, 28), | ||
id: 7, | ||
}, | ||
{ | ||
hierarchy: ['Mary'], | ||
jobTitle: 'Head of Engineering', | ||
recruitmentDate: new Date(2016, 3, 14), | ||
id: 8, | ||
}, | ||
{ | ||
hierarchy: ['Mary', 'Jennifer'], | ||
jobTitle: 'Tech lead front', | ||
recruitmentDate: new Date(2016, 5, 17), | ||
id: 9, | ||
}, | ||
{ | ||
hierarchy: ['Mary', 'Jennifer', 'Anna'], | ||
jobTitle: 'Front-end developer', | ||
recruitmentDate: new Date(2019, 11, 7), | ||
id: 10, | ||
}, | ||
{ | ||
hierarchy: ['Mary', 'Michael'], | ||
jobTitle: 'Tech lead devops', | ||
recruitmentDate: new Date(2021, 7, 1), | ||
id: 11, | ||
}, | ||
{ | ||
hierarchy: ['Mary', 'Linda'], | ||
jobTitle: 'Tech lead back', | ||
recruitmentDate: new Date(2017, 0, 12), | ||
id: 12, | ||
}, | ||
{ | ||
hierarchy: ['Mary', 'Linda', 'Elizabeth'], | ||
jobTitle: 'Back-end developer', | ||
recruitmentDate: new Date(2019, 2, 22), | ||
id: 13, | ||
}, | ||
{ | ||
hierarchy: ['Mary', 'Linda', 'William'], | ||
jobTitle: 'Back-end developer', | ||
recruitmentDate: new Date(2018, 4, 19), | ||
id: 14, | ||
}, | ||
]; | ||
|
||
const columns = [ | ||
{ field: 'jobTitle', headerName: 'Job Title', width: 200 }, | ||
{ | ||
field: 'recruitmentDate', | ||
headerName: 'Recruitment Date', | ||
type: 'date', | ||
width: 150, | ||
}, | ||
]; | ||
|
||
const getChildren = (parentPath) => { | ||
const parentPathStr = parentPath.join('-'); | ||
return ALL_ROWS.filter( | ||
(row) => row.hierarchy.slice(0, -1).join('-') === parentPathStr, | ||
); | ||
}; | ||
|
||
/** | ||
* This is a naive implementation with terrible performances on a real dataset. | ||
* This fake server is only here for demonstration purposes. | ||
*/ | ||
const fakeDataFetcher = (parentPath = []) => | ||
new Promise((resolve) => { | ||
setTimeout(() => { | ||
const rows = getChildren(parentPath).map((row) => ({ | ||
...row, | ||
descendantCount: getChildren(row.hierarchy).length, | ||
})); | ||
|
||
resolve(rows); | ||
}, 500 + Math.random() * 300); | ||
}); | ||
|
||
const getTreeDataPath = (row) => row.hierarchy; | ||
|
||
const useUtilityClasses = (ownerState) => { | ||
const { classes } = ownerState; | ||
|
||
const slots = { | ||
root: ['treeDataGroupingCell'], | ||
toggle: ['treeDataGroupingCellToggle'], | ||
}; | ||
|
||
return composeClasses(slots, getDataGridUtilityClass, classes); | ||
}; | ||
|
||
/** | ||
* Reproduce the behavior of the `GridTreeDataGroupingCell` component in `@mui/x-data-grid-pro` | ||
* But base the amount of children on a `row.descendantCount` property rather than on the internal lookups. | ||
*/ | ||
const GroupingCellWithLazyLoading = (props) => { | ||
const { id, field, rowNode, row, hideDescendantCount } = props; | ||
|
||
const rootProps = useGridRootProps(); | ||
const apiRef = useGridApiContext(); | ||
const classes = useUtilityClasses({ classes: rootProps.classes }); | ||
|
||
const Icon = rowNode.childrenExpanded | ||
? rootProps.components.TreeDataCollapseIcon | ||
: rootProps.components.TreeDataExpandIcon; | ||
|
||
const handleKeyDown = (event) => { | ||
if (event.key === ' ') { | ||
event.stopPropagation(); | ||
} | ||
if (isNavigationKey(event.key) && !event.shiftKey) { | ||
apiRef.current.publishEvent(GridEvents.cellNavigationKeyDown, props, event); | ||
} | ||
}; | ||
|
||
const handleClick = (event) => { | ||
apiRef.current.setRowChildrenExpansion(id, !rowNode.childrenExpanded); | ||
apiRef.current.setCellFocus(id, field); | ||
event.stopPropagation(); | ||
}; | ||
|
||
return ( | ||
<Box className={classes.root} sx={{ ml: rowNode.depth * 2 }}> | ||
<div className={classes.toggle}> | ||
{row.descendantCount > 0 && ( | ||
<IconButton | ||
size="small" | ||
onClick={handleClick} | ||
onKeyDown={handleKeyDown} | ||
tabIndex={-1} | ||
aria-label={ | ||
rowNode.childrenExpanded | ||
? apiRef.current.getLocaleText('treeDataCollapse') | ||
: apiRef.current.getLocaleText('treeDataExpand') | ||
} | ||
> | ||
<Icon fontSize="inherit" /> | ||
</IconButton> | ||
)} | ||
</div> | ||
<span> | ||
{rowNode.groupingKey} | ||
{!hideDescendantCount && row.descendantCount > 0 | ||
? ` (${row.descendantCount})` | ||
: ''} | ||
</span> | ||
</Box> | ||
); | ||
}; | ||
|
||
GroupingCellWithLazyLoading.propTypes = { | ||
/** | ||
* The column field of the cell that triggered the event. | ||
*/ | ||
field: PropTypes.string.isRequired, | ||
hideDescendantCount: PropTypes.bool, | ||
/** | ||
* The grid row id. | ||
*/ | ||
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, | ||
/** | ||
* The row model of the row that the current cell belongs to. | ||
*/ | ||
row: PropTypes.any.isRequired, | ||
/** | ||
* The node of the row that the current cell belongs to. | ||
*/ | ||
rowNode: PropTypes.shape({ | ||
/** | ||
* The id of the row children. | ||
*/ | ||
children: PropTypes.arrayOf( | ||
PropTypes.oneOfType([PropTypes.number, PropTypes.string]), | ||
), | ||
/** | ||
* Current expansion status of the row. | ||
*/ | ||
childrenExpanded: PropTypes.bool, | ||
/** | ||
* 0-based depth of the row in the tree. | ||
*/ | ||
depth: PropTypes.number.isRequired, | ||
/** | ||
* The field used to group the children of this row. | ||
* Is `null` if no field has been used to group the children of this row. | ||
*/ | ||
groupingField: PropTypes.oneOfType([PropTypes.oneOf([null]), PropTypes.string]) | ||
.isRequired, | ||
/** | ||
* The key used to group the children of this row. | ||
*/ | ||
groupingKey: PropTypes.oneOfType([ | ||
PropTypes.number, | ||
PropTypes.string, | ||
PropTypes.bool, | ||
]).isRequired, | ||
/** | ||
* The grid row id. | ||
*/ | ||
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, | ||
/** | ||
* If `true`, this node has been automatically added to fill a gap in the tree structure. | ||
*/ | ||
isAutoGenerated: PropTypes.bool, | ||
/** | ||
* The row id of the parent (null if this row is a top level row). | ||
*/ | ||
parent: PropTypes.oneOfType([ | ||
PropTypes.oneOf([null]), | ||
PropTypes.number, | ||
PropTypes.string, | ||
]).isRequired, | ||
}).isRequired, | ||
}; | ||
|
||
const CUSTOM_GROUPING_COL_DEF = { | ||
renderCell: (params) => <GroupingCellWithLazyLoading {...params} />, | ||
}; | ||
|
||
export default function TreeDataLazyLoading() { | ||
const apiRef = useGridApiRef(); | ||
const [rows, setRows] = React.useState([]); | ||
|
||
React.useEffect(() => { | ||
fakeDataFetcher().then(setRows); | ||
|
||
const handleRowExpansionChange = async (node) => { | ||
const row = apiRef.current.getRow(node.id); | ||
|
||
if (!node.childrenExpanded || !row || row.childrenFetched) { | ||
return; | ||
} | ||
|
||
apiRef.current.updateRows([ | ||
{ | ||
id: `placeholder-children-${node.id}`, | ||
hierarchy: [...row.hierarchy, ''], | ||
}, | ||
]); | ||
|
||
const childrenRows = await fakeDataFetcher(row.hierarchy); | ||
apiRef.current.updateRows([ | ||
...childrenRows, | ||
{ id: node.id, childrenFetched: true }, | ||
{ id: `placeholder-children-${node.id}`, _action: 'delete' }, | ||
]); | ||
|
||
if (childrenRows.length) { | ||
apiRef.current.setRowChildrenExpansion(node.id, true); | ||
} | ||
}; | ||
|
||
/** | ||
* By default, the grid does not toggle the expansion of rows with 0 children | ||
* We need to override the `cellKeyDown` event listener to force the expansion if there are children on the server | ||
*/ | ||
const handleCellKeyDown = (params, event) => { | ||
const cellParams = apiRef.current.getCellParams(params.id, params.field); | ||
if (cellParams.colDef.type === 'treeDataGroup' && event.key === ' ') { | ||
event.stopPropagation(); | ||
event.preventDefault(); | ||
event.defaultMuiPrevented = true; | ||
|
||
apiRef.current.setRowChildrenExpansion( | ||
params.id, | ||
!params.rowNode.childrenExpanded, | ||
); | ||
} | ||
}; | ||
|
||
apiRef.current.subscribeEvent( | ||
GridEvents.rowExpansionChange, | ||
handleRowExpansionChange, | ||
); | ||
|
||
apiRef.current.subscribeEvent(GridEvents.cellKeyDown, handleCellKeyDown, { | ||
isFirst: true, | ||
}); | ||
}, [apiRef]); | ||
|
||
return ( | ||
<div style={{ height: 400, width: '100%' }}> | ||
<DataGridPro | ||
treeData | ||
apiRef={apiRef} | ||
rows={rows} | ||
columns={columns} | ||
getTreeDataPath={getTreeDataPath} | ||
groupingColDef={CUSTOM_GROUPING_COL_DEF} | ||
disableChildrenFiltering | ||
/> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By curiosity, why putting it in a
useEffect
instead of auseCallback
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This
useEffect
is only called once to initialize the event listeners and init the top level rows.