perf(mail-merge): Add lazy rendering to table

Summary: Add new LazyRenderedList component and updates Table to use it

Test Plan: TODO

Reviewers: bengotow, evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D2936
This commit is contained in:
Juan Tejada 2016-04-29 13:08:35 -07:00
parent e9ae4def9a
commit 38bf9080c2
5 changed files with 116 additions and 40 deletions

View file

@ -0,0 +1,83 @@
import React, {Component, PropTypes} from 'react'
import {findDOMNode} from 'react-dom'
const MIN_RANGE_SIZE = 2
function getRange({total, itemHeight, containerHeight, scrollTop}) {
const itemsPerBody = Math.floor((containerHeight) / itemHeight);
const start = Math.max(0, Math.floor(scrollTop / itemHeight) - (itemsPerBody * 2));
const end = Math.max(MIN_RANGE_SIZE, Math.min(start + (4 * itemsPerBody), total));
return {start, end}
}
class LazyRenderedList extends Component {
static propTypes = {
items: PropTypes.array,
itemHeight: PropTypes.number,
containerHeight: PropTypes.number,
BufferTag: PropTypes.string,
ItemRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
RootRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
}
static defaultProps = {
itemHeight: 30,
containerHeight: 150,
BufferTag: 'div',
}
constructor(props) {
super(props)
this.state = {start: 0, end: MIN_RANGE_SIZE}
}
componentWillReceiveProps(nextProps) {
this.updateRangeState(nextProps)
}
onScroll() {
this.updateRangeState(this.props)
}
updateRangeState({itemHeight, items, containerHeight}) {
const {scrollTop} = findDOMNode(this)
this.setState(getRange({total: items.length, itemHeight, containerHeight, scrollTop}))
}
renderItems() {
const {items, itemHeight, BufferTag, ItemRenderer} = this.props
const {start, end} = this.state
const topHeight = start * itemHeight
const bottomHeight = (items.length - end) * itemHeight
const top = <BufferTag key="lazy-top" style={{height: topHeight}} />
const bottom = <BufferTag key="lazy-bottom" style={{height: bottomHeight}} />
const elements = items.slice(start, end).map((item, idx) => (
<ItemRenderer
key={`item-${start + idx}`}
item={item}
idx={start + idx}
/>
))
elements.unshift(top)
elements.push(bottom)
return elements
}
render() {
const {RootRenderer, containerHeight} = this.props
return (
<RootRenderer
style={{height: containerHeight, overflowX: 'hidden', overflowY: 'auto'}}
onScroll={::this.onScroll}
>
{this.renderItems()}
</RootRenderer>
)
}
}
export default LazyRenderedList

View file

@ -13,8 +13,8 @@ export class SelectableCell extends Component {
static propTypes = {
className: PropTypes.string,
tableData: Table.propTypes.tableData,
rowIdx: TableCell.propTypes.rowIdx,
colIdx: TableCell.propTypes.colIdx,
rowIdx: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
colIdx: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
selection: PropTypes.object,
onSetSelection: PropTypes.func.isRequired,
}

View file

@ -1,47 +1,28 @@
import _ from 'underscore'
import classnames from 'classnames'
import React, {Component, PropTypes} from 'react'
import LazyRenderedList from './lazy-rendered-list'
// TODO Ugh gross. Use flow
const RowDataType = PropTypes.arrayOf(PropTypes.node)
const RendererType = PropTypes.oneOfType([PropTypes.func, PropTypes.string])
const IndexType = PropTypes.oneOfType([PropTypes.number, PropTypes.string])
const TablePropTypes = {
idx: IndexType,
renderer: RendererType,
tableData: PropTypes.shape({
rows: PropTypes.arrayOf(RowDataType),
rows: PropTypes.array,
}),
}
export class TableCell extends Component {
static propTypes = {
className: PropTypes.string,
isHeader: PropTypes.bool,
tableData: TablePropTypes.tableData.isRequired,
rowIdx: TablePropTypes.idx.isRequired,
colIdx: TablePropTypes.idx.isRequired,
}
static defaultProps = {
className: '',
}
render() {
const {className, isHeader, children, ...props} = this.props
const CellTag = isHeader ? 'th' : 'td'
return (
<CellTag {...props} className={`table-cell ${className}`} >
{children}
</CellTag>
)
}
export function TableCell({className = '', isHeader, children, ...props}) {
const CellTag = isHeader ? 'th' : 'td'
return (
<CellTag {...props} className={`table-cell ${className}`} >
{children}
</CellTag>
)
}
export class TableRow extends Component {
static propTypes = {
@ -102,6 +83,8 @@ export default class Table extends Component {
className: PropTypes.string,
displayHeader: PropTypes.bool,
displayNumbers: PropTypes.bool,
rowHeight: PropTypes.number,
bodyHeight: PropTypes.number,
tableData: TablePropTypes.tableData.isRequired,
extraProps: PropTypes.object,
RowRenderer: TablePropTypes.renderer,
@ -116,10 +99,10 @@ export default class Table extends Component {
}
renderBody() {
const {tableData, displayNumbers, displayHeader, extraProps, RowRenderer, CellRenderer} = this.props
const {tableData, rowHeight, bodyHeight, displayNumbers, displayHeader, extraProps, RowRenderer, CellRenderer} = this.props
const rows = displayHeader ? tableData.rows.slice(1) : tableData.rows
const rowElements = rows.map((row, idx) => {
const itemRenderer = ({idx}) => {
const rowIdx = displayHeader ? idx + 1 : idx;
return (
<RowRenderer
@ -132,12 +115,17 @@ export default class Table extends Component {
{...extraProps}
/>
)
})
}
return (
<tbody>
{rowElements}
</tbody>
<LazyRenderedList
items={rows}
itemHeight={rowHeight}
containerHeight={bodyHeight}
BufferTag="tr"
ItemRenderer={itemRenderer}
RootRenderer="tbody"
/>
)
}
@ -161,10 +149,10 @@ export default class Table extends Component {
}
render() {
const {className} = this.props
const {className, ...otherProps} = this.props
return (
<div className={`nylas-table ${className}`}>
<div className={`nylas-table ${className}`} {...otherProps}>
<table>
{this.renderHeader()}
{this.renderBody()}

@ -1 +1 @@
Subproject commit 91b58e912a7f2d9d68632eb2b434ba185af352ba
Subproject commit a795839311c962f5e84fe29b753e91a1949f6999

View file

@ -7,7 +7,12 @@
.nylas-table {
height: 100%;
width: 100%;
overflow: scroll;
overflow-y: hidden;
overflow-x: scroll;
thead, tbody {
display: block;
}
.table-row {