0

I may be trying to do something that isn't supported, but I am trying to use react-virtualized's CellMeasurer with MultiGrid. I also am using a ScrollSync to detect when the user has scrolled all the way to the right and show/hide an indicator.

One caveat is that I have a tab control that manipulates what data (both rows and columns). I have a flag set in my redux store when the data has changed, and am using that to remeasure my cells.

It is working pretty closely to what I would expect. The first time I go to a new tab, the cells are all measured correctly with two exceptions.

1) The first column (1 fixed column) remeasures, but the width of the top left, and bottom left grids does not update. This leaves a gap between the new measurement and the default size. Once I scroll, this fixes itself - pretty sure because I have the ScrollSync.

Before Scroll enter image description here After Scroll enter image description here

2) Column index 1 never gets smaller than the default width. This is the first non-fixed column. enter image description here enter image description here Works with larger Content: enter image description here

Then, the main issue is when I return to tabs that have already been shown before. When this happens, the measurements from columns that existed in the previous tab carry over even though my flag for new data is still triggering a remeasure. I think I need to do something with clearing the cache, but my attempts so far have resulted in all columns going to the default width. Is there a certain sequence of CellMeasurerCache.clearAll, MultiGrid.measureAllCells and MultiGrid.recomputeGridSize that will work properly for me here?

Render

  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>
    );
  }

Cell Renderer

  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>
    );
  }

Component Lifecycle

  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);
    }
  }

EDIT Here is a plunker. This example shows most of what I was explaining. It is also is giving more height to the rows than expected (can't tell what is different than my other implementation)

Troy Cosentino
  • 4,658
  • 9
  • 37
  • 59
  • I have the same questions, any other solutions? – unlimited Mar 09 '17 at 15:05
  • 1
    I've made some good progress on updating `CellMeasurer` to play nicer with `MultiGrid` out of the box. At React Conf this week so my availability is a bit limited but I hope to release a bugfix in the next few days. – bvaughn Mar 13 '17 at 22:05

3 Answers3

2

First suggestion: Don't use ScrollSync. Just use the onScroll property of MultiGrid directly. I believe ScrollSync is overkill for this case.

Second suggestion: If possible, avoid measuring both width and height with CellMeasurer as that will require the entire Grid to be measured greedily in order to calculate the max cells in each column+row. There's a dev warning being logged in your Plnkr about this but it's buried by the other logging:

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

Unfortunately, to address the meat of your question- I believe you've uncovered a couple of flaws with the interaction between CellMeasurer and MultiGrid.

Edit These flaws have been addressed with the 9.2.3 release. Please upgrade. :)

You can see a demo of CellMeasurer + MultiGrid here and the source code can be seen here.

bvaughn
  • 13,300
  • 45
  • 46
  • So clear the cache before calling `MultiGrid.recomputeGridSize` then? And the grid would need to be rendered with the new data first, so doing this in `componentDidUpdate` makes sense? Yeah definitely, I'll get a plnkr together later today with the code.. just have to untangle some things first. Appreciate the help. – Troy Cosentino Mar 06 '17 at 19:37
  • Yes to both of those questions. – bvaughn Mar 06 '17 at 23:42
  • Updated with a Plnkr – Troy Cosentino Mar 07 '17 at 03:35
  • Ok that makes sense, thanks for taking the time to look. I think I follow for the most part, based on your second part it sounds like it would need to apply the (a different?) measurer to to the grids phase by phase and probably adjusting to those sizes – Troy Cosentino Mar 08 '17 at 02:20
  • I'm also thinking about a sort of compromise like just measuring the first row to get an estimate width, and then allowing editing from there. I may take a look at how your doing MultiGrid and try to do something custom – Troy Cosentino Mar 08 '17 at 02:28
  • I have a branch locally where I'm playing with this to see if I can't get it to work out of the box. I'm a little swamped right now with work so I don't have as much time to dedicate to this as I'd like until the weekend. I'll keep you posted. – bvaughn Mar 08 '17 at 17:10
  • Happy to say that 9.2.3 release (just published) should hopefully resolve this issue for you. – bvaughn Mar 14 '17 at 21:25
0

You can have a look at the following code. I have used CellMeasurer to make cell size. Both column width and row height will be measured in runtime.

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;
    }
bikram
  • 7,127
  • 2
  • 51
  • 63