Skip to content

Commit

Permalink
feat(react-grid): add scroll to row in virtual table (#2511)
Browse files Browse the repository at this point in the history
  • Loading branch information
LazyLahtak authored Dec 31, 2019
1 parent d4ad281 commit 089c1b5
Show file tree
Hide file tree
Showing 20 changed files with 318 additions and 8 deletions.
3 changes: 3 additions & 0 deletions packages/dx-grid-core/src/plugins/virtual-table/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ export const emptyViewport: GridViewport = {
width: 800,
height: 600,
};

export const TOP_POSITION = Symbol('top');
export const BOTTOM_POSITION = Symbol('bottom');
43 changes: 42 additions & 1 deletion packages/dx-grid-core/src/plugins/virtual-table/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getViewport, checkColumnWidths } from './helpers';
import { getViewport, checkColumnWidths, calculateScrollHeight, getScrollTop } from './helpers';
import { TOP_POSITION, BOTTOM_POSITION } from './constants';

const estimatedRowheight = 40;
const createItems = length => (
Expand Down Expand Up @@ -170,4 +171,44 @@ describe('#checkColumnWidths', () => {
.toThrow(/columnExtension.*VirtualTable/);
});
});

describe('#calculateScrollHeight', () => {
const rowHeight = 100;

it('should work', () => {
expect(calculateScrollHeight(rowHeight, 10))
.toEqual(1000);
});

it('should return "undefined" if index is not defined', () => {
expect(calculateScrollHeight(rowHeight, undefined))
.toEqual(undefined);
});

it('should return "undefined" if index is less than 0', () => {
expect(calculateScrollHeight(rowHeight, -1))
.toEqual(undefined);
});
});

describe('#getScrollTop', () => {
const rows = [{ rowId: 1 }, { rowId: 2 }, { rowId: 3 }, { rowId: 4 }, { rowId: 5 }];
const rowHeight = 100;
const rowId = 4;

it('should work', () => {
expect(getScrollTop(rows, rows.length, rowId, rowHeight, false))
.toEqual(300);
});

it('should return 0 if scrolled to TOP_POSITION', () => {
expect(getScrollTop(rows, rows.length, TOP_POSITION, rowHeight, false))
.toEqual(0);
});

it('should return height of scroll if scrolled to BOTTOM_POSITION', () => {
expect(getScrollTop(rows, rows.length, BOTTOM_POSITION, rowHeight, false))
.toEqual(rowHeight * rows.length);
});
});
});
31 changes: 30 additions & 1 deletion packages/dx-grid-core/src/plugins/virtual-table/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import {
getRowsVisibleBoundary, getColumnBoundaries,
} from '../../utils/virtual-table';
import { GetViewportFn, CheckTableColumnWidths, TableColumn } from '../../types';
import {
GetViewportFn,
CheckTableColumnWidths,
TableColumn,
GetScrollHeightByIndex,
GetScrollPosition,
} from '../../types';
import { arraysEqual } from './utils';
import { TOP_POSITION, BOTTOM_POSITION } from './constants';

const VALID_UNITS = ['px', ''];
/* tslint:disable max-line-length */
Expand Down Expand Up @@ -88,3 +95,25 @@ export const checkColumnWidths: CheckTableColumnWidths = (tableColumns) => {
return acc;
}, [] as TableColumn[]);
};

export const calculateScrollHeight: GetScrollHeightByIndex = (rowHeight, index) =>
index > -1 ? rowHeight * index : undefined;

export const getScrollTop: GetScrollPosition = (rows, rowsCount, rowId, rowHeight, isDataRemote) => {
if (rowId === TOP_POSITION) {
return 0;
}
if (rowId === BOTTOM_POSITION) {
return rowsCount * rowHeight;
}

const searchIndexRequired = !isDataRemote && rowId !== undefined;
const indexById = searchIndexRequired
? rows.findIndex(row => row.rowId === rowId)
: undefined;

return calculateScrollHeight(
rowHeight,
indexById!,
);
};
9 changes: 9 additions & 0 deletions packages/dx-grid-core/src/types/virtual-table.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PureComputed } from '@devexpress/dx-core';
import { Getters } from '@devexpress/dx-react-core';
import { TableColumn, TableRow, GetCellColSpanFn } from './table.types';
import { Row } from './grid-core.types';

