React-virtualized: [Request] Allow wrapping Grid rows in a component

Created on 18 Feb 2016  路  33Comments  路  Source: bvaughn/react-virtualized

Hi again!

I was wondering if you might be open to a rendering tweak that would (I think) allow more advanced use cases. Right now the Grid renders all columns and rows as boxes, each positioned appropriately in the large scroll space. As such there isn't really a concrete bit of markup that is a "row".

For us we are building a lot of Excel-like functionality on top of a basic grid, including keyboard navigation, cell cut/paste, drag to paste, etc. Practically this entails tracking a lot of per row state; most complicated is the tracking of complex cell/row editing state and validation errors.

In our current implementation we isolate a lot of that state in a Row component, so that we can carefully gate updates to limit entire grid re-renders. We'd like to use react-virualized as the base scrolling component (since its the best), but the lack of being able to define a Row component to handle updates for a set of cells is a bit of a blocker.

I do want to say that it is possible to locate that state higher up the component, but that is limiting technically because a row edit may involve a lot of higher volume updates (like typing into a textbox) which trigger a lot of updates in rows that aren't actually affected. In practice (we tried it with FB's fixedDataTable) it just is too slow.

We also can't locate all the state in each Cell Renderer because often updates cascade to the rest of the row (such as one cell value invalidating another cell).

We've testing altering the rendering of the Grid to wrap all cells in a row in a div and styling the div with the vertical offset (vs each cell) and its worked well as a proof of concept, but I am not sure if you have some other reason for not doing that.

discussion

Most helpful comment

Thank you for a quick answer!
Yes, I have complicated component with drag and drop logics in rows, that uses MultiGrid. Author of this request had similar needs, as I understood. I moved from your Table component to MultiGrid because it gives horizontal scrolling + fixed columns/rows out of the box. I guess, for my needs I could use List + Table with dynamically calculated width inside horizontally scrollable container + ScrollSync, to have fixed column and horizontal scroll at the same time (and have controllable row element in table), but that's what MultiGrid does, except of control over rows (because there are no row elements). I found this issue and realized I am not the only guy who needed that. If we could have something like optional rowWrapper function which should wrap it's children cells with separate react element and has styles (exactly like rowRenderer in Table), that could give us control over row-level manipulations like hover or drag and drop, while keeping Grid the same if you don't need specific wrapper for row (again, like rowRenderer in table works, except for it has default row renderer ). I think that most of use cases for MultiGrid are still something like showing Table and interacting with it's rows and cells somehow, so, that could be a good feature. Althought, of course I understand that the concept of Grid is showing blocks as a Grid, not in a rows :)
Anyway, thanks for great components, I used them already in several projects and always happy with their api, performance and flexibility

All 33 comments

Hey @jquense,

What you're describing makes sense. I had considered wrapping Grid cells in a "row" div but didn't want to add the row DOM elements unnecessarily. What you're describing sounds somewhat similar to how FlexTable uses Grid (internally). I wonder if you've considered just using VirtualScroll (or a 1-column Grid) for your purposes? Then you could have complete control over the contents of each "row" and whether they wrapped or not. You would just need to tell VirtualScroll what the overall height of each "row" was.

Thanks for the quick response! We have considered using VirtualScroll, at first there was issues with it not handling horizontal scrolling well but I think its reimplementation on top of the Grid fixes that. The other bit though it would be nice to not lose the virtualized columns, especially in a excel-like context.

I see how pushing the "row" markup to the Grid might complicate the one column Components on top of it...perhaps in those cases you could just use the Row Renderer to avoid extra markup? Nothing strikes as a really elegant solution there :P

Oh, interesting. So you're wanting horizontal scrolling _and_ row-wrapping? How would the columns know when to wrap? (Could you give me more context, maybe a mockup or something?)

I'll see if I can mock something up, though I want to clarify, by "wrap" I just mean be able to have a component around the cells in a row. I don't want to visually collapse columns. Ideally I don't even need extra markup except for the fact that React components can't render and array (cells).

This is really more of a technical issue of having a middle place between the Grid and Cell to place a shouldComponentUpdate and allow a set of cells in a row to propagate up some state (like current value) to a component without needing to trigger a render of entire grid.

