将 CellMeasurer 与 MultiGrid 结合使用

Using CellMeasurer with MultiGrid

我可能正在尝试做一些不受支持的事情,但我正在尝试将 react-virtualized 的 CellMeasurer 与 MultiGrid 一起使用。我还使用 ScrollSync 来检测用户何时一直向右滚动,并使用 show/hide 指示器。

一个警告是我有一个选项卡控件可以操作哪些数据(行和列)。当数据发生变化时,我在我的 redux 存储中设置了一个标志,我正在使用它来重新测量我的单元格。

它的工作非常接近我的预期。我第一次转到新选项卡时,所有单元格都被正确测量,但有两个例外。

1) 第一列(1 个固定列)重新测量,但左上角和左下角网格的宽度没有更新。这会在新测量值和默认尺寸之间留下差距。一旦我滚动,它就会自行修复 - 很确定,因为我有 ScrollSync。

滚动前 滚动后

2) 列索引 1 永远不会小于默认宽度。这是第一个 non-fixed 列。 适用于更大的内容:

然后,主要问题是当我 return 到之前已经显示的选项卡时。发生这种情况时,即使我的新数据标志仍在触发重新测量,但前一个选项卡中存在的列的测量值仍然存在。我想我需要做一些清除缓存的事情,但到目前为止我的尝试导致所有列都变为默认宽度。是否有特定的 CellMeasurerCache.clearAllMultiGrid.measureAllCellsMultiGrid.recomputeGridSize 顺序适合我?

渲染

  render() {
    const { tableName, ui } = this.props;
    const dataSet = this.getFinalData();
    console.log('rendering');

    return (
      <ScrollSync>
        {({
          // clientHeight,
          // scrollHeight,
          // scrollTop,
          clientWidth, // width of the grid
          scrollWidth, // width of the entire page
          scrollLeft, // how far the user has scrolled
          onScroll,
        }) => {
          // if we have new daya, default yo scrolled left
          const newData = Ui.getTableNewData(ui, tableName);

          const scrolledAllRight = !newData &&
              (scrollLeft + clientWidth >= scrollWidth);
          const scrolledAllLeft = newData || scrollLeft === 0;

          return (
            <AutoSizer>
              {({ width, height }) => {
                const boxShadow = scrolledAllLeft ? false :
                    '1px -3px 3px #a2a2a2';
                return (
                  <div className="grid-container">
                    <MultiGrid
                      cellRenderer={this.cellRenderer}
                      columnWidth={this.getColumnWidth}
                      columnCount={this.getColumnCount()}
                      fixedColumnCount={1}
                      height={height}
                      rowHeight={this.getRowHeight}
                      rowCount={dataSet.length}
                      fixedRowCount={1}
                      deferredMeasurementCache={this.cellSizeCache}
                      noRowsRenderer={DataGrid.emptyRenderer}
                      width={width}
                      className={classNames('data-grid', {
                        'scrolled-left': scrolledAllLeft,
                      })}
                      onScroll={onScroll}
                      styleBottomLeftGrid={{ boxShadow }}
                      ref={(grid) => {
                        this.mainGrid = grid;
                      }}
                    />
                    <div
                      className={classNames('scroll-x-indicator', {
                        faded: scrolledAllRight,
                      })}
                    >
                      <i className="fa fa-fw fa-angle-double-right" />
                    </div>
                  </div>
                );
              }}
            </AutoSizer>
          );
        }}
      </ScrollSync>
    );
  }

单元格渲染器

  cellRenderer({ columnIndex, rowIndex, style, parent }) {
    const data = this.getFinalData(rowIndex);
    const column = this.getColumn(columnIndex);

    return (
      <CellMeasurer
        cache={this.cellSizeCache}
        columnIndex={columnIndex}
        key={`${columnIndex},${rowIndex}`}
        parent={parent}
        rowIndex={rowIndex}
        ref={(cellMeasurer) => {
          this.cellMeasurer = cellMeasurer;
        }}
      >
        <div
          style={{
            ...style,
            maxWidth: 500,
          }}
          className={classNames({
            'grid-header-cell': rowIndex === 0,
            'grid-cell': rowIndex > 0,
            'grid-row-even': rowIndex % 2 === 0,
            'first-col': columnIndex === 0,
            'last-col': columnIndex === this.getColumnCount(),
          })}
        >
          <div className="grid-cell-data">
            {data[column.key]}
          </div>
        </div>
      </CellMeasurer>
    );
  }

