Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Beta fix stub cells #460

Merged
merged 7 commits into from
Jun 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions examples/AutoScrollExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* Copyright Schrodinger, LLC
*/

"use strict";

const FakeObjectDataListStore = require('./helpers/FakeObjectDataListStore');
const { ImageCell, LinkCell } = require('./helpers/cells');
const { Table, Column, Cell } = require('fixed-data-table-2');
const React = require('react');

class AutoScrollExample extends React.Component {
constructor(props) {
super(props);

this.state = {
dataList: new FakeObjectDataListStore(1000000),
scrollTop: 0,
scrollLeft: 0,
autoScrollEnabled: true,
horizontalScrollDelta: 0,
verticalScrollDelta: 0,
};

this.columns = [];
const cellRenderer = ({ columnKey, rowIndex }) =>
(<div className='autoScrollCell'> {rowIndex}, {columnKey} </div>);

for (let i = 0; i < 100; i++) {
this.columns[i] = (
<Column
key={i}
columnKey={i}
header={<div> {i} </div>}
cell={cellRenderer}
width={100}
allowCellsRecycling={true}
/>
)
}

this.onVerticalScroll = this.onVerticalScroll.bind(this);
this.onHorizontalScroll = this.onHorizontalScroll.bind(this);
this.toggleAutoScroll = this.toggleAutoScroll.bind(this);
this.setHorizontalScrollDelta = this.setHorizontalScrollDelta.bind(this);
this.setVerticalScrollDelta = this.setVerticalScrollDelta.bind(this);
}

componentDidMount() {
setInterval(() => {
if (!this.state.autoScrollEnabled) {
return;
}
this.setState((prevState) => ({
scrollTop: prevState.scrollTop + (prevState.verticalScrollDelta || 0),
scrollLeft: prevState.scrollLeft + (prevState.horizontalScrollDelta || 0),
}));
}, 16);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

60fps

}

render() {
return (
<div className='autoScrollContainer'>
{this.renderControls()}
{this.renderTable()}
</div>
);
}

renderControls() {
return (
<div className='autoScrollControls'>
<label>
Auto Scroll Enabled
<input type='checkbox' checked={this.state.autoScrollEnabled} onChange={this.toggleAutoScroll} />
</label>
<label>
Horizontal Scroll Delta
<input type='number' value={this.state.horizontalScrollDelta} onChange={this.setHorizontalScrollDelta} />
</label>
<label>
Vertical Scroll Delta
<input type='number' value={this.state.verticalScrollDelta} onChange={this.setVerticalScrollDelta} />
</label>
</div>
)
}

renderTable() {
var { dataList, scrollLeft, scrollTop } = this.state;
return (
<Table
rowHeight={50}
headerHeight={50}
rowsCount={dataList.getSize()}
width={1000}
height={500}
scrollLeft={scrollLeft}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scrollLeft and scrollTop specified since we make use of controlled scrolling

scrollTop={scrollTop}
onVerticalScroll={this.onVerticalScroll}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are needed since the user can also scroll the table directly outside of our "autoscroll" logic

onHorizontalScroll={this.onHorizontalScroll}
{...this.props}
>
<Column
columnKey="avatar"
cell={<ImageCell data={dataList} />}
fixed={true}
width={50}
/>
<Column
columnKey="firstName"
header={<Cell>First Name</Cell>}
cell={<LinkCell data={dataList} />}
fixed={true}
width={100}
/>
{this.columns}
</Table>
);
}

onVerticalScroll(scrollTop) {
this.setState({ scrollTop });
}

onHorizontalScroll(scrollLeft) {
this.setState({ scrollLeft });
}

toggleAutoScroll() {
this.setState((prevState) => ({
autoScrollEnabled: !prevState.autoScrollEnabled,
}));
}

setHorizontalScrollDelta(event) {
const { value } = event.target;
if (isNaN(value)) {
return;
}
this.setState({
horizontalScrollDelta: parseInt(value),
});
}

setVerticalScrollDelta(event) {
const { value } = event.target;
if (isNaN(value)) {
return;
}
this.setState({
verticalScrollDelta: parseInt(value),
});
}
}

module.exports = AutoScrollExample;
6 changes: 6 additions & 0 deletions site/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ exports.ExamplePages = {
title: 'Fixed Rows',
description: 'An example using multiple tables to mimic fixed rows.',
},
AUTO_SCROLL_EXAMPLE: {
location: 'example-auto-scroll.html',
fileName: 'AutoScrollExample.js',
title: 'Auto Scroll',
description: 'An example using Controlled Scrolling to mimic auto scrolling',
},
};

