react-virtualized
Version:
React components for efficiently rendering large, scrollable lists and tabular data
450 lines (386 loc) • 14 kB
JavaScript
import cn from 'classnames';
import FlexColumn from './FlexColumn';
import React, { Component, PropTypes } from 'react';
import { findDOMNode } from 'react-dom';
import shallowCompare from 'react-addons-shallow-compare';
import Grid from '../Grid';
import SortDirection from './SortDirection';
/**
* Table component with fixed headers and virtualized rows for improved performance with large data sets.
* This component expects explicit width, height, and padding parameters.
*/
var FlexTable = function (_Component) {
babelHelpers.inherits(FlexTable, _Component);
function FlexTable(props) {
babelHelpers.classCallCheck(this, FlexTable);
var _this = babelHelpers.possibleConstructorReturn(this, Object.getPrototypeOf(FlexTable).call(this, props));
_this.state = {
scrollbarWidth: 0
};
_this._createRow = _this._createRow.bind(_this);
return _this;
}
/**
* See Grid#recomputeGridSize
*/
babelHelpers.createClass(FlexTable, [{
key: 'recomputeRowHeights',
value: function recomputeRowHeights() {
this.refs.Grid.recomputeGridSize();
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
this._setScrollbarWidth();
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate() {
this._setScrollbarWidth();
}
}, {
key: 'render',
value: function render() {
var _this2 = this;
var _props = this.props;
var className = _props.className;
var disableHeader = _props.disableHeader;
var headerHeight = _props.headerHeight;
var height = _props.height;
var noRowsRenderer = _props.noRowsRenderer;
var onRowsRendered = _props.onRowsRendered;
var _onScroll = _props.onScroll;
var overscanRowsCount = _props.overscanRowsCount;
var rowClassName = _props.rowClassName;
var rowHeight = _props.rowHeight;
var rowsCount = _props.rowsCount;
var scrollToIndex = _props.scrollToIndex;
var scrollTop = _props.scrollTop;
var width = _props.width;
var scrollbarWidth = this.state.scrollbarWidth;
var availableRowsHeight = height - headerHeight;
// This row-renderer wrapper function is necessary in order to trigger re-render when the
// sort-by or sort-direction have changed (else Grid will not see any props changes)
var rowRenderer = function rowRenderer(index) {
return _this2._createRow(index);
};
var rowClass = rowClassName instanceof Function ? rowClassName(-1) : rowClassName;
return React.createElement(
'div',
{
className: cn('FlexTable', className)
},
!disableHeader && React.createElement(
'div',
{
className: cn('FlexTable__headerRow', rowClass),
style: {
height: headerHeight,
paddingRight: scrollbarWidth,
width: width
}
},
this._getRenderedHeaderRow()
),
React.createElement(Grid, {
ref: 'Grid',
className: 'FlexTable__Grid',
columnWidth: width,
columnsCount: 1,
height: availableRowsHeight,
noContentRenderer: noRowsRenderer,
onScroll: function onScroll(_ref) {
var clientHeight = _ref.clientHeight;
var scrollHeight = _ref.scrollHeight;
var scrollTop = _ref.scrollTop;
return _onScroll({ clientHeight: clientHeight, scrollHeight: scrollHeight, scrollTop: scrollTop });
},
onSectionRendered: function onSectionRendered(_ref2) {
var rowOverscanStartIndex = _ref2.rowOverscanStartIndex;
var rowOverscanStopIndex = _ref2.rowOverscanStopIndex;
var rowStartIndex = _ref2.rowStartIndex;
var rowStopIndex = _ref2.rowStopIndex;
return onRowsRendered({
overscanStartIndex: rowOverscanStartIndex,
overscanStopIndex: rowOverscanStopIndex,
startIndex: rowStartIndex,
stopIndex: rowStopIndex
});
},
overscanRowsCount: overscanRowsCount,
renderCell: function renderCell(_ref3) {
var columnIndex = _ref3.columnIndex;
var rowIndex = _ref3.rowIndex;
return rowRenderer(rowIndex);
},
rowHeight: rowHeight,
rowsCount: rowsCount,
scrollToRow: scrollToIndex,
scrollTop: scrollTop,
width: width
})
);
}
}, {
key: 'shouldComponentUpdate',
value: function shouldComponentUpdate(nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}
}, {
key: '_createColumn',
value: function _createColumn(column, columnIndex, rowData, rowIndex) {
var _column$props = column.props;
var cellClassName = _column$props.cellClassName;
var cellDataGetter = _column$props.cellDataGetter;
var columnData = _column$props.columnData;
var dataKey = _column$props.dataKey;
var cellRenderer = _column$props.cellRenderer;
var cellData = cellDataGetter(dataKey, rowData, columnData);
var renderedCell = cellRenderer(cellData, dataKey, rowData, rowIndex, columnData);
var style = this._getFlexStyleForColumn(column);
var title = typeof renderedCell === 'string' ? renderedCell : null;
return React.createElement(
'div',
{
key: 'Row' + rowIndex + '-Col' + columnIndex,
className: cn('FlexTable__rowColumn', cellClassName),
style: style
},
React.createElement(
'div',
{
className: 'FlexTable__truncatedColumnText',
title: title
},
renderedCell
)
);
}
}, {
key: '_createHeader',
value: function _createHeader(column, columnIndex) {
var _props2 = this.props;
var headerClassName = _props2.headerClassName;
var onHeaderClick = _props2.onHeaderClick;
var sort = _props2.sort;
var sortBy = _props2.sortBy;
var sortDirection = _props2.sortDirection;
var _column$props2 = column.props;
var dataKey = _column$props2.dataKey;
var disableSort = _column$props2.disableSort;
var headerRenderer = _column$props2.headerRenderer;
var label = _column$props2.label;
var columnData = _column$props2.columnData;
var sortEnabled = !disableSort && sort;
var classNames = cn('FlexTable__headerColumn', headerClassName, column.props.headerClassName, {
'FlexTable__sortableHeaderColumn': sortEnabled
});
var style = this._getFlexStyleForColumn(column);
// If this is a sortable header, clicking it should update the table data's sorting.
var newSortDirection = sortBy !== dataKey || sortDirection === SortDirection.DESC ? SortDirection.ASC : SortDirection.DESC;
var onClick = function onClick() {
sortEnabled && sort(dataKey, newSortDirection);
onHeaderClick(dataKey, columnData);
};
var renderedHeader = headerRenderer({
columnData: columnData,
dataKey: dataKey,
disableSort: disableSort,
label: label,
sortBy: sortBy,
sortDirection: sortDirection
});
return React.createElement(
'div',
{
key: 'Header-Col' + columnIndex,
className: classNames,
style: style,
onClick: onClick
},
renderedHeader
);
}
}, {
key: '_createRow',
value: function _createRow(rowIndex) {
var _this3 = this;
var _props3 = this.props;
var children = _props3.children;
var onRowClick = _props3.onRowClick;
var rowClassName = _props3.rowClassName;
var rowGetter = _props3.rowGetter;
var scrollbarWidth = this.state.scrollbarWidth;
var rowClass = rowClassName instanceof Function ? rowClassName(rowIndex) : rowClassName;
var renderedRow = React.Children.map(children, function (column, columnIndex) {
return _this3._createColumn(column, columnIndex, rowGetter(rowIndex), rowIndex);
});
return React.createElement(
'div',
{
key: rowIndex,
className: cn('FlexTable__row', rowClass),
onClick: function onClick() {
return onRowClick(rowIndex);
},
style: {
height: this._getRowHeight(rowIndex),
paddingRight: scrollbarWidth
}
},
renderedRow
);
}
/**
* Determines the flex-shrink, flex-grow, and width values for a cell (header or column).
*/
}, {
key: '_getFlexStyleForColumn',
value: function _getFlexStyleForColumn(column) {
var flexValue = column.props.flexGrow + ' ' + column.props.flexShrink + ' ' + column.props.width + 'px';
var style = {
flex: flexValue,
msFlex: flexValue,
WebkitFlex: flexValue
};
if (column.props.maxWidth) {
style.maxWidth = column.props.maxWidth;
}
if (column.props.minWidth) {
style.minWidth = column.props.minWidth;
}
return style;
}
}, {
key: '_getRenderedHeaderRow',
value: function _getRenderedHeaderRow() {
var _this4 = this;
var _props4 = this.props;
var children = _props4.children;
var disableHeader = _props4.disableHeader;
var items = disableHeader ? [] : children;
return React.Children.map(items, function (column, index) {
return _this4._createHeader(column, index);
});
}
}, {
key: '_getRowHeight',
value: function _getRowHeight(rowIndex) {
var rowHeight = this.props.rowHeight;
return rowHeight instanceof Function ? rowHeight(rowIndex) : rowHeight;
}
}, {
key: '_setScrollbarWidth',
value: function _setScrollbarWidth() {
var Grid = findDOMNode(this.refs.Grid);
var clientWidth = Grid.clientWidth || 0;
var offsetWidth = Grid.offsetWidth || 0;
var scrollbarWidth = offsetWidth - clientWidth;
this.setState({ scrollbarWidth: scrollbarWidth });
}
}]);
return FlexTable;
}(Component);
FlexTable.propTypes = {
/** One or more FlexColumns describing the data displayed in this row */
children: function children(props, propName, componentName) {
var children = React.Children.toArray(props.children);
for (var i = 0; i < children.length; i++) {
if (children[i].type !== FlexColumn) {
return new Error('FlexTable only accepts children of type FlexColumn');
}
}
},
/** Optional CSS class name */
className: PropTypes.string,
/** Disable rendering the header at all */
disableHeader: PropTypes.bool,
/** Optional CSS class to apply to all column headers */
headerClassName: PropTypes.string,
/** Fixed height of header row */
headerHeight: PropTypes.number.isRequired,
/** Fixed/available height for out DOM element */
height: PropTypes.number.isRequired,
/** Optional renderer to be used in place of table body rows when rowsCount is 0 */
noRowsRenderer: PropTypes.func,
/**
* Optional callback when a column's header is clicked.
* (dataKey: string): void
*/
onHeaderClick: PropTypes.func,
/**
* Callback invoked when a user clicks on a table row.
* (rowIndex: number): void
*/
onRowClick: PropTypes.func,
/**
* Callback invoked with information about the slice of rows that were just rendered.
* ({ startIndex, stopIndex }): void
*/
onRowsRendered: PropTypes.func,
/**
* Callback invoked whenever the scroll offset changes within the inner scrollable region.
* This callback can be used to sync scrolling between lists, tables, or grids.
* ({ clientHeight, scrollHeight, scrollTop }): void
*/
onScroll: PropTypes.func.isRequired,
/**
* Number of rows to render above/below the visible bounds of the list.
* These rows can help for smoother scrolling on touch devices.
*/
overscanRowsCount: PropTypes.number.isRequired,
/**
* Optional CSS class to apply to all table rows (including the header row).
* This property can be a CSS class name (string) or a function that returns a class name.
* If a function is provided its signature should be: (rowIndex: number): string
*/
rowClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
/**
* Callback responsible for returning a data row given an index.
* (index: number): any
*/
rowGetter: PropTypes.func.isRequired,
/**
* Either a fixed row height (number) or a function that returns the height of a row given its index.
* (index: number): number
*/
rowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]).isRequired,
/** Number of rows in table. */
rowsCount: PropTypes.number.isRequired,
/** Row index to ensure visible (by forcefully scrolling if necessary) */
scrollToIndex: PropTypes.number,
/** Vertical offset. */
scrollTop: PropTypes.number,
/**
* Sort function to be called if a sortable header is clicked.
* (dataKey: string, sortDirection: SortDirection): void
*/
sort: PropTypes.func,
/** FlexTable data is currently sorted by this :dataKey (if it is sorted at all) */
sortBy: PropTypes.string,
/** FlexTable data is currently sorted in this direction (if it is sorted at all) */
sortDirection: PropTypes.oneOf([SortDirection.ASC, SortDirection.DESC]),
/** Width of list */
width: PropTypes.number.isRequired
};
FlexTable.defaultProps = {
disableHeader: false,
headerHeight: 0,
noRowsRenderer: function noRowsRenderer() {
return null;
},
onHeaderClick: function onHeaderClick() {
return null;
},
onRowClick: function onRowClick() {
return null;
},
onRowsRendered: function onRowsRendered() {
return null;
},
onScroll: function onScroll() {
return null;
},
overscanRowsCount: 10
};
export default FlexTable;