组件生命周期

  constructor() {
    super();

    this.cellSizeCache = new CellMeasurerCache({
      defaultWidth: 300,
    });

    // used to update the sizing on command
    this.cellMeasurer = null;
    this.mainGrid = null;

    // this binding for event methods
    this.sort = this.sort.bind(this);
    this.cellRenderer = this.cellRenderer.bind(this);
    this.getColumnWidth = this.getColumnWidth.bind(this);
    this.getRowHeight = this.getRowHeight.bind(this);
  }

  componentDidMount() {
    this.componentDidUpdate();

    setTimeout(() => {
      this.mainGrid.recomputeGridSize();
      setTimeout(() => {
        this.mainGrid.measureAllCells();
      }, 1);
    }, 1);
  }

  componentDidUpdate() {
    const { tableName, ui } = this.props;

    // if we did have new data, it is now complete
    if (Ui.getTableNewData(ui, tableName)) {
      console.log('clearing');
      setTimeout(() => {
        this.mainGrid.measureAllCells();
        setTimeout(() => {
          this.mainGrid.recomputeGridSize();
        }, 1);
      }, 1);
      this.props.setTableNewData(tableName, false);
    }
  }

编辑 Here is a plunker。这个例子展示了我所解释的大部分内容。它还为行提供了比预期更高的高度(无法说出与我的其他实现有什么不同)

第一个建议:不要使用 ScrollSync。直接用MultiGridonScroll属性就可以了。我认为 ScrollSync 对于这种情况有点矫枉过正。

第二个建议:如果可能,避免使用 CellMeasurer 测量宽度 高度,因为这将需要贪婪地测量整个 Grid计算每列+行中的最大单元格。在您的 Plnkr 中记录了一个关于此的开发警告,但它被其他日志记录所掩盖:

CellMeasurerCache should only measure a cell's width or height. You have configured CellMeasurerCache to measure both. This will result in poor performance.

不幸的是,为了解决您问题的实质 - 我相信您已经发现了 CellMeasurerMultiGrid 之间交互的一些缺陷。

编辑 这些缺陷已在 9.2.3 版本中得到解决。请升级。 :)

您可以看到 CellMeasurer + MultiGrid here and the source code can be seen here 的演示。

也许这个例子对你有帮助。

https://commercetools.github.io/advanced-data-tables-demo/?selectedKind=Table&selectedStory=Full%20Example&full=0&down=1&left=1&panelRight=0&downPanel=kadirahq%2Fstorybook-addon-actions%2Factions-panel

你可以看看下面的代码。我使用 CellMeasurer 来设置单元格大小。列宽和行高都将在运行时测量。

import classnames from 'classnames';
import React, {Component} from 'react';
import {AutoSizer, CellMeasurer, CellMeasurerCache, MultiGrid} from 'react-virtualized';
import './Spreadsheet.css';

const LETTERS = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ';

export default class MySlide extends Component {
    constructor(props, context) {
        super(props, context);

        this.state = {
            cellValues: {},
            focusedColumnIndex: null,
            focusedRowIndex: null
        };

        this._cache = new CellMeasurerCache({
            defaultHeight: 30,
            defaultWidth: 150
        });

        this._cellRenderer = this._cellRenderer.bind(this);
        this._setRef = this._setRef.bind(this);
    }

    getRandomInt(min, max) {
        min = Math.ceil(min);
        max = Math.floor(max);
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }

    componentWillUpdate(nextProps, nextState) {
        const {cellValues, focusedColumnIndex, focusedRowIndex} = this.state;

        if (
            focusedColumnIndex !== nextState.focusedColumnIndex ||
            focusedRowIndex !== nextState.focusedRowIndex
        ) {
            this._multiGrid.forceUpdate();
        } else if (cellValues !== nextState.cellValues) {
            this._multiGrid.forceUpdate();
        }
    }


    render() {
        return (
            <AutoSizer disableHeight>
                {({width}) => (
                    <MultiGrid
                        cellRenderer={this._cellRenderer}
                        columnCount={LETTERS.length}
                        fixedColumnCount={1}
                        fixedRowCount={1}
                        height={600}
                        columnWidth={this._cache.columnWidth}
                        rowHeight={this._cache.rowHeight}
                        deferredMeasurementCache={this._cache}
                        overscanColumnCount={0}
                        overscanRowCount={0}
                        ref={this._setRef}
                        rowCount={100}
                        style={{
                            border: '1px solid #dadada',
                            whiteSpace: 'pre',
                            overflowX: 'hidden',
                            textOverflow: 'ellipsis'
                        }}
                        styleBottomLeftGrid={{
                            backgroundColor: '#ffffff'
                        }}
                        styleTopLeftGrid={{
                            backgroundColor: '#f3f3f3',
                            borderBottom: '4px solid #bcbcbc',
                            borderRight: '4px solid #bcbcbc'
                        }}
                        styleTopRightGrid={{
                            backgroundColor: '#f3f3f3'
                        }}
                        width={width}
                    />


                )}
            </AutoSizer>
        );
    }