Object.keys(exports.ExamplePages).forEach(
Expand Down
1 change: 1 addition & 0 deletions site/examples/ExamplesPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ var EXAMPLE_COMPONENTS = {
[ExamplePages.CONTEXT_EXAMPLE.location]: require('../../examples/ContextExample'),
[ExamplePages.FIXED_RIGHT_COLUMNS_EXAMPLE.location]: require('../../examples/FixedRightColumnsExample'),
[ExamplePages.FIXED_ROWS_EXAMPLE.location]: require('../../examples/FixedRowsExample'),
[ExamplePages.AUTO_SCROLL_EXAMPLE.location]: require('../../examples/AutoScrollExample'),
};

class ExamplesPage extends React.Component {
Expand Down
14 changes: 14 additions & 0 deletions site/examples/examplesPageStyle.less
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,17 @@
height: 50px;
width: 50px;
}

.autoScrollContainer {
.autoScrollControls {
display: flex;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just space the controls evenly (both between and outside)

justify-content: space-around;
align-items: baseline;
}
.autoScrollCell {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

center text both horizontally and vertically

display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
}
147 changes: 62 additions & 85 deletions src/FixedDataTableBufferedRows.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,119 +77,96 @@ class FixedDataTableBufferedRows extends React.Component {
}

render() /*object*/ {
var props = this.props;
var rowClassNameGetter = props.rowClassNameGetter || emptyFunction;
var rowsToRender = this.props.rowsToRender || [];

if (props.isScrolling) {
// We are scrolling, so there's no need to display any rows which lie outside the viewport.
// We still need to render them though, so as to not cause any mounts/unmounts.
this._staticRowArray.forEach((row, i) => {
const rowOutsideViewport = !this.isRowInsideViewport(row.props.index);
if (rowOutsideViewport) {
this._staticRowArray[i] = this.getStubRow(i, false, row.props.index);
}
});
let { offsetTop, scrollTop, isScrolling, rowsToRender } = this.props;
const baseOffsetTop = offsetTop - scrollTop;
rowsToRender = rowsToRender || [];

if (isScrolling) {
// allow static array to grow while scrolling
this._staticRowArray.length = Math.max(this._staticRowArray.length, rowsToRender.length);
} else {
// when scrolling is done, static array can shrink to fit the buffer
this._staticRowArray.length = rowsToRender.length;
}

var baseOffsetTop = props.offsetTop - props.scrollTop;

for (let i = 0; i < rowsToRender.length; i++) {
const rowIndex = rowsToRender[i];

// if the row doesn't exist in the buffer, assign a fake row to it.
// this is so that we can get rid of unnecessary row mounts/unmounts
// render each row from the buffer into the static row array
for (let i = 0; i < this._staticRowArray.length; i++) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored so that there's only a single loop over the rows

let rowIndex = rowsToRender[i];
// if the row doesn't exist in the buffer set, then take the previous one
if (rowIndex === undefined) {
// if a previous row existed, let's just make use of that
if (this._staticRowArray[i] === undefined) {
this._staticRowArray[i] = this.getStubRow(i, true, -1);
}
continue;
rowIndex = this._staticRowArray[i] && this._staticRowArray[i].props.index;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we take the previous rendered row to avoid unmounts

if it is still undefined (aka, fake row), it will be rendered as null

}

const currentRowHeight = this.props.rowSettings.rowHeightGetter(rowIndex);
const currentSubRowHeight = this.props.rowSettings.subRowHeightGetter(rowIndex);
const rowOffsetTop = baseOffsetTop + props.rowOffsets[rowIndex];
const rowKey = props.rowKeyGetter ? props.rowKeyGetter(rowIndex) : i;
const hasBottomBorder = (rowIndex === props.rowSettings.rowsCount - 1) &&
props.showLastRowBorder;
const visible = this.isRowInsideViewport(rowIndex);

this._staticRowArray[i] =
<FixedDataTableRow
key={rowKey}
isScrolling={props.isScrolling}
index={rowIndex}
width={props.width}
height={currentRowHeight}
subRowHeight={currentSubRowHeight}
rowExpanded={props.rowExpanded}
scrollLeft={Math.round(props.scrollLeft)}
offsetTop={Math.round(rowOffsetTop)}
visible={visible}
fixedColumns={props.fixedColumns}
fixedRightColumns={props.fixedRightColumns}
scrollableColumns={props.scrollableColumns}
onClick={props.onRowClick}
onContextMenu={props.onRowContextMenu}
onDoubleClick={props.onRowDoubleClick}
onMouseDown={props.onRowMouseDown}
onMouseUp={props.onRowMouseUp}
onMouseEnter={props.onRowMouseEnter}
onMouseLeave={props.onRowMouseLeave}
onTouchStart={props.onRowTouchStart}
onTouchEnd={props.onRowTouchEnd}
onTouchMove={props.onRowTouchMove}
showScrollbarY={props.showScrollbarY}
className={joinClasses(
rowClassNameGetter(rowIndex),
cx('public/fixedDataTable/bodyRow'),
cx({
'fixedDataTableLayout/hasBottomBorder': hasBottomBorder,
'public/fixedDataTable/hasBottomBorder': hasBottomBorder,
})
)}
/>;
this._staticRowArray[i] = this.renderRow({
rowIndex,
key: i,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of interest, why is this the key not set to rowIndex?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because if the key is given as a function of rowIndex, rows will get mounted/unmounted whenever row indexes change. So we keep the keys constant, form 0 to no:rows present.

FDT uses IntegerBufferSet to manage row index to key mappings, so that when you scroll and it leads to a new row getting visible in the viewport, it'll replace the old one that went outside the viewport.

So basically if we scroll down by 1 row, for an initial viewport of 10 rows, then row 0 at key 0 might gets replaced by row 10, and the key should still be 0 to prevent react mounting/unmounting.

baseOffsetTop,
});
}

return <div>{this._staticRowArray}</div>;
return <div> {this._staticRowArray} </div>;
}


/**
* Returns a stub row which won't be visible to the user.
* This allows us to still render a row and React won't unmount it.
*
* @param {number} rowIndex
* @param {number} key
* @param {boolean} fake
* @param {number} index
* @param {number} baseOffsetTop
* @return {!Object}
*/
getStubRow(key, fake, index) /*object*/ {
renderRow({ rowIndex, key, baseOffsetTop }) /*object*/ {
const props = this.props;
const rowClassNameGetter = props.rowClassNameGetter || emptyFunction;
const fake = rowIndex === undefined;
let rowProps = {};

// if row exists, then calculate row specific props
if (!fake) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was were we had trouble (actual issue)

previously rows outside the viewport were seen as stub/fake, and hence given bad props

rowProps.height = this.props.rowSettings.rowHeightGetter(rowIndex);
rowProps.subRowHeight = this.props.rowSettings.subRowHeightGetter(rowIndex);
rowProps.offsetTop = Math.round(baseOffsetTop + props.rowOffsets[rowIndex]);
rowProps.key = props.rowKeyGetter ? props.rowKeyGetter(rowIndex) : key;

const hasBottomBorder = (rowIndex === props.rowSettings.rowsCount - 1) && props.showLastRowBorder;
rowProps.className = joinClasses(
rowClassNameGetter(rowIndex),
cx('public/fixedDataTable/bodyRow'),
cx({
'fixedDataTableLayout/hasBottomBorder': hasBottomBorder,
'public/fixedDataTable/hasBottomBorder': hasBottomBorder,
})
);
}

const visible = inRange(rowIndex, this.props.firstViewportRowIndex, this.props.endViewportRowIndex);

return (
<FixedDataTableRow
key={key}
index={rowIndex}
isScrolling={props.isScrolling}
index={index}
width={props.width}
height={0}
offsetTop={0}
rowExpanded={props.rowExpanded}
scrollLeft={Math.round(props.scrollLeft)}
visible={false}
fake={fake}
fixedColumns={props.fixedColumns}
fixedRightColumns={props.fixedRightColumns}
scrollableColumns={props.scrollableColumns}
onClick={props.onRowClick}
onContextMenu={props.onRowContextMenu}
onDoubleClick={props.onRowDoubleClick}
onMouseDown={props.onRowMouseDown}
onMouseUp={props.onRowMouseUp}
onMouseEnter={props.onRowMouseEnter}
onMouseLeave={props.onRowMouseLeave}
onTouchStart={props.onRowTouchStart}
onTouchEnd={props.onRowTouchEnd}
onTouchMove={props.onRowTouchMove}
showScrollbarY={props.showScrollbarY}
visible={visible}
fake={fake}
{...rowProps}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apply the row specific props (will be an empty object for fake/stub rows)

/>
);
}

isRowInsideViewport(/*number*/rowIndex) {
return inRange(rowIndex, this.props.firstViewportRowIndex, this.props.endViewportRowIndex);
}
};

module.exports = FixedDataTableBufferedRows;