From bd6c15a22bc7374ffa06b5d2b48ecea1281cb465 Mon Sep 17 00:00:00 2001 From: Danail Hadjiatanasov Date: Thu, 1 Sep 2022 13:02:03 +0300 Subject: [PATCH] [DataGridPro] Implement Lazy loading (#5214) --- docs/data/data-grid/events/events.json | 15 ++ .../row-updates/InfiniteLoadingGrid.js | 2 +- .../row-updates/InfiniteLoadingGrid.tsx | 2 +- .../data-grid/row-updates/LazyLoadingGrid.js | 106 ++++++++ .../data-grid/row-updates/LazyLoadingGrid.tsx | 114 ++++++++ .../row-updates/LazyLoadingGrid.tsx.preview | 14 + .../data/data-grid/row-updates/row-updates.md | 34 +++ .../x/api/data-grid/data-grid-premium.json | 7 +- docs/pages/x/api/data-grid/data-grid-pro.json | 8 +- docs/pages/x/api/data-grid/data-grid.json | 2 + docs/pages/x/api/data-grid/grid-api.md | 1 + .../data-grid/data-grid-premium-pt.json | 6 + .../data-grid/data-grid-premium-zh.json | 6 + .../api-docs/data-grid/data-grid-premium.json | 6 + .../api-docs/data-grid/data-grid-pro-pt.json | 7 + .../api-docs/data-grid/data-grid-pro-zh.json | 7 + .../api-docs/data-grid/data-grid-pro.json | 7 + .../api-docs/data-grid/data-grid-pt.json | 5 + .../api-docs/data-grid/data-grid-zh.json | 5 + .../api-docs/data-grid/data-grid.json | 5 + .../src/hooks/useQuery.ts | 14 +- .../src/renderer/renderEditProgress.tsx | 3 +- .../src/DataGridPremium/DataGridPremium.tsx | 15 ++ .../src/DataGridPro/DataGridPro.tsx | 15 ++ .../DataGridPro/useDataGridProComponent.tsx | 4 + .../src/DataGridPro/useDataGridProProps.ts | 2 + .../infiniteLoader/useGridInfiniteLoader.ts | 16 +- .../features/lazyLoader/useGridLazyLoader.ts | 233 ++++++++++++++++ .../useGridLazyLoaderPreProcessors.tsx | 47 ++++ .../src/models/dataGridProProps.ts | 22 ++ .../src/models/gridFetchRowsParams.ts | 23 ++ .../grid/x-data-grid-pro/src/models/index.ts | 1 + .../src/tests/events.DataGridPro.test.tsx | 18 ++ .../src/tests/lazyLoader.DataGridPro.test.tsx | 114 ++++++++ .../src/typeOverloads/modules.ts | 10 +- .../x-data-grid/src/components/GridRow.tsx | 248 +++++++++++------- .../src/components/cell/GridSkeletonCell.tsx | 55 ++++ .../x-data-grid/src/components/cell/index.ts | 1 + .../components/containers/GridRootStyles.ts | 1 + .../constants/defaultGridSlotsComponents.ts | 2 + .../x-data-grid/src/constants/gridClasses.ts | 5 + .../src/hooks/features/rows/useGridRows.ts | 79 +++++- .../virtualization/useGridVirtualScroller.tsx | 16 +- .../x-data-grid/src/models/api/gridRowApi.ts | 6 + .../src/models/events/gridEventLookup.ts | 5 + .../src/models/events/gridEvents.ts | 2 + .../src/models/gridSlotsComponent.ts | 5 + .../gridRenderedRowsIntervalChangeParams.ts | 10 + .../x-data-grid/src/models/params/index.ts | 1 + packages/grid/x-data-grid/src/utils/utils.ts | 17 ++ scripts/x-data-grid-generator.exports.json | 2 +- scripts/x-data-grid-premium.exports.json | 4 + scripts/x-data-grid-pro.exports.json | 4 + scripts/x-data-grid.exports.json | 3 + 54 files changed, 1238 insertions(+), 124 deletions(-) create mode 100644 docs/data/data-grid/row-updates/LazyLoadingGrid.js create mode 100644 docs/data/data-grid/row-updates/LazyLoadingGrid.tsx create mode 100644 docs/data/data-grid/row-updates/LazyLoadingGrid.tsx.preview create mode 100644 packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoader.ts create mode 100644 packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoaderPreProcessors.tsx create mode 100644 packages/grid/x-data-grid-pro/src/models/gridFetchRowsParams.ts create mode 100644 packages/grid/x-data-grid-pro/src/tests/lazyLoader.DataGridPro.test.tsx create mode 100644 packages/grid/x-data-grid/src/components/cell/GridSkeletonCell.tsx create mode 100644 packages/grid/x-data-grid/src/models/params/gridRenderedRowsIntervalChangeParams.ts diff --git a/docs/data/data-grid/events/events.json b/docs/data/data-grid/events/events.json index 6fe8cf8b2617..446ad5189875 100644 --- a/docs/data/data-grid/events/events.json +++ b/docs/data/data-grid/events/events.json @@ -204,6 +204,14 @@ "params": "GridEditRowsModel", "event": "MuiEvent<{}>" }, + { + "projects": ["x-data-grid-pro", "x-data-grid-premium"], + "name": "fetchRows", + "description": "Fired when a new batch of rows is requested to be loaded. Called with a GridFetchRowsParams object.", + "params": "GridFetchRowsParams", + "event": "MuiEvent<{}>", + "componentProp": "onFetchRows" + }, { "projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"], "name": "filterModelChange", @@ -264,6 +272,13 @@ "event": "MuiEvent<{}>", "componentProp": "onPreferencePanelOpen" }, + { + "projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"], + "name": "renderedRowsIntervalChange", + "description": "Fired when the rendered rows index interval changes. Called with a GridRenderedRowsIntervalChangeParams object.", + "params": "GridRenderedRowsIntervalChangeParams", + "event": "MuiEvent<{}>" + }, { "projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"], "name": "resize", diff --git a/docs/data/data-grid/row-updates/InfiniteLoadingGrid.js b/docs/data/data-grid/row-updates/InfiniteLoadingGrid.js index 6ac83806e6e7..aa46747096dd 100644 --- a/docs/data/data-grid/row-updates/InfiniteLoadingGrid.js +++ b/docs/data/data-grid/row-updates/InfiniteLoadingGrid.js @@ -47,7 +47,7 @@ export default function InfiniteLoadingGrid() { React.useEffect(() => { return () => { - mounted.current = false; + mounted.current = true; }; }, []); diff --git a/docs/data/data-grid/row-updates/InfiniteLoadingGrid.tsx b/docs/data/data-grid/row-updates/InfiniteLoadingGrid.tsx index d16023676681..9997e8fd6085 100644 --- a/docs/data/data-grid/row-updates/InfiniteLoadingGrid.tsx +++ b/docs/data/data-grid/row-updates/InfiniteLoadingGrid.tsx @@ -47,7 +47,7 @@ export default function InfiniteLoadingGrid() { React.useEffect(() => { return () => { - mounted.current = false; + mounted.current = true; }; }, []); diff --git a/docs/data/data-grid/row-updates/LazyLoadingGrid.js b/docs/data/data-grid/row-updates/LazyLoadingGrid.js new file mode 100644 index 000000000000..c2ceca12266b --- /dev/null +++ b/docs/data/data-grid/row-updates/LazyLoadingGrid.js @@ -0,0 +1,106 @@ +import * as React from 'react'; +import { debounce } from '@mui/material/utils'; +import { DataGridPro, useGridApiRef } from '@mui/x-data-grid-pro'; +import { createFakeServer, loadServerRows } from '@mui/x-data-grid-generator'; + +const DATASET_OPTION = { + dataSet: 'Employee', + rowLength: 10000, +}; + +const { columns, columnsWithDefaultColDef, useQuery } = + createFakeServer(DATASET_OPTION); + +const emptyObject = {}; + +export default function LazyLoadingGrid() { + // dataServerSide simulates your database. + const { data: dataServerSide } = useQuery(emptyObject); + + const apiRef = useGridApiRef(); + const [initialRows, setInitialRows] = React.useState([]); + const [rowCount, setRowCount] = React.useState(0); + + const fetchRow = React.useCallback( + async (params) => { + const serverRows = await loadServerRows( + dataServerSide, + { + filterModel: params.filterModel, + sortModel: params.sortModel, + }, + { + minDelay: 300, + maxDelay: 800, + useCursorPagination: false, + }, + columnsWithDefaultColDef, + ); + + return { + slice: serverRows.returnedRows.slice( + params.firstRowToRender, + params.lastRowToRender, + ), + total: serverRows.returnedRows.length, + }; + }, + [dataServerSide], + ); + + // The initial fetch request of the viewport. + React.useEffect(() => { + if (dataServerSide.length === 0) { + return; + } + + (async () => { + const { slice, total } = await fetchRow({ + firstRowToRender: 0, + lastRowToRender: 10, + sortModel: [], + filterModel: { + items: [], + }, + }); + + setInitialRows(slice); + setRowCount(total); + })(); + }, [dataServerSide, fetchRow]); + + // Fetch rows as they become visible in the viewport + const handleFetchRows = React.useCallback( + async (params) => { + const { slice, total } = await fetchRow(params); + + apiRef.current.unstable_replaceRows(params.firstRowToRender, slice); + setRowCount(total); + }, + [apiRef, fetchRow], + ); + + const debouncedHandleFetchRows = React.useMemo( + () => debounce(handleFetchRows, 200), + [handleFetchRows], + ); + + return ( +
+ +
+ ); +} diff --git a/docs/data/data-grid/row-updates/LazyLoadingGrid.tsx b/docs/data/data-grid/row-updates/LazyLoadingGrid.tsx new file mode 100644 index 000000000000..9a038b571637 --- /dev/null +++ b/docs/data/data-grid/row-updates/LazyLoadingGrid.tsx @@ -0,0 +1,114 @@ +import * as React from 'react'; +import { debounce } from '@mui/material/utils'; +import { + DataGridPro, + GridFetchRowsParams, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { + createFakeServer, + loadServerRows, + UseDemoDataOptions, +} from '@mui/x-data-grid-generator'; + +const DATASET_OPTION: UseDemoDataOptions = { + dataSet: 'Employee', + rowLength: 10000, +}; + +const { columns, columnsWithDefaultColDef, useQuery } = + createFakeServer(DATASET_OPTION); + +const emptyObject = {}; + +export default function LazyLoadingGrid() { + // dataServerSide simulates your database. + const { data: dataServerSide } = useQuery(emptyObject); + + const apiRef = useGridApiRef(); + const [initialRows, setInitialRows] = React.useState([]); + const [rowCount, setRowCount] = React.useState(0); + + const fetchRow = React.useCallback( + async (params: GridFetchRowsParams) => { + const serverRows = await loadServerRows( + dataServerSide, + { + filterModel: params.filterModel, + sortModel: params.sortModel, + }, + { + minDelay: 300, + maxDelay: 800, + useCursorPagination: false, + }, + columnsWithDefaultColDef, + ); + + return { + slice: serverRows.returnedRows.slice( + params.firstRowToRender, + params.lastRowToRender, + ), + total: serverRows.returnedRows.length, + }; + }, + [dataServerSide], + ); + + // The initial fetch request of the viewport. + React.useEffect(() => { + if (dataServerSide.length === 0) { + return; + } + + (async () => { + const { slice, total } = await fetchRow({ + firstRowToRender: 0, + lastRowToRender: 10, + sortModel: [], + filterModel: { + items: [], + }, + }); + + setInitialRows(slice); + setRowCount(total); + })(); + }, [dataServerSide, fetchRow]); + + // Fetch rows as they become visible in the viewport + const handleFetchRows = React.useCallback( + async (params: GridFetchRowsParams) => { + const { slice, total } = await fetchRow(params); + + apiRef.current.unstable_replaceRows(params.firstRowToRender, slice); + setRowCount(total); + }, + [apiRef, fetchRow], + ); + + const debouncedHandleFetchRows = React.useMemo( + () => debounce(handleFetchRows, 200), + [handleFetchRows], + ); + + return ( +
+ +
+ ); +} diff --git a/docs/data/data-grid/row-updates/LazyLoadingGrid.tsx.preview b/docs/data/data-grid/row-updates/LazyLoadingGrid.tsx.preview new file mode 100644 index 000000000000..5077be50494f --- /dev/null +++ b/docs/data/data-grid/row-updates/LazyLoadingGrid.tsx.preview @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/docs/data/data-grid/row-updates/row-updates.md b/docs/data/data-grid/row-updates/row-updates.md index ac5bae53f1fb..0b25a419573a 100644 --- a/docs/data/data-grid/row-updates/row-updates.md +++ b/docs/data/data-grid/row-updates/row-updates.md @@ -39,6 +39,40 @@ In addition, the area in which `onRowsScrollEnd` is called can be changed using {{"demo": "InfiniteLoadingGrid.js", "bg": "inline", "disableAd": true}} +## Lazy loading [](/x/introduction/licensing/#pro-plan) + +:::warning +This feature is experimental, it needs to be explicitly activated using the `lazyLoading` experimental feature flag. + +```tsx + +``` + +::: + +Lazy Loading works like a pagination system, but instead of loading new rows based on pages, it loads them based on the viewport. +It loads new rows in chunks, as the user scrolls through the grid and reveals empty rows. + +The data grid builds the vertical scroll as if all the rows are already there, and displays empty (skeleton) rows while loading the data. Only rows that are displayed get fetched. + +To enable lazy loading, there are a few steps you need to follow: + +First, set `rowsLoadingMode="server"`. +Then, set `rowCount` to reflect the number of available rows on the server. +Third, set a callback function on `onFetchRows` to load the data corresponding to the row indices passed within `GridFetchRowsParams`. +Finally, replace the empty rows with the newly fetched ones using `apiRef.current.replaceRows()` like in the demo below. + +{{"demo": "LazyLoadingGrid.js", "bg": "inline", "disableAd": true}} + +:::warning +The `onFetchRows` callback is called every time a new row is in the viewport, so when you scroll, you can easily send multiple requests to your backend. We recommend developers limit those by implementing debouncing. +::: + +:::info +In order for filtering and sorting to work you need to set their modes to `server`. +You can find out more information about how to do that on the [server-side filter page](/x/react-data-grid/filtering/#server-side-filter) and on the [server-side sorting page](/x/react-data-grid/sorting/#server-side-sorting). +::: + ## High frequency [](/x/introduction/licensing/#pro-plan) Whenever the rows are updated, the grid has to apply the sorting and filters. This can be a problem if you have high frequency updates. To maintain good performances, the grid allows to batch the updates and only apply them after a period of time. The `throttleRowsMs` prop can be used to define the frequency (in milliseconds) at which rows updates are applied. diff --git a/docs/pages/x/api/data-grid/data-grid-premium.json b/docs/pages/x/api/data-grid/data-grid-premium.json index 7617b0c44042..6bb1be16e2d7 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -70,7 +70,7 @@ "experimentalFeatures": { "type": { "name": "shape", - "description": "{ aggregation?: bool, columnGrouping?: bool, newEditingApi?: bool, preventCommitWhileValidating?: bool, rowPinning?: bool, warnIfFocusStateIsNotSynced?: bool }" + "description": "{ aggregation?: bool, columnGrouping?: bool, lazyLoading?: bool, newEditingApi?: bool, preventCommitWhileValidating?: bool, rowPinning?: bool, warnIfFocusStateIsNotSynced?: bool }" } }, "filterMode": { @@ -159,6 +159,7 @@ }, "onEditRowsModelChange": { "type": { "name": "func" } }, "onError": { "type": { "name": "func" } }, + "onFetchRows": { "type": { "name": "func" } }, "onFilterModelChange": { "type": { "name": "func" } }, "onMenuClose": { "type": { "name": "func" } }, "onMenuOpen": { "type": { "name": "func" } }, @@ -205,6 +206,9 @@ "rowHeight": { "type": { "name": "number" }, "default": "52" }, "rowModesModel": { "type": { "name": "object" } }, "rowReordering": { "type": { "name": "bool" } }, + "rowsLoadingMode": { + "type": { "name": "enum", "description": "'client'
| 'server'" } + }, "rowSpacingType": { "type": { "name": "enum", "description": "'border'
| 'margin'" }, "default": "\"margin\"" @@ -342,6 +346,7 @@ "cell", "cellContent", "cellCheckbox", + "cellSkeleton", "checkboxInput", "columnHeader--alignCenter", "columnHeader--alignLeft", diff --git a/docs/pages/x/api/data-grid/data-grid-pro.json b/docs/pages/x/api/data-grid/data-grid-pro.json index 5ceaf1cfa5f7..843ab9a530c1 100644 --- a/docs/pages/x/api/data-grid/data-grid-pro.json +++ b/docs/pages/x/api/data-grid/data-grid-pro.json @@ -59,7 +59,7 @@ "experimentalFeatures": { "type": { "name": "shape", - "description": "{ columnGrouping?: bool, newEditingApi?: bool, preventCommitWhileValidating?: bool, rowPinning?: bool, warnIfFocusStateIsNotSynced?: bool }" + "description": "{ columnGrouping?: bool, lazyLoading?: bool, newEditingApi?: bool, preventCommitWhileValidating?: bool, rowPinning?: bool, warnIfFocusStateIsNotSynced?: bool }" } }, "filterMode": { @@ -143,6 +143,7 @@ }, "onEditRowsModelChange": { "type": { "name": "func" } }, "onError": { "type": { "name": "func" } }, + "onFetchRows": { "type": { "name": "func" } }, "onFilterModelChange": { "type": { "name": "func" } }, "onMenuClose": { "type": { "name": "func" } }, "onMenuOpen": { "type": { "name": "func" } }, @@ -183,6 +184,9 @@ "rowHeight": { "type": { "name": "number" }, "default": "52" }, "rowModesModel": { "type": { "name": "object" } }, "rowReordering": { "type": { "name": "bool" } }, + "rowsLoadingMode": { + "type": { "name": "enum", "description": "'client'
| 'server'" } + }, "rowSpacingType": { "type": { "name": "enum", "description": "'border'
| 'margin'" }, "default": "\"margin\"" @@ -296,6 +300,7 @@ "QuickFilterIcon": { "default": "GridSearchIcon", "type": { "name": "elementType" } }, "Row": { "default": "GridRow", "type": { "name": "elementType" } }, "RowReorderIcon": { "default": "GridDragIcon", "type": { "name": "elementType" } }, + "SkeletonCell": { "default": "GridSkeletonCell", "type": { "name": "elementType" } }, "Toolbar": { "default": "null", "type": { "name": "elementType | null" } }, "TreeDataCollapseIcon": { "default": "GridExpandMoreIcon", "type": { "name": "elementType" } }, "TreeDataExpandIcon": { "default": "GridKeyboardArrowRight", "type": { "name": "elementType" } } @@ -320,6 +325,7 @@ "cell", "cellContent", "cellCheckbox", + "cellSkeleton", "checkboxInput", "columnHeader--alignCenter", "columnHeader--alignLeft", diff --git a/docs/pages/x/api/data-grid/data-grid.json b/docs/pages/x/api/data-grid/data-grid.json index 7a7df8334ba9..c92205b78d67 100644 --- a/docs/pages/x/api/data-grid/data-grid.json +++ b/docs/pages/x/api/data-grid/data-grid.json @@ -250,6 +250,7 @@ "QuickFilterIcon": { "default": "GridSearchIcon", "type": { "name": "elementType" } }, "Row": { "default": "GridRow", "type": { "name": "elementType" } }, "RowReorderIcon": { "default": "GridDragIcon", "type": { "name": "elementType" } }, + "SkeletonCell": { "default": "GridSkeletonCell", "type": { "name": "elementType" } }, "Toolbar": { "default": "null", "type": { "name": "elementType | null" } }, "TreeDataCollapseIcon": { "default": "GridExpandMoreIcon", "type": { "name": "elementType" } }, "TreeDataExpandIcon": { "default": "GridKeyboardArrowRight", "type": { "name": "elementType" } } @@ -274,6 +275,7 @@ "cell", "cellContent", "cellCheckbox", + "cellSkeleton", "checkboxInput", "columnHeader--alignCenter", "columnHeader--alignLeft", diff --git a/docs/pages/x/api/data-grid/grid-api.md b/docs/pages/x/api/data-grid/grid-api.md index ad11c37f9f55..046966d80cee 100644 --- a/docs/pages/x/api/data-grid/grid-api.md +++ b/docs/pages/x/api/data-grid/grid-api.md @@ -120,6 +120,7 @@ import { GridApi } from '@mui/x-data-grid-pro'; | unpinColumn [](/x/introduction/licensing/#pro-plan) | (field: string) => void | Unpins a column. | | unstable_getAllGroupDetails | () => GridColumnGroupLookup | Returns the column group lookup. | | unstable_getColumnGroupPath | (field: string) => GridColumnGroup['groupId'][] | Returns the id of the groups leading to the requested column.
The array is ordered by increasing depth (the last element is the direct parent of the column). | +| unstable_replaceRows | (firstRowToReplace: number, newRows: GridRowModel[]) => void | Replace a set of rows with new rows. | | unstable_setPinnedRows [](/x/introduction/licensing/#pro-plan) | (pinnedRows?: GridPinnedRowsProp) => void | Changes the pinned rows. | | updateColumn | (col: GridColDef) => void | Updates the definition of a column. | | updateColumns | (cols: GridColDef[]) => void | Updates the definition of multiple columns at the same time. | diff --git a/docs/translations/api-docs/data-grid/data-grid-premium-pt.json b/docs/translations/api-docs/data-grid/data-grid-premium-pt.json index 8a3158aeffaf..735edc1be3fc 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium-pt.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium-pt.json @@ -97,6 +97,7 @@ "onEditCellPropsChange": "Callback fired when the edit cell value changes.

Signature:
function(params: GridEditCellPropsParams, event: MuiEvent<React.SyntheticEvent>, details: GridCallbackDetails) => void
params: With all properties from GridEditCellPropsParams.
event: The event that caused this prop to be called.
details: Additional details for this callback.", "onEditRowsModelChange": "Callback fired when the editRowsModel changes.

Signature:
function(editRowsModel: GridEditRowsModel, details: GridCallbackDetails) => void
editRowsModel: With all properties from GridEditRowsModel.
details: Additional details for this callback.", "onError": "Callback fired when an exception is thrown in the grid.

Signature:
function(args: any, event: MuiEvent<{}>, details: GridCallbackDetails) => void
args: The arguments passed to the showError call.
event: The event object.
details: Additional details for this callback.", + "onFetchRows": "Callback fired when rowCount is set and the next batch of virtualized rows is rendered.

Signature:
function(params: GridFetchRowsParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridFetchRowsParams.
event: The event object.
details: Additional details for this callback.", "onFilterModelChange": "Callback fired when the Filter model changes before the filters are applied.

Signature:
function(model: GridFilterModel, details: GridCallbackDetails) => void
model: With all properties from GridFilterModel.
details: Additional details for this callback.", "onMenuClose": "Callback fired when the menu is closed.

Signature:
function(params: GridMenuParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridMenuParams.
event: The event object.
details: Additional details for this callback.", "onMenuOpen": "Callback fired when the menu is opened.

Signature:
function(params: GridMenuParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridMenuParams.
event: The event object.
details: Additional details for this callback.", @@ -133,6 +134,7 @@ "rowModesModel": "Controls the modes of the rows.", "rowReordering": "If true, the reordering of rows is enabled.", "rows": "Set of rows of type GridRowsProp.", + "rowsLoadingMode": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading. * @default "client"", "rowSpacingType": "Sets the type of space between rows added by getRowSpacing.", "rowsPerPageOptions": "Select the pageSize dynamically using the component UI.", "rowThreshold": "Number of rows from the rowBuffer that can be visible before a new slice is rendered.", @@ -226,6 +228,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the cell checkbox element" }, + "cellSkeleton": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the skeleton cell element" + }, "checkboxInput": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the selection checkbox element" diff --git a/docs/translations/api-docs/data-grid/data-grid-premium-zh.json b/docs/translations/api-docs/data-grid/data-grid-premium-zh.json index 8a3158aeffaf..735edc1be3fc 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium-zh.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium-zh.json @@ -97,6 +97,7 @@ "onEditCellPropsChange": "Callback fired when the edit cell value changes.

Signature:
function(params: GridEditCellPropsParams, event: MuiEvent<React.SyntheticEvent>, details: GridCallbackDetails) => void
params: With all properties from GridEditCellPropsParams.
event: The event that caused this prop to be called.
details: Additional details for this callback.", "onEditRowsModelChange": "Callback fired when the editRowsModel changes.

Signature:
function(editRowsModel: GridEditRowsModel, details: GridCallbackDetails) => void
editRowsModel: With all properties from GridEditRowsModel.
details: Additional details for this callback.", "onError": "Callback fired when an exception is thrown in the grid.

Signature:
function(args: any, event: MuiEvent<{}>, details: GridCallbackDetails) => void
args: The arguments passed to the showError call.
event: The event object.
details: Additional details for this callback.", + "onFetchRows": "Callback fired when rowCount is set and the next batch of virtualized rows is rendered.

Signature:
function(params: GridFetchRowsParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridFetchRowsParams.
event: The event object.
details: Additional details for this callback.", "onFilterModelChange": "Callback fired when the Filter model changes before the filters are applied.

Signature:
function(model: GridFilterModel, details: GridCallbackDetails) => void
model: With all properties from GridFilterModel.
details: Additional details for this callback.", "onMenuClose": "Callback fired when the menu is closed.

Signature:
function(params: GridMenuParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridMenuParams.
event: The event object.
details: Additional details for this callback.", "onMenuOpen": "Callback fired when the menu is opened.

Signature:
function(params: GridMenuParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridMenuParams.
event: The event object.
details: Additional details for this callback.", @@ -133,6 +134,7 @@ "rowModesModel": "Controls the modes of the rows.", "rowReordering": "If true, the reordering of rows is enabled.", "rows": "Set of rows of type GridRowsProp.", + "rowsLoadingMode": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading. * @default "client"", "rowSpacingType": "Sets the type of space between rows added by getRowSpacing.", "rowsPerPageOptions": "Select the pageSize dynamically using the component UI.", "rowThreshold": "Number of rows from the rowBuffer that can be visible before a new slice is rendered.", @@ -226,6 +228,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the cell checkbox element" }, + "cellSkeleton": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the skeleton cell element" + }, "checkboxInput": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the selection checkbox element" diff --git a/docs/translations/api-docs/data-grid/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium.json index 8a3158aeffaf..735edc1be3fc 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium.json @@ -97,6 +97,7 @@ "onEditCellPropsChange": "Callback fired when the edit cell value changes.

Signature:
function(params: GridEditCellPropsParams, event: MuiEvent<React.SyntheticEvent>, details: GridCallbackDetails) => void
params: With all properties from GridEditCellPropsParams.
event: The event that caused this prop to be called.
details: Additional details for this callback.", "onEditRowsModelChange": "Callback fired when the editRowsModel changes.

Signature:
function(editRowsModel: GridEditRowsModel, details: GridCallbackDetails) => void
editRowsModel: With all properties from GridEditRowsModel.
details: Additional details for this callback.", "onError": "Callback fired when an exception is thrown in the grid.

Signature:
function(args: any, event: MuiEvent<{}>, details: GridCallbackDetails) => void
args: The arguments passed to the showError call.
event: The event object.
details: Additional details for this callback.", + "onFetchRows": "Callback fired when rowCount is set and the next batch of virtualized rows is rendered.

Signature:
function(params: GridFetchRowsParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridFetchRowsParams.
event: The event object.
details: Additional details for this callback.", "onFilterModelChange": "Callback fired when the Filter model changes before the filters are applied.

Signature:
function(model: GridFilterModel, details: GridCallbackDetails) => void
model: With all properties from GridFilterModel.
details: Additional details for this callback.", "onMenuClose": "Callback fired when the menu is closed.

Signature:
function(params: GridMenuParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridMenuParams.
event: The event object.
details: Additional details for this callback.", "onMenuOpen": "Callback fired when the menu is opened.

Signature:
function(params: GridMenuParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridMenuParams.
event: The event object.
details: Additional details for this callback.", @@ -133,6 +134,7 @@ "rowModesModel": "Controls the modes of the rows.", "rowReordering": "If true, the reordering of rows is enabled.", "rows": "Set of rows of type GridRowsProp.", + "rowsLoadingMode": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading. * @default "client"", "rowSpacingType": "Sets the type of space between rows added by getRowSpacing.", "rowsPerPageOptions": "Select the pageSize dynamically using the component UI.", "rowThreshold": "Number of rows from the rowBuffer that can be visible before a new slice is rendered.", @@ -226,6 +228,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the cell checkbox element" }, + "cellSkeleton": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the skeleton cell element" + }, "checkboxInput": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the selection checkbox element" diff --git a/docs/translations/api-docs/data-grid/data-grid-pro-pt.json b/docs/translations/api-docs/data-grid/data-grid-pro-pt.json index 614393b37a4a..a386378ef133 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro-pt.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro-pt.json @@ -90,6 +90,7 @@ "onEditCellPropsChange": "Callback fired when the edit cell value changes.

Signature:
function(params: GridEditCellPropsParams, event: MuiEvent<React.SyntheticEvent>, details: GridCallbackDetails) => void
params: With all properties from GridEditCellPropsParams.
event: The event that caused this prop to be called.
details: Additional details for this callback.", "onEditRowsModelChange": "Callback fired when the editRowsModel changes.

Signature:
function(editRowsModel: GridEditRowsModel, details: GridCallbackDetails) => void
editRowsModel: With all properties from GridEditRowsModel.
details: Additional details for this callback.", "onError": "Callback fired when an exception is thrown in the grid.

Signature:
function(args: any, event: MuiEvent<{}>, details: GridCallbackDetails) => void
args: The arguments passed to the showError call.
event: The event object.
details: Additional details for this callback.", + "onFetchRows": "Callback fired when rowCount is set and the next batch of virtualized rows is rendered.

Signature:
function(params: GridFetchRowsParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridFetchRowsParams.
event: The event object.
details: Additional details for this callback.", "onFilterModelChange": "Callback fired when the Filter model changes before the filters are applied.

Signature:
function(model: GridFilterModel, details: GridCallbackDetails) => void
model: With all properties from GridFilterModel.
details: Additional details for this callback.", "onMenuClose": "Callback fired when the menu is closed.

Signature:
function(params: GridMenuParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridMenuParams.
event: The event object.
details: Additional details for this callback.", "onMenuOpen": "Callback fired when the menu is opened.

Signature:
function(params: GridMenuParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridMenuParams.
event: The event object.
details: Additional details for this callback.", @@ -123,6 +124,7 @@ "rowModesModel": "Controls the modes of the rows.", "rowReordering": "If true, the reordering of rows is enabled.", "rows": "Set of rows of type GridRowsProp.", + "rowsLoadingMode": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading. * @default "client"", "rowSpacingType": "Sets the type of space between rows added by getRowSpacing.", "rowsPerPageOptions": "Select the pageSize dynamically using the component UI.", "rowThreshold": "Number of rows from the rowBuffer that can be visible before a new slice is rendered.", @@ -216,6 +218,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the cell checkbox element" }, + "cellSkeleton": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the skeleton cell element" + }, "checkboxInput": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the selection checkbox element" @@ -646,6 +652,7 @@ "BaseTooltip": "The custom Tooltip component used in the grid.", "BasePopper": "The custom Popper component used in the grid.", "Cell": "Component rendered for each cell.", + "SkeletonCell": "Component rendered for each skeleton cell.", "ColumnHeaderFilterIconButton": "Filter icon component rendered in each column header.", "ColumnMenu": "Column menu component rendered by clicking on the 3 dots "kebab" icon in column headers.", "ErrorOverlay": "Error overlay component rendered above the grid when an error is caught.", diff --git a/docs/translations/api-docs/data-grid/data-grid-pro-zh.json b/docs/translations/api-docs/data-grid/data-grid-pro-zh.json index 614393b37a4a..a386378ef133 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro-zh.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro-zh.json @@ -90,6 +90,7 @@ "onEditCellPropsChange": "Callback fired when the edit cell value changes.

Signature:
function(params: GridEditCellPropsParams, event: MuiEvent<React.SyntheticEvent>, details: GridCallbackDetails) => void
params: With all properties from GridEditCellPropsParams.
event: The event that caused this prop to be called.
details: Additional details for this callback.", "onEditRowsModelChange": "Callback fired when the editRowsModel changes.

Signature:
function(editRowsModel: GridEditRowsModel, details: GridCallbackDetails) => void
editRowsModel: With all properties from GridEditRowsModel.
details: Additional details for this callback.", "onError": "Callback fired when an exception is thrown in the grid.

Signature:
function(args: any, event: MuiEvent<{}>, details: GridCallbackDetails) => void
args: The arguments passed to the showError call.
event: The event object.
details: Additional details for this callback.", + "onFetchRows": "Callback fired when rowCount is set and the next batch of virtualized rows is rendered.

Signature:
function(params: GridFetchRowsParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridFetchRowsParams.
event: The event object.
details: Additional details for this callback.", "onFilterModelChange": "Callback fired when the Filter model changes before the filters are applied.

Signature:
function(model: GridFilterModel, details: GridCallbackDetails) => void
model: With all properties from GridFilterModel.
details: Additional details for this callback.", "onMenuClose": "Callback fired when the menu is closed.

Signature:
function(params: GridMenuParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridMenuParams.
event: The event object.
details: Additional details for this callback.", "onMenuOpen": "Callback fired when the menu is opened.

Signature:
function(params: GridMenuParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridMenuParams.
event: The event object.
details: Additional details for this callback.", @@ -123,6 +124,7 @@ "rowModesModel": "Controls the modes of the rows.", "rowReordering": "If true, the reordering of rows is enabled.", "rows": "Set of rows of type GridRowsProp.", + "rowsLoadingMode": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading. * @default "client"", "rowSpacingType": "Sets the type of space between rows added by getRowSpacing.", "rowsPerPageOptions": "Select the pageSize dynamically using the component UI.", "rowThreshold": "Number of rows from the rowBuffer that can be visible before a new slice is rendered.", @@ -216,6 +218,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the cell checkbox element" }, + "cellSkeleton": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the skeleton cell element" + }, "checkboxInput": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the selection checkbox element" @@ -646,6 +652,7 @@ "BaseTooltip": "The custom Tooltip component used in the grid.", "BasePopper": "The custom Popper component used in the grid.", "Cell": "Component rendered for each cell.", + "SkeletonCell": "Component rendered for each skeleton cell.", "ColumnHeaderFilterIconButton": "Filter icon component rendered in each column header.", "ColumnMenu": "Column menu component rendered by clicking on the 3 dots "kebab" icon in column headers.", "ErrorOverlay": "Error overlay component rendered above the grid when an error is caught.", diff --git a/docs/translations/api-docs/data-grid/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro.json index 614393b37a4a..a386378ef133 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro.json @@ -90,6 +90,7 @@ "onEditCellPropsChange": "Callback fired when the edit cell value changes.

Signature:
function(params: GridEditCellPropsParams, event: MuiEvent<React.SyntheticEvent>, details: GridCallbackDetails) => void
params: With all properties from GridEditCellPropsParams.
event: The event that caused this prop to be called.
details: Additional details for this callback.", "onEditRowsModelChange": "Callback fired when the editRowsModel changes.

Signature:
function(editRowsModel: GridEditRowsModel, details: GridCallbackDetails) => void
editRowsModel: With all properties from GridEditRowsModel.
details: Additional details for this callback.", "onError": "Callback fired when an exception is thrown in the grid.

Signature:
function(args: any, event: MuiEvent<{}>, details: GridCallbackDetails) => void
args: The arguments passed to the showError call.
event: The event object.
details: Additional details for this callback.", + "onFetchRows": "Callback fired when rowCount is set and the next batch of virtualized rows is rendered.

Signature:
function(params: GridFetchRowsParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridFetchRowsParams.
event: The event object.
details: Additional details for this callback.", "onFilterModelChange": "Callback fired when the Filter model changes before the filters are applied.

Signature:
function(model: GridFilterModel, details: GridCallbackDetails) => void
model: With all properties from GridFilterModel.
details: Additional details for this callback.", "onMenuClose": "Callback fired when the menu is closed.

Signature:
function(params: GridMenuParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridMenuParams.
event: The event object.
details: Additional details for this callback.", "onMenuOpen": "Callback fired when the menu is opened.

Signature:
function(params: GridMenuParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridMenuParams.
event: The event object.
details: Additional details for this callback.", @@ -123,6 +124,7 @@ "rowModesModel": "Controls the modes of the rows.", "rowReordering": "If true, the reordering of rows is enabled.", "rows": "Set of rows of type GridRowsProp.", + "rowsLoadingMode": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading. * @default "client"", "rowSpacingType": "Sets the type of space between rows added by getRowSpacing.", "rowsPerPageOptions": "Select the pageSize dynamically using the component UI.", "rowThreshold": "Number of rows from the rowBuffer that can be visible before a new slice is rendered.", @@ -216,6 +218,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the cell checkbox element" }, + "cellSkeleton": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the skeleton cell element" + }, "checkboxInput": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the selection checkbox element" @@ -646,6 +652,7 @@ "BaseTooltip": "The custom Tooltip component used in the grid.", "BasePopper": "The custom Popper component used in the grid.", "Cell": "Component rendered for each cell.", + "SkeletonCell": "Component rendered for each skeleton cell.", "ColumnHeaderFilterIconButton": "Filter icon component rendered in each column header.", "ColumnMenu": "Column menu component rendered by clicking on the 3 dots "kebab" icon in column headers.", "ErrorOverlay": "Error overlay component rendered above the grid when an error is caught.", diff --git a/docs/translations/api-docs/data-grid/data-grid-pt.json b/docs/translations/api-docs/data-grid/data-grid-pt.json index 9392cdc4b538..ad7d3b2d3192 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pt.json +++ b/docs/translations/api-docs/data-grid/data-grid-pt.json @@ -186,6 +186,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the cell checkbox element" }, + "cellSkeleton": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the skeleton cell element" + }, "checkboxInput": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the selection checkbox element" @@ -616,6 +620,7 @@ "BaseTooltip": "The custom Tooltip component used in the grid.", "BasePopper": "The custom Popper component used in the grid.", "Cell": "Component rendered for each cell.", + "SkeletonCell": "Component rendered for each skeleton cell.", "ColumnHeaderFilterIconButton": "Filter icon component rendered in each column header.", "ColumnMenu": "Column menu component rendered by clicking on the 3 dots "kebab" icon in column headers.", "ErrorOverlay": "Error overlay component rendered above the grid when an error is caught.", diff --git a/docs/translations/api-docs/data-grid/data-grid-zh.json b/docs/translations/api-docs/data-grid/data-grid-zh.json index 9392cdc4b538..ad7d3b2d3192 100644 --- a/docs/translations/api-docs/data-grid/data-grid-zh.json +++ b/docs/translations/api-docs/data-grid/data-grid-zh.json @@ -186,6 +186,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the cell checkbox element" }, + "cellSkeleton": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the skeleton cell element" + }, "checkboxInput": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the selection checkbox element" @@ -616,6 +620,7 @@ "BaseTooltip": "The custom Tooltip component used in the grid.", "BasePopper": "The custom Popper component used in the grid.", "Cell": "Component rendered for each cell.", + "SkeletonCell": "Component rendered for each skeleton cell.", "ColumnHeaderFilterIconButton": "Filter icon component rendered in each column header.", "ColumnMenu": "Column menu component rendered by clicking on the 3 dots "kebab" icon in column headers.", "ErrorOverlay": "Error overlay component rendered above the grid when an error is caught.", diff --git a/docs/translations/api-docs/data-grid/data-grid.json b/docs/translations/api-docs/data-grid/data-grid.json index 9392cdc4b538..ad7d3b2d3192 100644 --- a/docs/translations/api-docs/data-grid/data-grid.json +++ b/docs/translations/api-docs/data-grid/data-grid.json @@ -186,6 +186,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the cell checkbox element" }, + "cellSkeleton": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the skeleton cell element" + }, "checkboxInput": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the selection checkbox element" @@ -616,6 +620,7 @@ "BaseTooltip": "The custom Tooltip component used in the grid.", "BasePopper": "The custom Popper component used in the grid.", "Cell": "Component rendered for each cell.", + "SkeletonCell": "Component rendered for each skeleton cell.", "ColumnHeaderFilterIconButton": "Filter icon component rendered in each column header.", "ColumnMenu": "Column menu component rendered by clicking on the 3 dots "kebab" icon in column headers.", "ErrorOverlay": "Error overlay component rendered above the grid when an error is caught.", diff --git a/packages/grid/x-data-grid-generator/src/hooks/useQuery.ts b/packages/grid/x-data-grid-generator/src/hooks/useQuery.ts index 7f5e6f160790..87478a253b64 100644 --- a/packages/grid/x-data-grid-generator/src/hooks/useQuery.ts +++ b/packages/grid/x-data-grid-generator/src/hooks/useQuery.ts @@ -107,7 +107,7 @@ const getFilteredRows = ( /** * Simulates server data loading */ -const loadServerRows = ( +export const loadServerRows = ( rows: GridRowModel[], queryOptions: QueryOptions, serverOptions: ServerOptions, @@ -170,12 +170,14 @@ interface PageInfo { pageSize?: number; } -export interface ServerOptions { +interface DefaultServerOptions { minDelay: number; maxDelay: number; useCursorPagination?: boolean; } +type ServerOptions = Partial; + export interface QueryOptions { cursor?: GridRowId; page?: number; @@ -183,6 +185,8 @@ export interface QueryOptions { // TODO: implement the behavior liked to following models filterModel?: GridFilterModel; sortModel?: GridSortModel; + firstRowToRender?: number; + lastRowToRender?: number; } const DEFAULT_DATASET_OPTIONS: UseDemoDataOptions = { @@ -194,7 +198,7 @@ const DEFAULT_DATASET_OPTIONS: UseDemoDataOptions = { declare const DISABLE_CHANCE_RANDOM: any; const disableDelay = typeof DISABLE_CHANCE_RANDOM !== 'undefined' && DISABLE_CHANCE_RANDOM; -const DEFAULT_SERVER_OPTIONS: ServerOptions = { +const DEFAULT_SERVER_OPTIONS: DefaultServerOptions = { minDelay: disableDelay ? 0 : 100, maxDelay: disableDelay ? 0 : 300, useCursorPagination: true, @@ -202,7 +206,7 @@ const DEFAULT_SERVER_OPTIONS: ServerOptions = { export const createFakeServer = ( dataSetOptions?: Partial, - serverOptions?: Partial, + serverOptions?: ServerOptions, ) => { const dataSetOptionsWithDefault = { ...DEFAULT_DATASET_OPTIONS, ...dataSetOptions }; const serverOptionsWithDefault = { ...DEFAULT_SERVER_OPTIONS, ...serverOptions }; @@ -279,5 +283,5 @@ export const createFakeServer = ( }; }; - return { columns, initialState, useQuery }; + return { columns, columnsWithDefaultColDef, initialState, useQuery }; }; diff --git a/packages/grid/x-data-grid-generator/src/renderer/renderEditProgress.tsx b/packages/grid/x-data-grid-generator/src/renderer/renderEditProgress.tsx index effda70f8117..8c4601ef6642 100644 --- a/packages/grid/x-data-grid-generator/src/renderer/renderEditProgress.tsx +++ b/packages/grid/x-data-grid-generator/src/renderer/renderEditProgress.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import clsx from 'clsx'; import { GridRenderEditCellParams, useGridApiContext } from '@mui/x-data-grid-premium'; import Slider, { SliderProps, sliderClasses } from '@mui/material/Slider'; -import { SliderValueLabelProps } from '@mui/base/SliderUnstyled'; import Tooltip from '@mui/material/Tooltip'; import { debounce } from '@mui/material/utils'; import { alpha, styled } from '@mui/material/styles'; @@ -43,7 +42,7 @@ const StyledSlider = styled(Slider)(({ theme }) => ({ }, })); -const ValueLabelComponent = (props: SliderValueLabelProps) => { +const ValueLabelComponent = (props: any) => { const { children, open, value } = props; return ( diff --git a/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index bbdfc8f442b2..2e6b955f04d2 100644 --- a/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -288,6 +288,7 @@ DataGridPremiumRaw.propTypes = { experimentalFeatures: PropTypes.shape({ aggregation: PropTypes.bool, columnGrouping: PropTypes.bool, + lazyLoading: PropTypes.bool, newEditingApi: PropTypes.bool, preventCommitWhileValidating: PropTypes.bool, rowPinning: PropTypes.bool, @@ -648,6 +649,13 @@ DataGridPremiumRaw.propTypes = { * @param {GridCallbackDetails} details Additional details for this callback. */ onError: PropTypes.func, + /** + * Callback fired when rowCount is set and the next batch of virtualized rows is rendered. + * @param {GridFetchRowsParams} params With all properties from [[GridFetchRowsParams]]. + * @param {MuiEvent<{}>} event The event object. + * @param {GridCallbackDetails} details Additional details for this callback. + */ + onFetchRows: PropTypes.func, /** * Callback fired when the Filter model changes before the filters are applied. * @param {GridFilterModel} model With all properties from [[GridFilterModel]]. @@ -875,6 +883,13 @@ DataGridPremiumRaw.propTypes = { * Set of rows of type [[GridRowsProp]]. */ rows: PropTypes.array.isRequired, + /** + * Loading rows can be processed on the server or client-side. + * Set it to 'client' if you would like enable infnite loading. + * Set it to 'server' if you would like to enable lazy loading. + * * @default "client" + */ + rowsLoadingMode: PropTypes.oneOf(['client', 'server']), /** * Sets the type of space between rows added by `getRowSpacing`. * @default "margin" diff --git a/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index b36812c31aa7..5af063bfef75 100644 --- a/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -258,6 +258,7 @@ DataGridProRaw.propTypes = { */ experimentalFeatures: PropTypes.shape({ columnGrouping: PropTypes.bool, + lazyLoading: PropTypes.bool, newEditingApi: PropTypes.bool, preventCommitWhileValidating: PropTypes.bool, rowPinning: PropTypes.bool, @@ -605,6 +606,13 @@ DataGridProRaw.propTypes = { * @param {GridCallbackDetails} details Additional details for this callback. */ onError: PropTypes.func, + /** + * Callback fired when rowCount is set and the next batch of virtualized rows is rendered. + * @param {GridFetchRowsParams} params With all properties from [[GridFetchRowsParams]]. + * @param {MuiEvent<{}>} event The event object. + * @param {GridCallbackDetails} details Additional details for this callback. + */ + onFetchRows: PropTypes.func, /** * Callback fired when the Filter model changes before the filters are applied. * @param {GridFilterModel} model With all properties from [[GridFilterModel]]. @@ -816,6 +824,13 @@ DataGridProRaw.propTypes = { * Set of rows of type [[GridRowsProp]]. */ rows: PropTypes.array.isRequired, + /** + * Loading rows can be processed on the server or client-side. + * Set it to 'client' if you would like enable infnite loading. + * Set it to 'server' if you would like to enable lazy loading. + * * @default "client" + */ + rowsLoadingMode: PropTypes.oneOf(['client', 'server']), /** * Sets the type of space between rows added by `getRowSpacing`. * @default "margin" diff --git a/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx b/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx index c633ae78f507..53713e8efcf6 100644 --- a/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx +++ b/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx @@ -70,6 +70,8 @@ import { import { useGridDetailPanelPreProcessors } from '../hooks/features/detailPanel/useGridDetailPanelPreProcessors'; import { useGridRowReorder } from '../hooks/features/rowReorder/useGridRowReorder'; import { useGridRowReorderPreProcessors } from '../hooks/features/rowReorder/useGridRowReorderPreProcessors'; +import { useGridLazyLoader } from '../hooks/features/lazyLoader/useGridLazyLoader'; +import { useGridLazyLoaderPreProcessors } from '../hooks/features/lazyLoader/useGridLazyLoaderPreProcessors'; import { useGridRowPinning, rowPinningStateInitializer, @@ -89,6 +91,7 @@ export const useDataGridProComponent = ( useGridSelectionPreProcessors(apiRef, props); useGridRowReorderPreProcessors(apiRef, props); useGridTreeDataPreProcessors(apiRef, props); + useGridLazyLoaderPreProcessors(apiRef, props); useGridRowPinningPreProcessors(apiRef); useGridDetailPanelPreProcessors(apiRef, props); // The column pinning `hydrateColumns` pre-processor must be after every other `hydrateColumns` pre-processors @@ -153,6 +156,7 @@ export const useDataGridProComponent = ( useGridRowReorder(apiRef, props); useGridScroll(apiRef, props); useGridInfiniteLoader(apiRef, props); + useGridLazyLoader(apiRef, props); useGridColumnMenu(apiRef); useGridCsvExport(apiRef); useGridPrintExport(apiRef, props); diff --git a/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts b/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts index 45a207beb4df..f9a4a27c739e 100644 --- a/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts +++ b/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts @@ -6,6 +6,7 @@ import { GridSlotsComponent, DATA_GRID_PROPS_DEFAULT_VALUES, GridValidRowModel, + GridFeatureModeConstant, } from '@mui/x-data-grid'; import { DataGridProProps, @@ -25,6 +26,7 @@ export const DATA_GRID_PRO_PROPS_DEFAULT_VALUES: DataGridProPropsWithDefaultValu disableChildrenFiltering: false, disableChildrenSorting: false, rowReordering: false, + rowsLoadingMode: GridFeatureModeConstant.client, getDetailPanelHeight: () => 500, }; diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/infiniteLoader/useGridInfiniteLoader.ts b/packages/grid/x-data-grid-pro/src/hooks/features/infiniteLoader/useGridInfiniteLoader.ts index 15f73053dcca..a0e82095c177 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/infiniteLoader/useGridInfiniteLoader.ts +++ b/packages/grid/x-data-grid-pro/src/hooks/features/infiniteLoader/useGridInfiniteLoader.ts @@ -7,6 +7,7 @@ import { useGridApiOptionHandler, gridVisibleColumnDefinitionsSelector, gridRowsMetaSelector, + GridFeatureModeConstant, } from '@mui/x-data-grid'; import { useGridVisibleRows } from '@mui/x-data-grid/internals'; import { GridRowScrollEndParams } from '../../../models'; @@ -22,7 +23,7 @@ export const useGridInfiniteLoader = ( apiRef: React.MutableRefObject, props: Pick< DataGridProProcessedProps, - 'onRowsScrollEnd' | 'scrollEndThreshold' | 'pagination' | 'paginationMode' + 'onRowsScrollEnd' | 'scrollEndThreshold' | 'pagination' | 'paginationMode' | 'rowsLoadingMode' >, ): void => { const visibleColumns = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector); @@ -35,7 +36,9 @@ export const useGridInfiniteLoader = ( const handleRowsScrollEnd = React.useCallback( (scrollPosition: GridScrollParams) => { const dimensions = apiRef.current.getRootDimensions(); - if (!dimensions) { + + // Prevent the infite loading working in combination with lazy loading + if (!dimensions || props.rowsLoadingMode !== GridFeatureModeConstant.client) { return; } @@ -59,7 +62,14 @@ export const useGridInfiniteLoader = ( isInScrollBottomArea.current = true; } }, - [contentHeight, props.scrollEndThreshold, visibleColumns, apiRef, currentPage.rows.length], + [ + contentHeight, + props.scrollEndThreshold, + props.rowsLoadingMode, + visibleColumns, + apiRef, + currentPage.rows.length, + ], ); const handleGridScroll = React.useCallback>( diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoader.ts b/packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoader.ts new file mode 100644 index 000000000000..e25123ae15e9 --- /dev/null +++ b/packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoader.ts @@ -0,0 +1,233 @@ +import * as React from 'react'; +import { + useGridApiEventHandler, + GridFeatureModeConstant, + GridRenderedRowsIntervalChangeParams, + useGridSelector, + gridSortModelSelector, + gridFilterModelSelector, + useGridApiOptionHandler, + GridEventListener, + GridRowEntry, + GridDimensions, + GridFeatureMode, +} from '@mui/x-data-grid'; +import { useGridVisibleRows } from '@mui/x-data-grid/internals'; +import { getRenderableIndexes } from '@mui/x-data-grid/hooks/features/virtualization/useGridVirtualScroller'; +import { GridApiPro } from '../../../models/gridApiPro'; +import { + DataGridProProcessedProps, + GridExperimentalProFeatures, +} from '../../../models/dataGridProProps'; +import { GridFetchRowsParams } from '../../../models/gridFetchRowsParams'; + +function findSkeletonRowsSection( + visibleRows: GridRowEntry[], + range: { firstRowIndex: number; lastRowIndex: number }, +) { + let { firstRowIndex, lastRowIndex } = range; + const visibleRowsSection = visibleRows.slice(range.firstRowIndex, range.lastRowIndex); + let startIndex = 0; + let endIndex = visibleRowsSection.length - 1; + let isSkeletonSectionFound = false; + + while (!isSkeletonSectionFound && firstRowIndex < lastRowIndex) { + if (!visibleRowsSection[startIndex].model && !visibleRowsSection[endIndex].model) { + isSkeletonSectionFound = true; + } + + if (visibleRowsSection[startIndex].model) { + startIndex += 1; + firstRowIndex += 1; + } + + if (visibleRowsSection[endIndex].model) { + endIndex -= 1; + lastRowIndex -= 1; + } + } + + return isSkeletonSectionFound + ? { + firstRowIndex, + lastRowIndex, + } + : undefined; +} + +function isLazyLoadingDisabled({ + lazyLoadingFeatureFlag, + rowsLoadingMode, + gridDimentions, +}: { + lazyLoadingFeatureFlag: boolean; + rowsLoadingMode: GridFeatureMode; + gridDimentions: GridDimensions | null; +}) { + if (!lazyLoadingFeatureFlag || !gridDimentions) { + return true; + } + + if (rowsLoadingMode !== GridFeatureModeConstant.server) { + return true; + } + + return false; +} + +/** + * @requires useGridRows (state) + * @requires useGridPagination (state) + * @requires useGridDimensions (method) - can be after + * @requires useGridScroll (method + */ +export const useGridLazyLoader = ( + apiRef: React.MutableRefObject, + props: Pick< + DataGridProProcessedProps, + | 'onFetchRows' + | 'rowsLoadingMode' + | 'pagination' + | 'paginationMode' + | 'rowBuffer' + | 'experimentalFeatures' + >, +): void => { + const visibleRows = useGridVisibleRows(apiRef, props); + const sortModel = useGridSelector(apiRef, gridSortModelSelector); + const filterModel = useGridSelector(apiRef, gridFilterModelSelector); + const renderedRowsIntervalCache = React.useRef({ + firstRowToRender: 0, + lastRowToRender: 0, + }); + const { lazyLoading } = (props.experimentalFeatures ?? {}) as GridExperimentalProFeatures; + + const getCurrentIntervalToRender = React.useCallback(() => { + const currentRenderContext = apiRef.current.unstable_getRenderContext(); + const [firstRowToRender, lastRowToRender] = getRenderableIndexes({ + firstIndex: currentRenderContext.firstRowIndex, + lastIndex: currentRenderContext.lastRowIndex, + minFirstIndex: 0, + maxLastIndex: visibleRows.rows.length, + buffer: props.rowBuffer, + }); + + return { + firstRowToRender, + lastRowToRender, + }; + }, [apiRef, props.rowBuffer, visibleRows.rows.length]); + + const handleRenderedRowsIntervalChange = React.useCallback< + GridEventListener<'renderedRowsIntervalChange'> + >( + (params) => { + const dimensions = apiRef.current.getRootDimensions(); + + if ( + isLazyLoadingDisabled({ + lazyLoadingFeatureFlag: lazyLoading, + rowsLoadingMode: props.rowsLoadingMode, + gridDimentions: dimensions, + }) + ) { + return; + } + + const fetchRowsParams: GridFetchRowsParams = { + firstRowToRender: params.firstRowToRender, + lastRowToRender: params.lastRowToRender, + sortModel, + filterModel, + }; + + if ( + renderedRowsIntervalCache.current.firstRowToRender === params.firstRowToRender && + renderedRowsIntervalCache.current.lastRowToRender === params.lastRowToRender + ) { + return; + } + + if (sortModel.length === 0 && filterModel.items.length === 0) { + const skeletonRowsSection = findSkeletonRowsSection(visibleRows.rows, { + firstRowIndex: params.firstRowToRender, + lastRowIndex: params.lastRowToRender, + }); + + if (!skeletonRowsSection) { + return; + } + + fetchRowsParams.firstRowToRender = skeletonRowsSection.firstRowIndex; + fetchRowsParams.lastRowToRender = skeletonRowsSection.lastRowIndex; + } + + renderedRowsIntervalCache.current = params; + + apiRef.current.publishEvent('fetchRows', fetchRowsParams); + }, + [apiRef, props.rowsLoadingMode, sortModel, filterModel, visibleRows.rows, lazyLoading], + ); + + const handleGridSortModelChange = React.useCallback>( + (newSortModel) => { + const dimensions = apiRef.current.getRootDimensions(); + if ( + isLazyLoadingDisabled({ + lazyLoadingFeatureFlag: lazyLoading, + rowsLoadingMode: props.rowsLoadingMode, + gridDimentions: dimensions, + }) + ) { + return; + } + + apiRef.current.unstable_requestPipeProcessorsApplication('hydrateRows'); + + const { firstRowToRender, lastRowToRender } = getCurrentIntervalToRender(); + const fetchRowsParams: GridFetchRowsParams = { + firstRowToRender, + lastRowToRender, + sortModel: newSortModel, + filterModel, + }; + + apiRef.current.publishEvent('fetchRows', fetchRowsParams); + }, + [apiRef, props.rowsLoadingMode, filterModel, lazyLoading, getCurrentIntervalToRender], + ); + + const handleGridFilterModelChange = React.useCallback>( + (newFilterModel) => { + const dimensions = apiRef.current.getRootDimensions(); + + if ( + isLazyLoadingDisabled({ + lazyLoadingFeatureFlag: lazyLoading, + rowsLoadingMode: props.rowsLoadingMode, + gridDimentions: dimensions, + }) + ) { + return; + } + + apiRef.current.unstable_requestPipeProcessorsApplication('hydrateRows'); + + const { firstRowToRender, lastRowToRender } = getCurrentIntervalToRender(); + const fetchRowsParams: GridFetchRowsParams = { + firstRowToRender, + lastRowToRender, + sortModel, + filterModel: newFilterModel, + }; + + apiRef.current.publishEvent('fetchRows', fetchRowsParams); + }, + [apiRef, props.rowsLoadingMode, sortModel, lazyLoading, getCurrentIntervalToRender], + ); + + useGridApiEventHandler(apiRef, 'renderedRowsIntervalChange', handleRenderedRowsIntervalChange); + useGridApiEventHandler(apiRef, 'sortModelChange', handleGridSortModelChange); + useGridApiEventHandler(apiRef, 'filterModelChange', handleGridFilterModelChange); + useGridApiOptionHandler(apiRef, 'fetchRows', props.onFetchRows); +}; diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoaderPreProcessors.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoaderPreProcessors.tsx new file mode 100644 index 000000000000..c731f461da35 --- /dev/null +++ b/packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoaderPreProcessors.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { + DataGridProProcessedProps, + GridExperimentalProFeatures, +} from '@mui/x-data-grid-pro/models/dataGridProProps'; +import { GridApiPro } from '@mui/x-data-grid-pro/models/gridApiPro'; +import { GridPipeProcessor, useGridRegisterPipeProcessor } from '@mui/x-data-grid/internals'; +import { GridFeatureModeConstant, GridRowId } from '@mui/x-data-grid'; + +export const GRID_SKELETON_ROW_ROOT_ID = 'auto-generated-skeleton-row-root'; + +const getSkeletonRowId = (index: number) => `${GRID_SKELETON_ROW_ROOT_ID}-${index}`; + +export const useGridLazyLoaderPreProcessors = ( + apiRef: React.MutableRefObject, + props: Pick, +) => { + const { lazyLoading } = (props.experimentalFeatures ?? {}) as GridExperimentalProFeatures; + + const addSkeletonRows = React.useCallback>( + (groupingParams) => { + if ( + !lazyLoading || + props.rowsLoadingMode !== GridFeatureModeConstant.server || + !props.rowCount || + groupingParams.ids.length >= props.rowCount + ) { + return groupingParams; + } + + const newRowsIds: GridRowId[] = [...groupingParams.ids]; + + for (let i = 0; i < props.rowCount - groupingParams.ids.length; i += 1) { + const skeletonId = getSkeletonRowId(i); + newRowsIds.push(skeletonId); + } + + return { + ...groupingParams, + ids: newRowsIds, + }; + }, + [props.rowCount, props.rowsLoadingMode, lazyLoading], + ); + + useGridRegisterPipeProcessor(apiRef, 'hydrateRows', addSkeletonRows); +}; diff --git a/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts b/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts index 595bfbdd9ff5..74de2229aa03 100644 --- a/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts +++ b/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts @@ -6,6 +6,7 @@ import { GridRowParams, GridRowId, GridValidRowModel, + GridFeatureMode, } from '@mui/x-data-grid'; import { GridExperimentalFeatures, @@ -24,6 +25,13 @@ import { import { GridInitialStatePro } from './gridStatePro'; export interface GridExperimentalProFeatures extends GridExperimentalFeatures { + /** + * Enables the data grid to lazy load rows while scrolling. + */ + lazyLoading: boolean; + /** + * Enables the the ability for rows to be pinned in data grid. + */ rowPinning: boolean; } @@ -104,6 +112,13 @@ export interface DataGridProPropsWithDefaultValue extends DataGridPropsWithDefau * @default false */ rowReordering: boolean; + /** + * Loading rows can be processed on the server or client-side. + * Set it to 'client' if you would like enable infnite loading. + * Set it to 'server' if you would like to enable lazy loading. + * * @default "client" + */ + rowsLoadingMode: GridFeatureMode; } export interface DataGridProPropsWithoutDefaultValue @@ -194,6 +209,13 @@ export interface DataGridProPropsWithoutDefaultValue; + /** + * Callback fired when rowCount is set and the next batch of virtualized rows is rendered. + * @param {GridFetchRowsParams} params With all properties from [[GridFetchRowsParams]]. + * @param {MuiEvent<{}>} event The event object. + * @param {GridCallbackDetails} details Additional details for this callback. + */ + onFetchRows?: GridEventListener<'fetchRows'>; /** * Rows data to pin on top or bottom. */ diff --git a/packages/grid/x-data-grid-pro/src/models/gridFetchRowsParams.ts b/packages/grid/x-data-grid-pro/src/models/gridFetchRowsParams.ts new file mode 100644 index 000000000000..73d1ee9319bc --- /dev/null +++ b/packages/grid/x-data-grid-pro/src/models/gridFetchRowsParams.ts @@ -0,0 +1,23 @@ +import { GridFilterModel, GridSortModel } from '@mui/x-data-grid/models'; + +/** + * Object passed as parameter to the [[onFetchRows]] option. + */ +export interface GridFetchRowsParams { + /** + * The index of the first row to render. + */ + firstRowToRender: number; + /** + * The index of the last row to render. + */ + lastRowToRender: number; + /** + * The sort model used to sort the grid. + */ + sortModel: GridSortModel; + /** + * The filter model. + */ + filterModel: GridFilterModel; +} diff --git a/packages/grid/x-data-grid-pro/src/models/index.ts b/packages/grid/x-data-grid-pro/src/models/index.ts index 1ff808230e1d..43bbe4ae8279 100644 --- a/packages/grid/x-data-grid-pro/src/models/index.ts +++ b/packages/grid/x-data-grid-pro/src/models/index.ts @@ -1,3 +1,4 @@ export * from './gridGroupingColDefOverride'; export * from './gridRowScrollEndParams'; export * from './gridRowOrderChangeParams'; +export * from './gridFetchRowsParams'; diff --git a/packages/grid/x-data-grid-pro/src/tests/events.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/events.DataGridPro.test.tsx index ca64e8fde7d3..a2d8e19e80fb 100644 --- a/packages/grid/x-data-grid-pro/src/tests/events.DataGridPro.test.tsx +++ b/packages/grid/x-data-grid-pro/src/tests/events.DataGridPro.test.tsx @@ -336,6 +336,24 @@ describe(' - Events Params', () => { expect(handleRowsScrollEnd.callCount).to.equal(1); }); + it('publishing GRID_ROWS_SCROLL should call onFetchRows callback when rows lazy loading is enabled', () => { + const handleFetchRows = spy(); + render( + , + ); + act(() => apiRef.current.publishEvent('rowsScroll', { left: 0, top: 3 * 52 })); + expect(handleFetchRows.callCount).to.equal(1); + }); + it('call onRowsScrollEnd when viewport scroll reaches the bottom', function test() { if (isJSDOM) { this.skip(); // Needs layout diff --git a/packages/grid/x-data-grid-pro/src/tests/lazyLoader.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/lazyLoader.DataGridPro.test.tsx new file mode 100644 index 000000000000..f8a98ff7aa2b --- /dev/null +++ b/packages/grid/x-data-grid-pro/src/tests/lazyLoader.DataGridPro.test.tsx @@ -0,0 +1,114 @@ +import * as React from 'react'; +// @ts-ignore Remove once the test utils are typed +import { createRenderer, fireEvent, act } from '@mui/monorepo/test/utils'; +import { getColumnHeaderCell, getRow } from 'test/utils/helperFn'; +import { expect } from 'chai'; +import { + DataGridPro, + DataGridProProps, + GridApi, + GridColumns, + GridRowModel, + GridRowsProp, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { spy } from 'sinon'; + +const isJSDOM = /jsdom/.test(window.navigator.userAgent); + +describe(' - Lazy loader', () => { + const { render } = createRenderer(); + + const baselineProps: { rows: GridRowsProp; columns: GridColumns } = { + rows: [ + { + id: 1, + first: 'Mike', + }, + { + id: 2, + first: 'Jack', + }, + { + id: 3, + first: 'Jim', + }, + ], + columns: [{ field: 'id' }, { field: 'first' }], + }; + + let apiRef: React.MutableRefObject; + + const TestLazyLoader = (props: Partial) => { + apiRef = useGridApiRef(); + return ( +
+ +
+ ); + }; + + it('should not call onFetchRows if the viewport is fully loaded', function test() { + if (isJSDOM) { + this.skip(); // Needs layout + } + const handleFetchRows = spy(); + const rows = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }, { id: 7 }]; + render(); + expect(handleFetchRows.callCount).to.equal(0); + }); + + it('should call onFetchRows when sorting is applied', function test() { + if (isJSDOM) { + this.skip(); // Needs layout + } + const handleFetchRows = spy(); + render(); + + expect(handleFetchRows.callCount).to.equal(1); + // Should be 1. When tested in the browser it's called only 2 time + fireEvent.click(getColumnHeaderCell(0)); + expect(handleFetchRows.callCount).to.equal(2); + }); + + it('should render skeleton cell if rowCount is bigger than the number of rows', function test() { + if (isJSDOM) { + this.skip(); // Needs layout + } + + render(); + + // The 4th row should be a skeleton one + expect(getRow(3).dataset.id).to.equal('auto-generated-skeleton-row-root-0'); + }); + + it('should update all rows accordingly when `apiRef.current.unstable_replaceRows` is called', () => { + render(); + + const newRows: GridRowModel[] = [ + { id: 4, name: 'John' }, + { id: 5, name: 'Mac' }, + ]; + + const initialAllRows = apiRef.current.state.rows.ids; + expect(initialAllRows.slice(3, 6)).to.deep.equal([ + 'auto-generated-skeleton-row-root-0', + 'auto-generated-skeleton-row-root-1', + 'auto-generated-skeleton-row-root-2', + ]); + act(() => apiRef.current.unstable_replaceRows(4, newRows)); + + const updatedAllRows = apiRef.current.state.rows.ids; + expect(updatedAllRows.slice(4, 6)).to.deep.equal([4, 5]); + }); +}); diff --git a/packages/grid/x-data-grid-pro/src/typeOverloads/modules.ts b/packages/grid/x-data-grid-pro/src/typeOverloads/modules.ts index b2f90c61545e..e804ab1b6d3d 100644 --- a/packages/grid/x-data-grid-pro/src/typeOverloads/modules.ts +++ b/packages/grid/x-data-grid-pro/src/typeOverloads/modules.ts @@ -1,5 +1,9 @@ import { GridRowId } from '@mui/x-data-grid'; -import type { GridRowScrollEndParams, GridRowOrderChangeParams } from '../models'; +import type { + GridRowScrollEndParams, + GridRowOrderChangeParams, + GridFetchRowsParams, +} from '../models'; import type { GridColumnPinningInternalCache, GridPinnedColumns, @@ -29,6 +33,10 @@ export interface GridEventLookupPro { * Fired when the user ends reordering a row. */ rowOrderChange: { params: GridRowOrderChangeParams }; + /** + * Fired when a new batch of rows is requested to be loaded. Called with a [[GridFetchRowsParams]] object. + */ + fetchRows: { params: GridFetchRowsParams }; } export interface GridPipeProcessingLookupPro { diff --git a/packages/grid/x-data-grid/src/components/GridRow.tsx b/packages/grid/x-data-grid/src/components/GridRow.tsx index 0671496bef5c..4dc39966e32c 100644 --- a/packages/grid/x-data-grid/src/components/GridRow.tsx +++ b/packages/grid/x-data-grid/src/components/GridRow.tsx @@ -1,5 +1,3 @@ -/* eslint-disable jsx-a11y/click-events-have-key-events */ -/* eslint-disable jsx-a11y/interactive-supports-focus */ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; @@ -30,6 +28,8 @@ import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../constants/gridDetailPanelTogg import { gridSortModelSelector } from '../hooks/features/sorting/gridSortingSelector'; import { gridRowTreeDepthSelector } from '../hooks/features/rows/gridRowsSelector'; import { gridDensityHeaderGroupingMaxDepthSelector } from '../hooks/features/density/densitySelector'; +import { randomNumberBetween } from '../utils/utils'; +import { GridCellProps } from './cell/GridCell'; export interface GridRowProps { rowId: GridRowId; @@ -41,7 +41,6 @@ export interface GridRowProps { index: number; rowHeight: number | 'auto'; containerWidth: number; - row: GridRowModel; firstColumnToRender: number; lastColumnToRender: number; visibleColumns: GridStateColDef[]; @@ -49,6 +48,7 @@ export interface GridRowProps { cellFocus: GridCellIdentifier | null; cellTabIndex: GridCellIdentifier | null; editRowsState: GridEditRowsModel; + row?: GridRowModel; isLastVisible?: boolean; onClick?: React.MouseEventHandler; onDoubleClick?: React.MouseEventHandler; @@ -251,6 +251,120 @@ function GridRow(props: React.HTMLAttributes & GridRowProps) { [apiRef, onClick, publish, rowId], ); + const getCell = React.useCallback( + ( + column: GridStateColDef, + cellProps: Pick< + GridCellProps, + 'width' | 'colSpan' | 'showRightBorder' | 'indexRelativeToAllColumns' + >, + ) => { + const cellParams = apiRef.current.getCellParams(rowId, column.field); + + const classNames: string[] = []; + + const disableDragEvents = + (rootProps.disableColumnReorder && column.disableReorder) || + (!(rootProps as any).rowReordering && + !!sortModel.length && + treeDepth > 1 && + Object.keys(editRowsState).length > 0); + + if (column.cellClassName) { + classNames.push( + clsx( + typeof column.cellClassName === 'function' + ? column.cellClassName(cellParams) + : column.cellClassName, + ), + ); + } + + const editCellState = editRowsState[rowId] ? editRowsState[rowId][column.field] : null; + let content: React.ReactNode = null; + + if (editCellState == null && column.renderCell) { + content = column.renderCell({ ...cellParams, api: apiRef.current }); + // TODO move to GridCell + classNames.push( + clsx(gridClasses['cell--withRenderer'], rootProps.classes?.['cell--withRenderer']), + ); + } + + if (editCellState != null && column.renderEditCell) { + let updatedRow = row; + if (apiRef.current.unstable_getRowWithUpdatedValues) { + // Only the new editing API has this method + updatedRow = apiRef.current.unstable_getRowWithUpdatedValues(rowId, column.field); + } + + const params: GridRenderEditCellParams = { + ...cellParams, + row: updatedRow, + ...editCellState, + api: apiRef.current, + }; + + content = column.renderEditCell(params); + // TODO move to GridCell + classNames.push(clsx(gridClasses['cell--editing'], rootProps.classes?.['cell--editing'])); + } + + if (rootProps.getCellClassName) { + // TODO move to GridCell + classNames.push(rootProps.getCellClassName(cellParams)); + } + + const hasFocus = + cellFocus !== null && cellFocus.id === rowId && cellFocus.field === column.field; + + const tabIndex = + cellTabIndex !== null && + cellTabIndex.id === rowId && + cellTabIndex.field === column.field && + cellParams.cellMode === 'view' + ? 0 + : -1; + + return ( + + {content} + + ); + }, + [ + apiRef, + cellTabIndex, + editRowsState, + cellFocus, + rootProps, + row, + rowHeight, + rowId, + treeDepth, + sortModel.length, + ], + ); + const style = { ...styleProp, maxHeight: rowHeight === 'auto' ? 'none' : rowHeight, // max-height doesn't support "auto" @@ -283,6 +397,7 @@ function GridRow(props: React.HTMLAttributes & GridRowProps) { rowClassNames.push(rootProps.getRowClassName(rowParams)); } + const randomNumber = randomNumberBetween(10000, 20, 80); const cells: JSX.Element[] = []; for (let i = 0; i < renderedColumns.length; i += 1) { @@ -295,110 +410,44 @@ function GridRow(props: React.HTMLAttributes & GridRowProps) { ? rootProps.showCellRightBorder : !removeLastBorderRight && rootProps.disableExtendRowFullWidth; - const cellParams = apiRef.current.getCellParams(rowId, column.field); - - const classNames: string[] = []; - - const disableDragEvents = - (rootProps.disableColumnReorder && column.disableReorder) || - (!(rootProps as any).rowReordering && - !!sortModel.length && - treeDepth > 1 && - Object.keys(editRowsState).length > 0); - - if (column.cellClassName) { - classNames.push( - clsx( - typeof column.cellClassName === 'function' - ? column.cellClassName(cellParams) - : column.cellClassName, - ), - ); - } - - const editCellState = editRowsState[rowId] ? editRowsState[rowId][column.field] : null; - let content: React.ReactNode = null; - - if (editCellState == null && column.renderCell) { - content = column.renderCell({ ...cellParams, api: apiRef.current }); - // TODO move to GridCell - classNames.push( - clsx(gridClasses['cell--withRenderer'], rootProps.classes?.['cell--withRenderer']), - ); - } - - if (editCellState != null && column.renderEditCell) { - let updatedRow = row; - if (apiRef.current.unstable_getRowWithUpdatedValues) { - // Only the new editing API has this method - updatedRow = apiRef.current.unstable_getRowWithUpdatedValues(rowId, column.field); - } - - const params: GridRenderEditCellParams = { - ...cellParams, - row: updatedRow, - ...editCellState, - api: apiRef.current, - }; - - content = column.renderEditCell(params); - // TODO move to GridCell - classNames.push(clsx(gridClasses['cell--editing'], rootProps.classes?.['cell--editing'])); - } - - if (rootProps.getCellClassName) { - // TODO move to GridCell - classNames.push(rootProps.getCellClassName(cellParams)); - } - - const hasFocus = - cellFocus !== null && cellFocus.id === rowId && cellFocus.field === column.field; - - const tabIndex = - cellTabIndex !== null && - cellTabIndex.id === rowId && - cellTabIndex.field === column.field && - cellParams.cellMode === 'view' - ? 0 - : -1; - const cellColSpanInfo = apiRef.current.unstable_getCellColSpanInfo( rowId, indexRelativeToAllColumns, ); if (cellColSpanInfo && !cellColSpanInfo.spannedByColSpan) { - const { colSpan, width } = cellColSpanInfo.cellProps; - - cells.push( - - {content} - , - ); + if (row) { + const { colSpan, width } = cellColSpanInfo.cellProps; + const cellProps = { width, colSpan, showRightBorder, indexRelativeToAllColumns }; + cells.push(getCell(column, cellProps)); + } else { + const { width } = cellColSpanInfo.cellProps; + const contentWidth = Math.round(randomNumber()); + + cells.push( + , + ); + } } } const emptyCellWidth = containerWidth - columnsTotalWidth; + const eventHandlers = row + ? { + onClick: publishClick, + onDoubleClick: publish('rowDoubleClick', onDoubleClick), + onMouseEnter: publish('rowMouseEnter', onMouseEnter), + onMouseLeave: publish('rowMouseLeave', onMouseLeave), + } + : null; + return (
& GridRowProps) { aria-rowindex={ariaRowIndex} aria-selected={selected} style={style} - onClick={publishClick} - onDoubleClick={publish('rowDoubleClick', onDoubleClick)} - onMouseEnter={publish('rowMouseEnter', onMouseEnter)} - onMouseLeave={publish('rowMouseLeave', onMouseLeave)} + {...eventHandlers} {...other} > {cells} @@ -439,7 +485,7 @@ GridRow.propTypes = { isLastVisible: PropTypes.bool, lastColumnToRender: PropTypes.number.isRequired, renderedColumns: PropTypes.arrayOf(PropTypes.object).isRequired, - row: PropTypes.object.isRequired, + row: PropTypes.object, rowHeight: PropTypes.oneOfType([PropTypes.oneOf(['auto']), PropTypes.number]).isRequired, rowId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, selected: PropTypes.bool.isRequired, diff --git a/packages/grid/x-data-grid/src/components/cell/GridSkeletonCell.tsx b/packages/grid/x-data-grid/src/components/cell/GridSkeletonCell.tsx new file mode 100644 index 000000000000..ac91e005d19e --- /dev/null +++ b/packages/grid/x-data-grid/src/components/cell/GridSkeletonCell.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import Skeleton from '@mui/material/Skeleton'; +import { capitalize } from '@mui/material/utils'; +import { unstable_composeClasses as composeClasses } from '@mui/material'; +import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; +import { getDataGridUtilityClass } from '../../constants/gridClasses'; +import { DataGridProcessedProps } from '../../models/props/DataGridProps'; + +export interface GridSkeletonCellProps { + width: number; + contentWidth: number; + field: string; + align: string; +} + +type OwnerState = Pick & { + classes?: DataGridProcessedProps['classes']; +}; + +const useUtilityClasses = (ownerState: OwnerState) => { + const { align, classes } = ownerState; + + const slots = { + root: ['cell', 'cellSkeleton', `cell--text${capitalize(align)}`], + }; + + return composeClasses(slots, getDataGridUtilityClass, classes); +}; + +function GridSkeletonCell(props: React.HTMLAttributes & GridSkeletonCellProps) { + const { field, align, width, contentWidth, ...other } = props; + const rootProps = useGridRootProps(); + const ownerState = { classes: rootProps.classes, align }; + const classes = useUtilityClasses(ownerState); + + return ( +
+ +
+ ); +} + +GridSkeletonCell.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + align: PropTypes.string.isRequired, + contentWidth: PropTypes.number.isRequired, + field: PropTypes.string.isRequired, + width: PropTypes.number.isRequired, +} as any; + +export { GridSkeletonCell }; diff --git a/packages/grid/x-data-grid/src/components/cell/index.ts b/packages/grid/x-data-grid/src/components/cell/index.ts index 12904043bdbe..2a54901cc8c1 100644 --- a/packages/grid/x-data-grid/src/components/cell/index.ts +++ b/packages/grid/x-data-grid/src/components/cell/index.ts @@ -6,3 +6,4 @@ export * from './GridEditInputCell'; export * from './GridEditSingleSelectCell'; export * from './GridActionsCell'; export * from './GridActionsCellItem'; +export * from './GridSkeletonCell'; diff --git a/packages/grid/x-data-grid/src/components/containers/GridRootStyles.ts b/packages/grid/x-data-grid/src/components/containers/GridRootStyles.ts index f1f9bd738b75..086d6eff1260 100644 --- a/packages/grid/x-data-grid/src/components/containers/GridRootStyles.ts +++ b/packages/grid/x-data-grid/src/components/containers/GridRootStyles.ts @@ -31,6 +31,7 @@ export const GridRootStyles = styled('div', { { [`& .${gridClasses.cell}`]: styles.cell }, { [`& .${gridClasses.cellContent}`]: styles.cellContent }, { [`& .${gridClasses.cellCheckbox}`]: styles.cellCheckbox }, + { [`& .${gridClasses.cellSkeleton}`]: styles.cellSkeleton }, { [`& .${gridClasses.checkboxInput}`]: styles.checkboxInput }, { [`& .${gridClasses['columnHeader--alignCenter']}`]: styles['columnHeader--alignCenter'] }, { [`& .${gridClasses['columnHeader--alignLeft']}`]: styles['columnHeader--alignLeft'] }, diff --git a/packages/grid/x-data-grid/src/constants/defaultGridSlotsComponents.ts b/packages/grid/x-data-grid/src/constants/defaultGridSlotsComponents.ts index b1f3747e932e..3219512d1b9d 100644 --- a/packages/grid/x-data-grid/src/constants/defaultGridSlotsComponents.ts +++ b/packages/grid/x-data-grid/src/constants/defaultGridSlotsComponents.ts @@ -11,6 +11,7 @@ import { GridArrowDownwardIcon, GridArrowUpwardIcon, GridCell, + GridSkeletonCell, GridCheckIcon, GridCloseIcon, GridColumnIcon, @@ -88,6 +89,7 @@ export const DATA_GRID_DEFAULT_SLOTS_COMPONENTS: GridSlotsComponent = { BaseTooltip: MUITooltip, BasePopper: MUIPopper, Cell: GridCell, + SkeletonCell: GridSkeletonCell, ColumnHeaderFilterIconButton: GridColumnHeaderFilterIconButton, ColumnMenu: GridColumnMenu, ErrorOverlay, diff --git a/packages/grid/x-data-grid/src/constants/gridClasses.ts b/packages/grid/x-data-grid/src/constants/gridClasses.ts index 5689732a045e..ba94b660c26f 100644 --- a/packages/grid/x-data-grid/src/constants/gridClasses.ts +++ b/packages/grid/x-data-grid/src/constants/gridClasses.ts @@ -69,6 +69,10 @@ export interface GridClasses { * Styles applied to the cell checkbox element. */ cellCheckbox: string; + /** + * Styles applied to the skeleton cell element. + */ + cellSkeleton: string; /** * Styles applied to the selection checkbox element. */ @@ -507,6 +511,7 @@ export const gridClasses = generateUtilityClasses('MuiDataGrid', [ 'cell', 'cellContent', 'cellCheckbox', + 'cellSkeleton', 'checkboxInput', 'columnHeader--alignCenter', 'columnHeader--alignLeft', diff --git a/packages/grid/x-data-grid/src/hooks/features/rows/useGridRows.ts b/packages/grid/x-data-grid/src/hooks/features/rows/useGridRows.ts index 80a7a742378d..3744c2b978ae 100644 --- a/packages/grid/x-data-grid/src/hooks/features/rows/useGridRows.ts +++ b/packages/grid/x-data-grid/src/hooks/features/rows/useGridRows.ts @@ -12,6 +12,7 @@ import { gridRowTreeSelector, gridRowIdsSelector, gridRowGroupingNameSelector, + gridRowsIdToIdLookupSelector, } from './gridRowsSelector'; import { GridSignature, useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { GridStateInitializer } from '../../utils/useGridInitializeState'; @@ -162,7 +163,7 @@ export const useGridRows = ( } // we remove duplicate updates. A server can batch updates, and send several updates for the same row in one fn call. - const uniqUpdates = new Map(); + const uniqueUpdates = new Map(); updates.forEach((update) => { const id = getRowIdFromRowModel( @@ -171,10 +172,10 @@ export const useGridRows = ( 'A row was provided without id when calling updateRows():', ); - if (uniqUpdates.has(id)) { - uniqUpdates.set(id, { ...uniqUpdates.get(id), ...update }); + if (uniqueUpdates.has(id)) { + uniqueUpdates.set(id, { ...uniqueUpdates.get(id), ...update }); } else { - uniqUpdates.set(id, update); + uniqueUpdates.set(id, update); } }); @@ -189,7 +190,7 @@ export const useGridRows = ( ids: [...prevCache.ids], }; - uniqUpdates.forEach((partialRow, id) => { + uniqueUpdates.forEach((partialRow, id) => { // eslint-disable-next-line no-underscore-dangle if (partialRow._action === 'delete') { delete newCache.idRowsLookup[id]; @@ -326,11 +327,76 @@ export const useGridRows = ( ids: updatedRows, }, })); - apiRef.current.applySorting(); + apiRef.current.publishEvent('rowsSet'); }, [apiRef, logger], ); + const replaceRows = React.useCallback( + (firstRowToRender, newRows) => { + if (props.signature === GridSignature.DataGrid && newRows.length > 1) { + throw new Error( + [ + "MUI: You can't replace rows using `apiRef.current.unstable_replaceRows` on the DataGrid.", + 'You need to upgrade to DataGridPro or DataGridPremium component to unlock this feature.', + ].join('\n'), + ); + } + + if (newRows.length === 0) { + return; + } + + const allRows = gridRowIdsSelector(apiRef); + const updatedRows = [...allRows]; + const idRowsLookup = gridRowsLookupSelector(apiRef); + const idToIdLookup = gridRowsIdToIdLookupSelector(apiRef); + const tree = gridRowTreeSelector(apiRef); + const updatedIdRowsLookup = { ...idRowsLookup }; + const updatedIdToIdLookup = { ...idToIdLookup }; + const updatedTree = { ...tree }; + + newRows.forEach((row, index) => { + const rowId = getRowIdFromRowModel( + row, + props.getRowId, + 'A row was provided without id when calling replaceRows().', + ); + const [replacedRowId] = updatedRows.splice(firstRowToRender + index, 1, rowId); + + delete updatedIdRowsLookup[replacedRowId]; + delete updatedIdToIdLookup[replacedRowId]; + delete updatedTree[replacedRowId]; + }); + + newRows.forEach((row) => { + const rowTreeNodeConfig: GridRowTreeNodeConfig = { + id: row.id, + parent: null, + depth: 0, + groupingKey: null, + groupingField: null, + }; + updatedIdRowsLookup[row.id] = row; + updatedIdToIdLookup[row.id] = row.id; + updatedTree[row.id] = rowTreeNodeConfig; + }); + + apiRef.current.setState((state) => ({ + ...state, + rows: { + ...state.rows, + idRowsLookup: updatedIdRowsLookup, + idToIdLookup: updatedIdToIdLookup, + tree: updatedTree, + ids: updatedRows, + }, + })); + apiRef.current.publishEvent('rowsSet'); + }, + [apiRef, props.signature, props.getRowId], + ); + const rowApi: GridRowApi = { getRow, getRowModels, @@ -343,6 +409,7 @@ export const useGridRows = ( getRowNode, getRowIndexRelativeToVisibleRows, getRowGroupChildren, + unstable_replaceRows: replaceRows, }; /** diff --git a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx index a05011909b55..a93119b8fcff 100644 --- a/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx +++ b/packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx @@ -278,9 +278,23 @@ export const useGridVirtualScroller = (props: UseGridVirtualScrollerProps) => { const updateRenderContext = React.useCallback( (nextRenderContext: GridRenderContext) => { setRenderContext(nextRenderContext); + + const [firstRowToRender, lastRowToRender] = getRenderableIndexes({ + firstIndex: nextRenderContext.firstRowIndex, + lastIndex: nextRenderContext.lastRowIndex, + minFirstIndex: 0, + maxLastIndex: currentPage.rows.length, + buffer: rootProps.rowBuffer, + }); + + apiRef.current.publishEvent('renderedRowsIntervalChange', { + firstRowToRender, + lastRowToRender, + }); + prevRenderContext.current = nextRenderContext; }, - [setRenderContext, prevRenderContext], + [apiRef, setRenderContext, prevRenderContext, currentPage.rows.length, rootProps.rowBuffer], ); React.useEffect(() => { diff --git a/packages/grid/x-data-grid/src/models/api/gridRowApi.ts b/packages/grid/x-data-grid/src/models/api/gridRowApi.ts index 93691e784e88..e09aa6d560d6 100644 --- a/packages/grid/x-data-grid/src/models/api/gridRowApi.ts +++ b/packages/grid/x-data-grid/src/models/api/gridRowApi.ts @@ -94,4 +94,10 @@ export interface GridRowApi { * @returns {GridRowId[]} The id of each row in the grouping criteria. */ getRowGroupChildren: (params: GridRowGroupChildrenGetterParams) => GridRowId[]; + /** + * Replace a set of rows with new rows. + * @param {number} firstRowToReplace The index of the first row to be replaced. + * @param {GridRowModel[]} newRows The new rows. + */ + unstable_replaceRows: (firstRowToReplace: number, newRows: GridRowModel[]) => void; } diff --git a/packages/grid/x-data-grid/src/models/events/gridEventLookup.ts b/packages/grid/x-data-grid/src/models/events/gridEventLookup.ts index 8d2701fcaecf..98cb8b620876 100644 --- a/packages/grid/x-data-grid/src/models/events/gridEventLookup.ts +++ b/packages/grid/x-data-grid/src/models/events/gridEventLookup.ts @@ -9,6 +9,7 @@ import type { GridHeaderSelectionCheckboxParams, GridMenuParams, GridPreferencePanelParams, + GridRenderedRowsIntervalChangeParams, GridRowParams, GridRowSelectionCheckboxParams, GridScrollParams, @@ -380,6 +381,10 @@ export interface GridEventLookup * @ignore - do not document. */ rowExpansionChange: { params: GridRowTreeNodeConfig }; + /** + * Fired when the rendered rows index interval changes. Called with a [[GridRenderedRowsIntervalChangeParams]] object. + */ + renderedRowsIntervalChange: { params: GridRenderedRowsIntervalChangeParams }; // Edit /** diff --git a/packages/grid/x-data-grid/src/models/events/gridEvents.ts b/packages/grid/x-data-grid/src/models/events/gridEvents.ts index 7375fa9c6ce6..67802c74841f 100644 --- a/packages/grid/x-data-grid/src/models/events/gridEvents.ts +++ b/packages/grid/x-data-grid/src/models/events/gridEvents.ts @@ -85,6 +85,8 @@ enum GridEvents { preferencePanelOpen = 'preferencePanelOpen', menuOpen = 'menuOpen', menuClose = 'menuClose', + renderedRowsIntervalChange = 'renderedRowsIntervalChange', + fetchRows = 'fetchRows', } export type GridEventsStr = keyof GridEventLookup; diff --git a/packages/grid/x-data-grid/src/models/gridSlotsComponent.ts b/packages/grid/x-data-grid/src/models/gridSlotsComponent.ts index 57cc4e7de50d..67ed22378bee 100644 --- a/packages/grid/x-data-grid/src/models/gridSlotsComponent.ts +++ b/packages/grid/x-data-grid/src/models/gridSlotsComponent.ts @@ -51,6 +51,11 @@ export interface GridSlotsComponent extends GridIconSlotsComponent { * @default GridCell */ Cell: React.JSXElementConstructor; + /** + * Component rendered for each skeleton cell. + * @default GridSkeletonCell + */ + SkeletonCell: React.JSXElementConstructor; /** * Filter icon component rendered in each column header. * @default GridColumnHeaderFilterIconButton diff --git a/packages/grid/x-data-grid/src/models/params/gridRenderedRowsIntervalChangeParams.ts b/packages/grid/x-data-grid/src/models/params/gridRenderedRowsIntervalChangeParams.ts new file mode 100644 index 000000000000..1ff94ad9f765 --- /dev/null +++ b/packages/grid/x-data-grid/src/models/params/gridRenderedRowsIntervalChangeParams.ts @@ -0,0 +1,10 @@ +export interface GridRenderedRowsIntervalChangeParams { + /** + * The index of the first row to render. + */ + firstRowToRender: number; + /** + * The index of the last row to render. + */ + lastRowToRender: number; +} diff --git a/packages/grid/x-data-grid/src/models/params/index.ts b/packages/grid/x-data-grid/src/models/params/index.ts index 42de04598300..55504ec7bd8e 100644 --- a/packages/grid/x-data-grid/src/models/params/index.ts +++ b/packages/grid/x-data-grid/src/models/params/index.ts @@ -12,3 +12,4 @@ export * from './gridCellParams'; export * from './gridSortModelParams'; export * from './gridPreferencePanelParams'; export * from './gridMenuParams'; +export * from './gridRenderedRowsIntervalChangeParams'; diff --git a/packages/grid/x-data-grid/src/utils/utils.ts b/packages/grid/x-data-grid/src/utils/utils.ts index 5ccfa10451c0..a7e64d4936d5 100644 --- a/packages/grid/x-data-grid/src/utils/utils.ts +++ b/packages/grid/x-data-grid/src/utils/utils.ts @@ -170,3 +170,20 @@ export function isDeepEqual(a: any, b: any) { // eslint-disable-next-line no-self-compare return a !== a && b !== b; } + +// Pseudo random number. See https://stackoverflow.com/a/47593316 +function mulberry32(a: number): () => number { + return () => { + /* eslint-disable */ + let t = (a += 0x6d2b79f5); + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + /* eslint-enable */ + }; +} + +export function randomNumberBetween(seed: number, min: number, max: number): () => number { + const random = mulberry32(seed); + return () => min + (max - min) * random(); +} diff --git a/scripts/x-data-grid-generator.exports.json b/scripts/x-data-grid-generator.exports.json index 4ff104f05251..3334162b3a68 100644 --- a/scripts/x-data-grid-generator.exports.json +++ b/scripts/x-data-grid-generator.exports.json @@ -13,6 +13,7 @@ { "name": "GridColDefGenerator", "kind": "Interface" }, { "name": "GridDataGeneratorContext", "kind": "Interface" }, { "name": "GridDemoData", "kind": "Interface" }, + { "name": "loadServerRows", "kind": "Variable" }, { "name": "Movie", "kind": "TypeAlias" }, { "name": "QueryOptions", "kind": "Interface" }, { "name": "random", "kind": "Variable" }, @@ -70,7 +71,6 @@ { "name": "renderRating", "kind": "Function" }, { "name": "renderStatus", "kind": "Function" }, { "name": "renderTotalPrice", "kind": "Function" }, - { "name": "ServerOptions", "kind": "Interface" }, { "name": "useDemoData", "kind": "Variable" }, { "name": "UseDemoDataOptions", "kind": "Interface" }, { "name": "useMovieData", "kind": "Variable" } diff --git a/scripts/x-data-grid-premium.exports.json b/scripts/x-data-grid-premium.exports.json index ad4a2b198a41..6bb83c837918 100644 --- a/scripts/x-data-grid-premium.exports.json +++ b/scripts/x-data-grid-premium.exports.json @@ -265,6 +265,7 @@ { "name": "GridExportStateParams", "kind": "Interface" }, { "name": "GridFeatureMode", "kind": "TypeAlias" }, { "name": "GridFeatureModeConstant", "kind": "Variable" }, + { "name": "GridFetchRowsParams", "kind": "Interface" }, { "name": "GridFileExportOptions", "kind": "Interface" }, { "name": "gridFilterableColumnDefinitionsSelector", "kind": "Variable" }, { "name": "gridFilterableColumnLookupSelector", "kind": "Variable" }, @@ -389,6 +390,7 @@ { "name": "GridRenderContext", "kind": "Interface" }, { "name": "GridRenderContextProps", "kind": "TypeAlias" }, { "name": "GridRenderEditCellParams", "kind": "Interface" }, + { "name": "GridRenderedRowsIntervalChangeParams", "kind": "Interface" }, { "name": "GridRenderPaginationProps", "kind": "Interface" }, { "name": "GridRenderRowProps", "kind": "Interface" }, { "name": "gridResizingColumnFieldSelector", "kind": "Variable" }, @@ -463,6 +465,8 @@ { "name": "gridSelectionStateSelector", "kind": "Variable" }, { "name": "GridSeparatorIcon", "kind": "Variable" }, { "name": "GridSignature", "kind": "Enum" }, + { "name": "GridSkeletonCell", "kind": "Function" }, + { "name": "GridSkeletonCellProps", "kind": "Interface" }, { "name": "GridSlotsComponent", "kind": "Interface" }, { "name": "GridSlotsComponentsProps", "kind": "Interface" }, { "name": "GridSortApi", "kind": "Interface" }, diff --git a/scripts/x-data-grid-pro.exports.json b/scripts/x-data-grid-pro.exports.json index d995dfb8337f..db8050c65627 100644 --- a/scripts/x-data-grid-pro.exports.json +++ b/scripts/x-data-grid-pro.exports.json @@ -239,6 +239,7 @@ { "name": "GridExportStateParams", "kind": "Interface" }, { "name": "GridFeatureMode", "kind": "TypeAlias" }, { "name": "GridFeatureModeConstant", "kind": "Variable" }, + { "name": "GridFetchRowsParams", "kind": "Interface" }, { "name": "GridFileExportOptions", "kind": "Interface" }, { "name": "gridFilterableColumnDefinitionsSelector", "kind": "Variable" }, { "name": "gridFilterableColumnLookupSelector", "kind": "Variable" }, @@ -360,6 +361,7 @@ { "name": "GridRenderContext", "kind": "Interface" }, { "name": "GridRenderContextProps", "kind": "TypeAlias" }, { "name": "GridRenderEditCellParams", "kind": "Interface" }, + { "name": "GridRenderedRowsIntervalChangeParams", "kind": "Interface" }, { "name": "GridRenderPaginationProps", "kind": "Interface" }, { "name": "GridRenderRowProps", "kind": "Interface" }, { "name": "gridResizingColumnFieldSelector", "kind": "Variable" }, @@ -426,6 +428,8 @@ { "name": "gridSelectionStateSelector", "kind": "Variable" }, { "name": "GridSeparatorIcon", "kind": "Variable" }, { "name": "GridSignature", "kind": "Enum" }, + { "name": "GridSkeletonCell", "kind": "Function" }, + { "name": "GridSkeletonCellProps", "kind": "Interface" }, { "name": "GridSlotsComponent", "kind": "Interface" }, { "name": "GridSlotsComponentsProps", "kind": "Interface" }, { "name": "GridSortApi", "kind": "Interface" }, diff --git a/scripts/x-data-grid.exports.json b/scripts/x-data-grid.exports.json index 494758be7b84..9131314ded99 100644 --- a/scripts/x-data-grid.exports.json +++ b/scripts/x-data-grid.exports.json @@ -333,6 +333,7 @@ { "name": "GridRenderContext", "kind": "Interface" }, { "name": "GridRenderContextProps", "kind": "TypeAlias" }, { "name": "GridRenderEditCellParams", "kind": "Interface" }, + { "name": "GridRenderedRowsIntervalChangeParams", "kind": "Interface" }, { "name": "GridRenderPaginationProps", "kind": "Interface" }, { "name": "GridRenderRowProps", "kind": "Interface" }, { "name": "GridRoot", "kind": "Variable" }, @@ -394,6 +395,8 @@ { "name": "gridSelectionStateSelector", "kind": "Variable" }, { "name": "GridSeparatorIcon", "kind": "Variable" }, { "name": "GridSignature", "kind": "Enum" }, + { "name": "GridSkeletonCell", "kind": "Function" }, + { "name": "GridSkeletonCellProps", "kind": "Interface" }, { "name": "GridSlotsComponent", "kind": "Interface" }, { "name": "GridSlotsComponentsProps", "kind": "Interface" }, { "name": "GridSortApi", "kind": "Interface" },