    _cellRenderer({columnIndex, key, parent, rowIndex, style}) {
        if (columnIndex === 0 && rowIndex === 0) {
            return <div key={key} style={style}/>
        } else if (columnIndex === 0) {
            return this._cellRendererLeft({columnIndex, key, parent, rowIndex, style})
        } else if (rowIndex === 0) {
            return this._cellRendererTop({columnIndex, key, parent, rowIndex, style})
        } else {
            return this._cellRendererMain({columnIndex, key, parent, rowIndex, style})
        }
    }

    _cellRendererLeft = ({columnIndex, key, parent, rowIndex, style}) => {
        const {focusedRowIndex} = this.state;

        return (
            <CellMeasurer
                cache={this._cache}
                columnIndex={columnIndex}
                key={key}
                parent={parent}
                rowIndex={rowIndex}>
                <div
                    className={classnames('FixedGridCell', {
                        FixedGridCellFocused: rowIndex === focusedRowIndex
                    })}
                    key={key}
                    style={{
                        ...style,
                        whiteSpace: 'nowrap',
                        padding: '16px'
                    }}
                >
                    {rowIndex}
                </div>
            </CellMeasurer>
        );
    }

    _cellRendererMain = ({columnIndex, key, parent, rowIndex, style}) => {
        const {cellValues, focusedColumnIndex, focusedRowIndex} = this.state;

        const value = cellValues[key] || '';


        const isFocused = (
            columnIndex === focusedColumnIndex &&
            rowIndex === focusedRowIndex
        );

        return (
            <CellMeasurer
                cache={this._cache}
                columnIndex={columnIndex}
                key={key}
                parent={parent}
                rowIndex={rowIndex}>
                <div
                    key={key}
                    style={{
                        ...style,
                        whiteSpace: 'nowrap',
                        padding: '16px'
                    }}
                    className={classnames('MainGridCell', {
                        MainGridCellFocused: isFocused,
                    })}
                    /*onFocus={() => this.setState({
                        focusedColumnIndex: columnIndex,
                        focusedRowIndex: rowIndex
                    })}
                    onChange={(event) => {
                        this.setState({
                            cellValues: {
                                ...cellValues,
                                [key]: event.target.value
                            }
                        })
                    }}*/>{rowIndex + ',' + columnIndex}
                    {columnIndex % 3 === 0 && ' This is a long sentence'}<br/>
                    {rowIndex % 4 === 0 && <br/>}
                    {rowIndex % 4 === 0 && 'This is a another line'}
                    {rowIndex % 6 === 0 && <br/>}
                    {rowIndex % 6 === 0 && 'This is a long sentence'}
                </div>
            </CellMeasurer>
        );
    }

    _cellRendererTop = ({columnIndex, key, parent, rowIndex, style}) => {
        const {focusedColumnIndex} = this.state;

        return (
            <CellMeasurer
                cache={this._cache}
                columnIndex={columnIndex}
                key={key}
                parent={parent}
                rowIndex={rowIndex}>
                <div
                    className={classnames('FixedGridCell', {
                        FixedGridCellFocused: columnIndex === focusedColumnIndex
                    })}
                    key={key}
                    style={{
                        ...style,
                        whiteSpace: 'nowrap',
                    }}
                >
                    {LETTERS[columnIndex]}
                </div>
            </CellMeasurer>
        );
    }

    _setRef(ref) {
        this._multiGrid = ref;
    }
}

Spreadsheet.css

    .GridContainer {
        height: 300px;
        position: relative;
        border: 1px solid #dadada;
        overflow: hidden;
    }
    
    .TopLeftCell {
        height: 40px;
        width: 50px;
        background-color: #f3f3f3;
        border-bottom: 4px solid #bcbcbc;
        border-right: 4px solid #bcbcbc;
    }
    
    .MainGrid {
        position: absolute !important;
        left: 50px;
        top: 40px;
    }
    
    .MainGridCell {
        display: flex;
        flex-direction: row;
        align-items: center;
        justify-content: flex-start;
        padding: 0.25rem;
        outline: 0;
        border: none;
        border-right: 1px solid #dadada;
        border-bottom: 1px solid #dadada;
        background-color: #fff;
        font-size: 1rem;
    }
    
    .MainGridCellFocused {
        box-shadow: 0 0 0 2px #4285FA inset;
    }
    
    .LeftGrid {
        position: absolute !important;
        left: 0;
        top: 40px;
        overflow: hidden !important;
    }
    
    .TopGrid {
        position: absolute !important;
        left: 50px;
        top: 0;
        height: 40px;
        overflow: hidden !important;
    }
    
    .FixedGridCell {
        display: flex;
        flex-direction: row;
        align-items: center;
        justify-content: center;
        background-color: #f3f3f3;
        border-right: 1px solid #ccc;
        border-bottom: 1px solid #ccc;
    }
    
    .FixedGridCellFocused {
        background-color: #dddddd;
    }