/** @internal */
export type GetColumnWidthFn = PureComputed<[TableColumn, number?], number | null>;
Expand Down Expand Up @@ -159,3 +160,11 @@ export type PageTriggersMetaFn = PureComputed<
export type CheckTableColumnWidths = PureComputed<
[TableColumn[]], void
>;
/** @internal */
export type GetScrollHeightByIndex = PureComputed<
[number, number], number | undefined
>;
/** @internal */
export type GetScrollPosition = PureComputed<
[Row[], number, number | string | symbol | undefined, number, number], number | undefined
>;
8 changes: 8 additions & 0 deletions packages/dx-react-core/src/sizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ export class Sizer extends React.PureComponent<SizerProps> {

componentDidUpdate() {
this.setupListeners();
// We can scroll the VirtualTable manually only by changing
// containter's (rootNode) scrollTop property.
// Viewport changes its own properties automatically.
const { scrollTop } = this.props;
if (scrollTop! > -1) {
this.rootNode.scrollTop = scrollTop!;
}
}

// There is no need to remove listeners as divs are removed from DOM when component is unmount.
Expand Down Expand Up @@ -138,6 +145,7 @@ export class Sizer extends React.PureComponent<SizerProps> {
onSizeChange,
containerComponent: Container,
style,
scrollTop,
...restProps
} = this.props;

Expand Down
1 change: 1 addition & 0 deletions packages/dx-react-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type SizerProps = {
onScroll?: (e) => void;
containerComponent?: any;
style?: object;
scrollTop?: number;
};

export type Getters = { readonly [getterName: string]: any };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,8 @@ export const VirtualTable: React.ComponentType<VirtualTableProps> & {
COLUMN_TYPE: symbol;
ROW_TYPE: symbol;
NODATA_ROW_TYPE: symbol;
TOP_POSITION: symbol;
BOTTOM_POSITION: symbol;
} & {
Table: React.ComponentType<object & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
TableHead: React.ComponentType<object & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
Expand All @@ -809,6 +811,8 @@ export const VirtualTable: React.ComponentType<VirtualTableProps> & {
StubRow: React.ComponentType<Table_2.RowProps & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
StubCell: React.ComponentType<Table_2.CellProps & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
StubHeaderCell: React.ComponentType<Table_2.CellProps & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
} & {
scrollToRow: (rowId: number | string) => void;
};

// @public (undocumented)
Expand All @@ -824,6 +828,7 @@ export interface VirtualTableProps {
messages?: Table_2.LocalizationMessages;
noDataCellComponent?: React.ComponentType<Table_2.NoDataCellProps>;
noDataRowComponent?: React.ComponentType<Table_2.RowProps>;
onTopRowChange?: (rowId: number | string) => void;
rowComponent?: React.ComponentType<Table_2.DataRowProps>;
stubCellComponent?: React.ComponentType<Table_2.CellProps>;
stubHeaderCellComponent?: React.ComponentType<Table_2.CellProps>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,8 @@ export const VirtualTable: React.ComponentType<VirtualTableProps> & {
COLUMN_TYPE: symbol;
ROW_TYPE: symbol;
NODATA_ROW_TYPE: symbol;
TOP_POSITION: symbol;
BOTTOM_POSITION: symbol;
} & {
Table: React.ComponentType<object & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
TableHead: React.ComponentType<object & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
Expand All @@ -809,6 +811,8 @@ export const VirtualTable: React.ComponentType<VirtualTableProps> & {
StubRow: React.ComponentType<Table_2.RowProps & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
StubCell: React.ComponentType<Table_2.CellProps & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
StubHeaderCell: React.ComponentType<Table_2.CellProps & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
} & {
scrollToRow: (rowId: number | string) => void;
};

// @public (undocumented)
Expand All @@ -824,6 +828,7 @@ export interface VirtualTableProps {
messages?: Table_2.LocalizationMessages;
noDataCellComponent?: React.ComponentType<Table_2.NoDataCellProps>;
noDataRowComponent?: React.ComponentType<Table_2.RowProps>;
onTopRowChange?: (rowId: number | string) => void;
rowComponent?: React.ComponentType<Table_2.DataRowProps>;
stubCellComponent?: React.ComponentType<Table_2.CellProps>;
stubHeaderCellComponent?: React.ComponentType<Table_2.CellProps>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React, { useState, useRef, useCallback } from 'react';<%&additionalImports%>
import {
EditingState,
SortingState,
IntegratedSorting,
} from '@devexpress/dx-react-grid';
import {
Grid,
VirtualTable,
TableHeaderRow,
TableEditColumn,
TableEditRow,
} from '@devexpress/dx-react-grid-<%&themeName%>';

import {
generateRows,
defaultColumnValues,
} from '../../../demo-data/generator';

const getRowId = row => row.id;

export default () => {
const [columns] = useState([
{ name: 'name', title: 'Name' },
{ name: 'gender', title: 'Gender' },
{ name: 'city', title: 'City' },
{ name: 'car', title: 'Car' },
]);
const [rows, setRows] = useState(generateRows({
columnValues: { id: ({ index }) => index, ...defaultColumnValues },
length: 100000,
}));
const [tableColumnExtensions] = useState([
{ columnName: 'id', width: 75 },
]);
const vtRef = useRef();

const scrollToRow = useCallback((rowId) => {
vtRef.current.scrollToRow(rowId);
}, [vtRef]);

const commitChanges = ({ added, changed, deleted }) => {
let changedRows;
let startingAddedId;
if (added) {
startingAddedId = rows.length > 0 ? rows[rows.length - 1].id + 1 : 0;
changedRows = [
...rows,
...added.map((row, index) => ({
id: startingAddedId + index,
...row,
})),
];
}
if (changed) {
changedRows = rows.map(row => (changed[row.id] ? { ...row, ...changed[row.id] } : row));
}
if (deleted) {
const deletedSet = new Set(deleted);
changedRows = rows.filter(row => !deletedSet.has(row.id));
}

setRows(changedRows);
if (added) scrollToRow(startingAddedId);
};

const commandWithScroll = useCallback((props) => {
const { id, onExecute } = props;
if (id === 'add') {
return (
<TableEditColumn.Command
{...props}
onExecute={() => {
scrollToRow(VirtualTable.TOP_POSITION);
onExecute();
}}
/>
);
}
return <TableEditColumn.Command {...props} />;
}, [scrollToRow]);

return (
<<%&wrapperTag%><%&wrapperAttributes%>>
<Grid
rows={rows}
columns={columns}
getRowId={getRowId}
>
<EditingState
defaultAddedRows={generateRows({ length: 1 })}
onCommitChanges={commitChanges}
/>
<SortingState
sorting={[{ columnName: 'name', direction: 'asc' }]}
/>
<IntegratedSorting />
<VirtualTable
columnExtensions={tableColumnExtensions}
ref={vtRef}
/>
<TableHeaderRow
showSortingControls
/>
<TableEditRow />
<TableEditColumn
showAddCommand
showDeleteCommand
commandComponent={commandWithScroll}
/>
</Grid>
</<%&wrapperTag%>>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,8 @@ export const VirtualTable: React.ComponentType<VirtualTableProps> & {
COLUMN_TYPE: symbol;
ROW_TYPE: symbol;
NODATA_ROW_TYPE: symbol;
TOP_POSITION: symbol;
BOTTOM_POSITION: symbol;
} & {
Table: React.ComponentType<object & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
TableHead: React.ComponentType<object & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
Expand All @@ -809,6 +811,8 @@ export const VirtualTable: React.ComponentType<VirtualTableProps> & {
StubRow: React.ComponentType<Table_2.RowProps & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
StubCell: React.ComponentType<Table_2.CellProps & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
StubHeaderCell: React.ComponentType<Table_2.CellProps & { className?: string; style?: React.CSSProperties; [x: string]: any }>;
} & {
scrollToRow: (rowId: number | string) => void;
};

// @public (undocumented)
Expand All @@ -824,6 +828,7 @@ export interface VirtualTableProps {
messages?: Table_2.LocalizationMessages;
noDataCellComponent?: React.ComponentType<Table_2.NoDataCellProps>;
noDataRowComponent?: React.ComponentType<Table_2.RowProps>;
onTopRowChange?: (rowId: number | string) => void;
rowComponent?: React.ComponentType<Table_2.DataRowProps>;
stubCellComponent?: React.ComponentType<Table_2.CellProps>;
stubHeaderCellComponent?: React.ComponentType<Table_2.CellProps>;
Expand Down
1 change: 1 addition & 0 deletions packages/dx-react-grid/api/dx-react-grid.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1478,6 +1478,7 @@ export interface VirtualTableProps {
messages?: Table.LocalizationMessages;
noDataCellComponent: React.ComponentType<Table.NoDataCellProps>;
noDataRowComponent: React.ComponentType<Table.RowProps>;
onTopRowChange: (rowId: number | string | symbol) => void;
rowComponent: React.ComponentType<Table.DataRowProps>;
// (undocumented)
skeletonCellComponent: React.ComponentType<Table.CellProps>;
Expand Down
10 changes: 10 additions & 0 deletions packages/dx-react-grid/docs/guides/virtual-scrolling.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ If the Grid should have the same size as the container element, set the `Virtual

.embedded-demo({ "path": "grid-virtual-scrolling/stretching-to-parent-element", "showThemeSelector": true })

## Scroll to Row

To scroll the table to a particular row, call the `scrollToRow` method and pass the row ID as its parameter. To call the method, you need the `VirtualTable` plugin's ref.

In the following demo, the `scrollToRow` method is used to scroll the table to a new or saved row. When you add a new row, it is added to the top of the table, and the table is scrolled to it. When you save the row, its position is changed according to sorting, and the table is scrolled to that position.

.embedded-demo({ "path": "grid-virtual-scrolling/scroll-to-row", "showThemeSelector": true })

NOTE: Scrolling to a row cannot be used with [lazy loading](./lazy-loading.md/#react-grid---virtual-scrolling-with-remote-data-lazy-loading). This is because the Grid loads rows in parts in lazy loading mode, and scrolling to a row requires all the row IDs.

## Note on the use of `VirtualTable` with `DataTypeProvider` and custom components

If you use a custom `rowComponent` or `cellComponent`, its height and the `estimatedRowHeight` value should be equal. The same applies to a custom formatter defined in the [DataTypeProvider](../reference/data-type-provider.md) plugin.
9 changes: 9 additions & 0 deletions packages/dx-react-grid/docs/reference/virtual-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ stubRowComponent | ComponentType&lt;[Table.RowProps](#tablerowprops)&gt; | | A c
stubCellComponent | ComponentType&lt;[Table.CellProps](table.md#tablecellprops)&gt; | | A component that renders a stub table cell if the cell value is not provided.
stubHeaderCellComponent | ComponentType&lt;[Table.CellProps](table.md#tablecellprops)&gt; | | A component that renders a stub header cell if the cell value is not provided.
messages? | [Table.LocalizationMessages](table.md#localization-messages) | | An object that specifies the localization messages.
onTopRowChange? | (rowId: number &#124; string) => void | | Handles a change of the top row.

## Methods

Name | Type | Description
-----|------|------------
scrollToRow | (rowId: number &#124; string) => void | Scrolls table to a row with the specified ID.

## Interfaces

Expand Down Expand Up @@ -77,6 +84,8 @@ Field | Type | Description
COLUMN_TYPE | symbol | The data column type's indentifier.
ROW_TYPE | symbol | The data row type's indentifier.
NODATA_ROW_TYPE | symbol | The nodata row type's indentifier.
TOP_POSITION | symbol | The top position of the table. Used in scrolling.
BOTTOM_POSITION | symbol | The bottom position of the table. Used in scrolling.

## Plugin Developer Reference

Expand Down
Loading

0 comments on commit 089c1b5

Please sign in to comment.