clauderic / react-sortable-hoc

A set of higher-order components to turn any list into an animated, accessible and touch-friendly sortable list✌️
https://clauderic.github.io/react-sortable-hoc/
MIT License
10.78k stars 978 forks source link

Using this with react-virtualized Grid #295

Open kraenhansen opened 6 years ago

kraenhansen commented 6 years ago

First of all - this looks like an awesome project, thank you for that!

I want to use it with react-virtualized's Grid where the rows are sortable. I cannot use the List as I have multiple columns and I cannot use it with the Table because I have too many columns and therefore need horizontal scrolling.

The issue that I am facing is that the Grid does not have a rowRenderer callback as the Table or List has (it only has cellRenderer and cellRenderer), thus I don't know what to wrap the SortableElement around. Do you have any ideas or have you seen any examples of this usage?

ubbe-xyz commented 6 years ago

Similar to #315 🤔

kraenhansen commented 6 years ago

I'll update this by mentioning that I've got this to work by implementing a cellRangeRenderer callback on the grid https://github.com/bvaughn/react-virtualized/blob/master/docs/Grid.md

I think it would be awesome with an example on this lib that shows that.

Here's my implementation (rowCellRangeRenderer.tsx) which needs to be updated to do better caching of the rows:

    import * as React from 'react';
    import {
      Grid,
      GridCellProps,
      GridCellRangeProps,
      List,
      Table,
    } from 'react-virtualized';

    /**
     * Default implementation of cellRangeRenderer used by Grid.
     * This renderer supports cell-caching while the user is scrolling.
     */

    export interface IGridRowProps {
      children: React.ReactNode[];
      isScrolling: boolean;
      isVisible: boolean;
      key: string;
      parent: Grid | List | Table;
      rowIndex: number;
      style: React.CSSProperties;
    }

    export type GridRowRenderer = (props: IGridRowProps) => JSX.Element;

    export const rowCellRangeRenderer = (rowRenderer: GridRowRenderer) => ({
      cellCache,
      cellRenderer,
      columnSizeAndPositionManager,
      columnStartIndex,
      columnStopIndex,
      deferredMeasurementCache,
      horizontalOffsetAdjustment,
      isScrolling,
      parent, // Grid (or List or Table)
      rowSizeAndPositionManager,
      rowStartIndex,
      rowStopIndex,
      styleCache,
      verticalOffsetAdjustment,
      visibleColumnIndices,
      visibleRowIndices,
    }: GridCellRangeProps) => {
      const renderedRows: React.ReactNode[] = [];

      // Browsers have native size limits for elements (eg Chrome 33M pixels, IE 1.5M pixes).
      // User cannot scroll beyond these size limitations.
      // In order to work around this, ScalingCellSizeAndPositionManager compresses offsets.
      // We should never cache styles for compressed offsets though as this can lead to bugs.
      // See issue #576 for more.
      const areOffsetsAdjusted =
        columnSizeAndPositionManager.areOffsetsAdjusted() ||
        rowSizeAndPositionManager.areOffsetsAdjusted();

      const canCacheStyle = !isScrolling && !areOffsetsAdjusted;

      for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
        const rowDatum = rowSizeAndPositionManager.getSizeAndPositionOfCell(
          rowIndex,
        );

        const renderedCells: React.ReactNode[] = [];
        const rowKey = `${rowIndex}`;

        for (
          let columnIndex = columnStartIndex;
          columnIndex <= columnStopIndex;
          columnIndex++
        ) {
          const columnDatum = columnSizeAndPositionManager.getSizeAndPositionOfCell(
            columnIndex,
          );
          const isCellVisible =
            columnIndex >= visibleColumnIndices.start &&
            columnIndex <= visibleColumnIndices.stop &&
            rowIndex >= visibleRowIndices.start &&
            rowIndex <= visibleRowIndices.stop;
          const cellKey = `${rowIndex}-${columnIndex}`;
          let cellStyle: React.CSSProperties;

          // Cache style objects so shallow-compare doesn't re-render unnecessarily.
          if (canCacheStyle && styleCache[cellKey]) {
            cellStyle = styleCache[cellKey];
          } else {
            // In deferred mode, cells will be initially rendered before we know their size.
            // Don't interfere with CellMeasurer's measurements by setting an invalid size.
            if (
              deferredMeasurementCache &&
              !deferredMeasurementCache.has(rowIndex, columnIndex)
            ) {
              // Position not-yet-measured cells at top/left 0,0,
              // And give them width/height of 'auto' so they can grow larger than the parent Grid if necessary.
              // Positioning them further to the right/bottom influences their measured size.
              cellStyle = {
                height: 'auto',
                left: 0,
                position: 'absolute',
                top: 0,
                width: 'auto',
              };
            } else {
              cellStyle = {
                height: rowDatum.size,
                left: columnDatum.offset + horizontalOffsetAdjustment,
                position: 'absolute',
                top: 0,
                width: columnDatum.size,
              };

              styleCache[cellKey] = cellStyle;
            }
          }

          const cellRendererParams: GridCellProps = {
            columnIndex,
            isScrolling,
            isVisible: isCellVisible,
            key: cellKey,
            parent,
            rowIndex,
            style: cellStyle,
          };

          let renderedCell: React.ReactNode;

          // Avoid re-creating cells while scrolling.
          // This can lead to the same cell being created many times and can cause performance issues for "heavy" cells.
          // If a scroll is in progress- cache and reuse cells.
          // This cache will be thrown away once scrolling completes.
          // However if we are scaling scroll positions and sizes, we should also avoid caching.
          // This is because the offset changes slightly as scroll position changes and caching leads to stale values.
          // For more info refer to issue #395
          if (
            isScrolling &&
            !horizontalOffsetAdjustment &&
            !verticalOffsetAdjustment
          ) {
            if (!cellCache[cellKey]) {
              cellCache[cellKey] = cellRenderer(cellRendererParams);
            }

            renderedCell = cellCache[cellKey];

            // If the user is no longer scrolling, don't cache cells.
            // This makes dynamic cell content difficult for users and would also lead to a heavier memory footprint.
          } else {
            renderedCell = cellRenderer(cellRendererParams);
          }

          if (renderedCell == null || renderedCell === false) {
            continue;
          }

          if (process.env.NODE_ENV !== 'production') {
            warnAboutMissingStyle(parent, renderedCell);
          }

          renderedCells.push(renderedCell);
        }

        const isRowVisible =
          rowIndex >= visibleRowIndices.start && rowIndex <= visibleRowIndices.stop;
        let rowStyle: React.CSSProperties;

        // Cache style objects so shallow-compare doesn't re-render unnecessarily.
        if (canCacheStyle && styleCache[rowKey]) {
          rowStyle = styleCache[rowKey];
        } else {
          rowStyle = {
            height: rowDatum.size,
            left: 0,
            position: 'absolute',
            right: 0,
            top: rowDatum.offset + verticalOffsetAdjustment,
          };

          styleCache[rowKey] = rowStyle;
        }

        const rowRendererParams: IGridRowProps = {
          children: renderedCells,
          isScrolling,
          isVisible: isRowVisible,
          key: rowKey,
          parent,
          rowIndex,
          style: rowStyle,
        };

        // The cache block is commented out - as caching rows when scrolling horizontally wont
        // render correctly when scrolling to new columns.
        // If re-enabled, its cache key should contain the range of column indecies that it shows.
        const renderedRow = rowRenderer(rowRendererParams);

        // Avoid re-creating cells while scrolling.
        // This can lead to the same cell being created many times and can cause performance issues for "heavy" cells.
        // If a scroll is in progress- cache and reuse cells.
        // This cache will be thrown away once scrolling completes.
        // However if we are scaling scroll positions and sizes, we should also avoid caching.
        // This is because the offset changes slightly as scroll position changes and caching leads to stale values.
        // For more info refer to issue #395

        /*
        if (
          isScrolling &&
          !horizontalOffsetAdjustment &&
          !verticalOffsetAdjustment
        ) {
          if (!cellCache[rowKey]) {
            cellCache[rowKey] = rowRenderer(rowRendererParams);
          }

          renderedRow = cellCache[rowKey];

          // If the user is no longer scrolling, don't cache cells.
          // This makes dynamic cell content difficult for users and would also lead to a heavier memory footprint.
        } else {
          renderedRow = rowRenderer(rowRendererParams);
        }
        */

        if (renderedRow == null) {
          continue;
        }

        if (process.env.NODE_ENV !== 'production') {
          warnAboutMissingStyle(parent, renderedRow);
        }

        renderedRows.push(renderedRow);
      }

      return renderedRows;
    };

    function warnAboutMissingStyle(parent: any, renderedCell: any) {
      if (process.env.NODE_ENV !== 'production') {
        if (renderedCell) {
          // If the direct child is a CellMeasurer, then we should check its child
          // See issue #611
          if (renderedCell.type && renderedCell.type.__internalCellMeasurerFlag) {
            renderedCell = renderedCell.props.children;
          }

          if (
            renderedCell &&
            renderedCell.props &&
            renderedCell.props.style === undefined &&
            parent.__warnedAboutMissingStyle !== true
          ) {
            parent.__warnedAboutMissingStyle = true;
            // tslint:disable-next-line:no-console
            console.warn(
              'Rendered cell should include style property for positioning.',
            );
          }
        }
      }
    }
R-iskey commented 6 years ago

Hi guys, I've the same problem, but I can't find good solution, maybe someone fixed this ?

kraenhansen commented 6 years ago

@R-iskey - did you see my implementation above? ☝️ To my knowledge, thats currently the best way to do it.

UnsungHero97 commented 6 years ago

@kraenhansen, thanks for sharing your rowCellRangeRenderer implementation. Can you also share how you tie it all together? How do you integrate Grid with SortableContainer, SortableElement?