Использование CellMeasurer с MultiGrid

Возможно, я пытаюсь сделать что-то, что не поддерживается, но я пытаюсь использовать CellMeasurer реактивной виртуализации с MultiGrid. Я также использую ScrollSync, чтобы определить, когда пользователь полностью прокрутил страницу вправо, и показать/скрыть индикатор.

Одно предостережение заключается в том, что у меня есть элемент управления вкладками, который манипулирует данными (как строками, так и столбцами). У меня установлен флаг в моем хранилище избыточности, когда данные изменились, и я использую его для повторного измерения своих ячеек.

Он работает довольно близко к тому, что я ожидал. Когда я впервые перехожу на новую вкладку, все ячейки измеряются правильно, за двумя исключениями.

1) Первый столбец (1 фиксированный столбец) повторно измеряется, но ширина верхней левой и нижней левой сетки не обновляется. Это оставляет разрыв между новым измерением и размером по умолчанию. Как только я прокручиваю, это исправляется — почти наверняка, потому что у меня есть ScrollSync.

Перед прокруткой введите здесь описание изображения После прокрутки введите здесь описание изображения

2) Индекс столбца 1 никогда не становится меньше ширины по умолчанию. Это первый нефиксированный столбец. введите здесь описание изображения введите здесь описание изображения Работает с большим содержимым: введите здесь описание изображения

Затем основная проблема возникает, когда я возвращаюсь к вкладкам, которые уже были показаны ранее. Когда это происходит, измерения из столбцов, существовавших на предыдущей вкладке, переносятся, хотя мой флажок для новых данных все еще вызывает повторное измерение. Я думаю, что мне нужно что-то сделать с очисткой кеша, но мои попытки до сих пор приводили к тому, что все столбцы переходят к ширине по умолчанию. Есть ли определенная последовательность CellMeasurerCache.clearAll, MultiGrid.measureAllCells и MultiGrid.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);
    }
  }

ИЗМЕНИТЬ Вот плункер. Этот пример показывает большую часть того, что я объяснял. Это также дает большую высоту строк, чем ожидалось (не могу сказать, что отличается от моей другой реализации)


person Troy Cosentino    schedule 06.03.2017    source источник
comment
У меня те же вопросы, есть другие решения?   -  person unlimited    schedule 09.03.2017
comment
Я добился значительного прогресса в обновлении CellMeasurer, чтобы лучше играть с MultiGrid из коробки. На React Conf на этой неделе, поэтому моя доступность немного ограничена, но я надеюсь выпустить исправление в ближайшие несколько дней.   -  person bvaughn    schedule 14.03.2017


Ответы (3)


Первое предложение: не используйте ScrollSync. Просто используйте свойство onScroll для MultiGrid напрямую. Я считаю, что ScrollSync является излишним для этого случая.

Второе предложение: если возможно, избегайте измерения ширины и высоты с помощью CellMeasurer, так как это потребует жадного измерения всего Grid, чтобы вычислить максимальное количество ячеек в каждом столбце + строке. В вашем Plnkr есть предупреждение разработчика об этом, но оно скрыто другим журналом:

CellMeasurerCache должен измерять только ширину или высоту ячейки. Вы настроили CellMeasurerCache для измерения обоих параметров. Это приведет к плохой работе.

К сожалению, чтобы ответить на суть вашего вопроса: я полагаю, вы обнаружили пару недостатков во взаимодействии между CellMeasurer и MultiGrid.

Изменить Эти недостатки были устранены в версии 9.2.3. Пожалуйста, обновите. :)

Вы можете увидеть демонстрацию CellMeasurer + MultiGrid здесь и исходный код можно увидеть .

person bvaughn    schedule 06.03.2017
comment
Итак, очистите кеш перед вызовом MultiGrid.recomputeGridSize? И сетка сначала должна быть отрисована с новыми данными, так что делать это в componentDidUpdate имеет смысл? Да, определенно, сегодня я соберу plnkr вместе с кодом... просто сначала нужно кое-что распутать. Цените помощь. - person Troy Cosentino; 06.03.2017
comment
Да на оба этих вопроса. - person bvaughn; 07.03.2017
comment
Обновлено с помощью Plnkr - person Troy Cosentino; 07.03.2017
comment
Хорошо, это имеет смысл, спасибо, что нашли время посмотреть. Я думаю, что по большей части следую, основываясь на вашей второй части, похоже, что нужно будет применять (другой?) измеритель к сеткам поэтапно и, вероятно, приспосабливаться к этим размерам. - person Troy Cosentino; 08.03.2017
comment
Я также думаю о своего рода компромиссе, например, просто измерить первую строку, чтобы получить приблизительную ширину, а затем разрешить редактирование оттуда. Я могу посмотреть, как вы работаете с MultiGrid, и попробовать сделать что-то нестандартное. - person Troy Cosentino; 08.03.2017
comment
У меня есть локальная ветка, где я играю с этим, чтобы увидеть, не смогу ли я заставить его работать из коробки. Я сейчас немного завален работой, поэтому у меня не так много времени, чтобы посвятить этому, как хотелось бы, до выходных. Я буду держать вас в курсе. - person bvaughn; 08.03.2017
comment
Рад сообщить, что выпуск 9.2.3 (только что опубликованный) должен решить эту проблему для вас. - person bvaughn; 15.03.2017


Вы можете взглянуть на следующий код. Я использовал 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;
    }
}

Таблица.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;
    }
person bikram    schedule 05.03.2021