Our current implementation is forked from: http://adazzle.github.io/react-data-grid/examples.html#/fixed (repo here) I quite like the functionality to build off of but the actual virtualized scroll bits are not great.

Ohhh! I misunderstood what you meant by wrapping. I thought you were talking about flex-wrap :)

Can I ask by chance if you've tried this with a react-virtualized component? To be honest I'm not sure that even in the worst case (re-rendering the Grid) it would be _that_ bad, since it wouldn't be rendering "hidden" cells.

I haven't tried it with these components, partly because it would require a big rewrite of the grid we already have, and partly because we've seen that any sort of recalculation/rerender (may just be React) gets expensive when you are rerendering on key presses even if it is only rendering like 20 rows.

That being said I get not wanting to make a substantive change based on me saying "its slow" without evidence. Let me see if there is any sort of middle ground to demonstrate...

Sounds good. I look forward to seeing it. Sounds like maybe an opportunity for another useful high-order component that decorates a Grid.

Closing this issue since there is no clear action required on my part for the time being. If your test harness reveals any performance problems @jquense please get back to me and I can re-open the issue if needed. :)

Hi, found this issue, not sure if I should write here or start new issue. Let it be here, thought it's closed, as it looks similar

Me too need a way to control Grid and MultiGrid rows, and I have no simple way to do that. I need it for two things - row hover state and row droppable(and may be draggable too). First case is simple - I only have to determine row index from cell hover, remember that index and pass it to all cells to highlight cells of current hovered row index. But when it comes to droppable it became more complicated - I need row-level droppable, but instead, I should make each cell in grid droppable, and to highlight needed row I need to determine current drop-over row from each cell, and with the help of some external action pass it back to all cells of highlighted row, as I do when handling hover. Considering the fact that each of my dropzones have 2 sub-dropzones (item can be dropped before, or after hovered row), I can't reach acceptable performance of drag and drop in my table component :(

It would be so nice If I could wrap rows in some component like we have it in react-virtualized/Table rowRenderer, so I can make each row draggable, sortable, droppable, hoverable or whatever

Could you please suggest on possible solution?

It would be so nice If I could wrap rows in some component like we have it in react-virtualized/Table rowRenderer, so I can make each row draggable, sortable, droppable, hoverable or whatever

You could use the List component for this. It literally wraps rows. The downside is that it doesn't do horizontal windowing, so maybe it's not what you want.

Your use case sounds really complicated. This project offers building blocks. I try to make the building blocks pretty powerful but I can't always offer complete solutions. I'd just suggest taking a look at things like clauderic/react-sortable-hoc and experiment.

Thank you for a quick answer!
Yes, I have complicated component with drag and drop logics in rows, that uses MultiGrid. Author of this request had similar needs, as I understood. I moved from your Table component to MultiGrid because it gives horizontal scrolling + fixed columns/rows out of the box. I guess, for my needs I could use List + Table with dynamically calculated width inside horizontally scrollable container + ScrollSync, to have fixed column and horizontal scroll at the same time (and have controllable row element in table), but that's what MultiGrid does, except of control over rows (because there are no row elements). I found this issue and realized I am not the only guy who needed that. If we could have something like optional rowWrapper function which should wrap it's children cells with separate react element and has styles (exactly like rowRenderer in Table), that could give us control over row-level manipulations like hover or drag and drop, while keeping Grid the same if you don't need specific wrapper for row (again, like rowRenderer in table works, except for it has default row renderer ). I think that most of use cases for MultiGrid are still something like showing Table and interacting with it's rows and cells somehow, so, that could be a good feature. Althought, of course I understand that the concept of Grid is showing blocks as a Grid, not in a rows :)
Anyway, thanks for great components, I used them already in several projects and always happy with their api, performance and flexibility

So, is there any chance of requested feature in next versions?

Not likely.

Grid already offers cellRangeRenderer prop for users to do more advanced cell manipulation. You could try to add your own row-wrapper this way.

I don't think this feature is common enough use case to warrant the added complexity to the base library though.

Many thanks!
Actually cellRangeRenderer was exactly what I needed, works! Wondering how I missed this property

Happy to hear it. 馃槃

Is there an easy way to tell which grid segment is being rendered for a MultiGrid, using cellRangeRenderer?

If I try to detect based on the columnStartIndex, columnStopIndex, rowStartIndex and rowStopIndex, that works, except if the grid window size is small, then the detection will fail. I'm thinking a workaround would be to have a local variable to track which grid segment is being rendered between calls to cellRangeRenderer, since it seems to cycle between top left, top right, bottom left, and bottom right.

Haven't really considered this use-case. The index ranges seems like the only/best thing that occurs to me off the top.

My workaround idea was to do something like this:

  cycleGridSegment () {
    this.renderedGridSegment = {
      topLeft: 'topRight',
      topRight: 'bottomLeft',
      bottomLeft: 'bottomRight',
      bottomRight: 'topLeft'
    }[this.renderedGridSegment];
  }

  cellRangeRenderer (args) {
    const renderer = {
      topLeft: this.topLeftRenderer,
      topRight: this.topRightRenderer,
      bottomLeft: this.bottomLeftRenderer,
      bottomRight: this.bottomRightRenderer
    }[this.renderedGridSegment];

    this.cycleGridSegment();
    return renderer(args);
  }

But I think the best solution would be just to have MultiGrid accept topLeftCellRangeRenderer, topRightCellRangeRenderer, etc. I'll see if I can put together a PR.

My use case involves fixed rows and columns, as well as merged rows so I think I need both MultiGrid and cellRangeRenderer to cleanly implement that, without resorting to absolutely positioning elements over top of grid cells.

@mikhail-eremin Any chance you have an example of row implementation with cellRangeRenderer?

@grahamlutz i did it like this. it contains some tunings for working with fixed rows and columns
It was made from example taken from cellRangeRenderer docs

this.props.rowRenderer is any wrapper that render it's children and doing whatever you want (e.g. drag and drop)

cellRangeRenderer = (
        {
            cellCache,                    // Temporary cell cache used while scrolling
            cellRenderer,                 // Cell renderer prop supplied to Grid
            columnSizeAndPositionManager, // @see CellSizeAndPositionManager,
            columnStartIndex,             // Index of first column (inclusive) to render
            columnStopIndex,              // Index of last column (inclusive) to render
            horizontalOffsetAdjustment,   // Horizontal pixel offset (required for scaling)
            isScrolling,                  // The Grid is currently being scrolled
            rowSizeAndPositionManager,    // @see CellSizeAndPositionManager,
            rowStartIndex,                // Index of first column (inclusive) to render
            rowStopIndex,                 // Index of last column (inclusive) to render
            scrollLeft,                   // Current horizontal scroll offset of Grid
            scrollTop,                    // Current vertical scroll offset of Grid
            styleCache,                   // Temporary style (size & position) cache used while scrolling
            verticalOffsetAdjustment      // Vertical pixel offset (required for scaling)
        }
    ) => {
        const renderedRows = [];
        /*
         NOTE
         We need to check if it is a fixed table part, or one which is under scrollbar, because
         cellRender here is considering real rowIndex, while rowStartIndex and rowStopIndex start from zero at each grid
         Eliminating this with magic maths >:0
        * */
        // HACK - check if it's fixed table part. We need it to make offset for rowIndex in ;
        const isFixedTablePart = (rowStopIndex - rowStartIndex) === (this.props.fixedRowCount - 1);

        // Loop throught rows
        for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
            // This contains :offset (top) and :size (height) information for the cell
            const rowDatum = rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex)
            const children = [];
            let rowWidth = 0;
            const rowStyle = {
                top: rowDatum.offset + verticalOffsetAdjustment,
                left: horizontalOffsetAdjustment,
                height: rowDatum.size
            }
            // Loop throught columns
            for (let columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
                // This contains :offset (left) and :size (width) information for the cell
                const columnDatum = columnSizeAndPositionManager.getSizeAndPositionOfCell(columnIndex)

                // Be sure to adjust cell position in case the total set of cells is too large to be supported by the browser natively.
                // In this case, Grid will shift cells as a user scrolls to increase cell density.
                const left = columnDatum.offset;

                // The rest of the information you need to render the cell are contained in the data.
                // Be sure to provide unique :key attributes.
                const key = `${rowIndex}-${columnIndex}`;
                const height = rowDatum.size;
                const width = columnDatum.size;
                rowWidth += width;

                const style = {
                    // top,
                    left,
                    height,
                    width
                };
                children.push(
                    cellRenderer({
                        columnIndex,
                        rowIndex,
                        key,
                        style
                }));
            }

            // end of columns loop, make row width from column width sum
            rowStyle.width = rowWidth;

            const trueRowIndex = isFixedTablePart ? rowIndex : (rowIndex + this.props.fixedRowCount);
            const indexInDataArray = trueRowIndex - 1;
            const isHeaderRow = trueRowIndex === 0;

            // If it's not header row - render with rowRenderer
            if (!isHeaderRow) {
                renderedRows.push(
                    <this.props.rowRenderer
                        key={`row-${trueRowIndex}`}
                        style={rowStyle}
                        isHover={this.state.hoveredRowIndex === trueRowIndex}
                        onMouseEnter={() => this.handleHover(trueRowIndex)}
                        onMouseLeave={this.mouseLeaveDelayed}
                        rowIndex={indexInDataArray}
                        {...this.props.rowRendererProps}
                    >
                        { children }
                    </this.props.rowRenderer>
                );
            } else {
                renderedRows.push(
                    <DefaultRowRenderer
                        key={`row-${trueRowIndex}`}
                        style={rowStyle}
                    >
                        { children }
                    </DefaultRowRenderer>
                );
            }
        }
        return renderedRows;
    }

What I ended up doing was simply use two Grid/MultiGrids - for my use case, I only ever have one or two full width rows. Then all I needed to do was ScrollSync the two together for horizontal scrolling.

If you can get away with this approach, it really simplifies your grid.

e.g.:

|cell|cell|cell|cell|cell| (Grid for rendering headers)
+----+----+----+----+----+
|     Full Width row     | (div react component)
+----+----+----+----+----+
|     Full Width row     |
+----+----+----+----+----+
|cell|cell|cell|cell|cell|
+----+----+----+----+----+
|cell|cell|cell|cell|cell| (Grid for rendering data)
+----+----+----+----+----+
|cell|cell|cell|cell|cell|
+----+----+----+----+----+

cool thanks @mikhail-eremin!

So where does cellRangeRenderer get columnStartIndex, columnStopIndex, rowStartIndex, and rowStopIndex? They seem to be random numbers haha. I am working on a rebuild of an existing, pretty complicated grid using that uses List, b/c we need locked columns.

Those indices are certainly not random 馃槈 They're documented.

Haha yeah "we don't believe in magic", right? I'm not seeing from that documentation where those values come from. I get that its the "Index of first column (inclusive) to render", but where does that come from? How does that get set?

side note: rowStartIndex also says // Index of first column (inclusive) to render, which I assume should be // Index of first row (inclusive) to render?

They come from the caller, Grid.

Yes, looks like the docs have a typo there. PRs welcome. 馃槃

That's the stuff! thanks!

Hi, I'm little late on this thread but @mikhail-eremin did have an example of collapsible Multigrid row?

Hi, I'm little late on this thread but @mikhail-eremin did have an example of collapsible Multigrid row?

No, only the snippet of code pasted some posts above, it doesn't implement collapsible rows

@mikhail-eremin do you have any tip where can I do that?

@rhinoceraptor were you able to implement collapsible rows to the Grid or Multigrid?

@mikhail-eremin do you have any tip where can I do that?

@hecomp
react-virtualized doesn't implement that because it's feature is windowing data. You may need other library if you want it "out of box".
If you still want to use exactly react-virtualized table - just implement that yourself on the data layer(e.g. redux or mobx or whatever you using), add expand/collapse triggers in a separate column, and just add/remove expanded rows to your dataset by this trigger, put them in your data, exactly like other regular rows, just have some marker in data for different styling or whatever you want for your expanded rows, it's all up to your case. You can even have some complicated rowRenderer which shows those expanded rows not as "row" but as some custom component, just use your fantasy and documentation

@mikhail-eremin great tips man. I thought that I had to do what you said. That was a cellRangeRenderer code snippet above that I thought it would be some guide to custom implement the expand functionality.

Was this page helpful?
0 / 5 - 0 ratings