React-virtualized: Grid: colspan, rowspan support

Created on 25 Feb 2016  路  24Comments  路  Source: bvaughn/react-virtualized

What I want to achieve:

variant 1

Use separate grid per each header row with ScrollSync:

<Grid
  className={styles.HeaderGrid}
  columnWidth={columnWidth*3}
  columnsCount={columnsCount / 3}
  height={rowHeight}
  overscanColumnsCount={overscanColumnsCount}
  renderCell={this._renderSpannedHeaderCell}
  rowHeight={rowHeight}
  rowsCount={1}
  scrollLeft={scrollLeft}
  width={width}
/>
<Grid
  className={styles.HeaderGrid}
  columnWidth={columnWidth}
  columnsCount={columnsCount}
  height={rowHeight}
  overscanColumnsCount={overscanColumnsCount}
  renderCell={this._renderHeaderCell}
  rowHeight={rowHeight}
  rowsCount={1}
  scrollLeft={scrollLeft}
  width={width}
/>

variant 2

override cell width in rendered content and hide "unused" cells

<Grid
  className={styles.HeaderGrid}
  columnWidth={columnWidth}
  columnsCount={columnsCount}
  height={rowHeight*2}
  overscanColumnsCount={overscanColumnsCount}
  renderCell={this._renderHeaderCell}
  rowHeight={rowHeight}
  rowsCount={2}
  scrollLeft={scrollLeft}
  width={width}
/>
/* ... */
  _renderHeaderCell ({ columnIndex, rowIndex }) {
    if (rowIndex === 0) {
      return (columnIndex % 3 === 0) ? (
        <div className={styles.headerCell} style={{ width: 75*3 }}>
          {`C${columnIndex}-long-long-long`}
        </div>
      ) : (
        <div className={styles.headerCell} style={{ display: 'none' }} />
      )
    } else {
      return (
        <div className={styles.headerCell}>
          {`C${columnIndex}`}
        </div>
      )
    }
  }

variant 3 (not available currently)

But would be great to have colspan, rowspan support in grid to avoid extra hidden cells in DOM.
Don't sure in implementation but one possible is:

<Grid
  className={styles.HeaderGrid}
  columnWidth={columnWidth}
  columnsCount={columnsCount}
  height={rowHeight*2}
  overscanColumnsCount={overscanColumnsCount}
  renderCell={this._renderHeaderCell}
  rowHeight={rowHeight}
  rowsCount={2}
  scrollLeft={scrollLeft}
  width={width}
/>
/* ... */
  _renderHeaderCell ({ columnIndex, rowIndex, Grid__cell }) {
    if (rowIndex === 0) {
      return (
        <Grid__cell colspan={3}> { /* since colspan="3" _renderHeaderCell calls for R0,C1 and R0,C2 will be skipped */ }
          <div className={styles.headerCell}>
            {`C${columnIndex}-long-long-long`}
          </div>
        </Grid__cell>
      )
    } else {
      return (
        <div className={styles.headerCell}>
          {`C${columnIndex}`}
        </div>
      )
    }
  }

All 24 comments

Sorry @Guria. This isn't going to be implemented. I've considered it before and it would add a lot of complication. If you'd like to control this level of cell-sizing you should create your own high-order component wrapper around VirtualScroll.

@bvaughn this is ok, since there is a way to acheive desired result. btw, which existing variant do you like more?

I think your first variant, with ScrollSync, is better. It's more inline with how I'm using RV components in other projects.

I'm currently working on a similar requirement. I'm using Collection instead of two Grid. Works pretty well so far.

image

Nice! If you run into performance issues with Collection (which isn't as performant as Grid since it has to do more work, not being able to assume things about its underlying data) you may try switching to Grid and using the cellRangeRenderer property to render the rows/columns Grid tells you to, below your own fixed header.

Oh @phahn, do you mind to share you solution with Collection? I have a problem with grid. I want to achieve both colspan, and rowspan

Hey @hung-phan I have to see if I can share some code after the weekend but in the meantime here's what I'm doing:

I have configuration objects for columns like ColumnGroup which can contain other ColumnGroups and Columns

I pass this list of ColumnGroups to my grid which flattens it and calculates for every entry it's level (distance from top) and the totalWidth of that ColumnGroup which is the sum of it's leaf Column widths.

With the information totalHeaderHeight, number of levels, current level and width of each ColumnGroup and Column I can then create the appropriate div elements in cellSizeAndPositionGetter

For row spans the concept is similar.

Interesting. Initially, I think it may be possible to do something similar with Grid by overriding (forking) cellRangeRenderer. And Grid would be more efficient than Collection since it can make some shortcut assumptions about its data due to the fact that it's all checkerboard/linear.

Thanks @phahn, and @bvaughn for the idea

If you create a cellRangeRenderer implementation this that you like, consider submitting a PR. If it seems generalizable enough, I'll add it for others. :)

Out of interest, has anyone had any luck doing this within a grid? I am migrating a major angular 1.x project to React and am looking to do something like the following:

rowspan

IOW some non-fixed rows should span the entire grid and contain an SVG. At the moment I am pretty new to react-virtualized, so apologies if this has already been discussed.

I don't really have enough info to say for sure @csvan, but I believe you might want to look into using List for that. Grid is really only necessary when you want to have (a lot of) scrolling horizontal content as well.

@bvaughn I do need both horizontal and vertical scrollsync, since we will be rendering several thousand rows and columns at once. Sorry for not providing sufficient detail.

I looked into alterative 2 in the OP, and it does approximately what I want (with some hacks). It's regrettable that it leaves redundant invisible columns, but there is no noticeable performance degradation so I can live with that. Will update here if I find a better solution.

Thanks for the update!

It's regrettable that it leaves redundant invisible columns

For what it's worth, you can return null for these "invisible" columns.

@bvaughn awesome, thanks!

@Guria @bvaughn unfortunately, I hit a roadblock.
If I set the width of the "spanning" cell to that of the visible width of the Grid, and set display: none for all remaining cells (as per example 2 in the OP), the "spanning" cell will still pop out of existence when the user scrolls the initial section of it out of the visible part of the grid.

For example, if the Grid expects each cell to be 75px wide, and I force my "spanning" cell to be 1200px wide, it will still disappear when the first 75px (approx) are scrolled out the visible part.

Is there any way I can tell the Grid to only remove elements after a certain scroll threshold?

I don't really have a clear picture of what you're doing to be honest, but if you're using a custom cellRangeRenderer then you should be able to cheat a little in terms of when the extra-width cell gets hidden.

@bvaughn I am using a MultiGrid so that I can have nice distinct column and row headers to render a spreadsheet-type grid. Working very nicely so far. I also have an upcoming requirement to support grouped columns. This would affect only how the cells are rendered in the top right Grid. I like some of your suggestions here like trying cellRangeRenderer and/or returning null for "invisible" cells. I haven't tried anything yet, but I was thinking of just trying to handle it in the cellRenderer by having the grouped column cell that spans multiple child columns inject a position: relative div that has the appropriate width. This allows it to bleed out of its containing div. Heck, maybe I could just modify the containing div's width directly. Needs more thought and experimentation. I like your response about being able to return null for "invisble" cells.
But one question for you is with a MultGrid, what is the best way to get the reference to the contained Grids? Where in the React life-cycle can I do this? I would need to know this if I wanted to try the cellRangeRenderer technique since I want that for only the top-right Grid. Thanks for all this great body of work!

@csvan @bvaughn I have made some progress with MultiGrid in trying to render grouped columns. The approach I started with follows some of the suggestions here:

  1. Render "spanning" cells and modify their width to be the number of child columns spanned.
  2. Render null for the other cell slots in a grouped column that are "underneath" the spanning cell.

It looked good until I started scrolling horizontally. I hit the same roadblock @csvan did. That is, when the spanning cell falls out of the viewport being rendered (because MultiGrid thinks it has a much smaller width), it disappears.
Here are 2 screen shots which illustrate what I see. First is the non-scrolled picture which looks good:
image
Then, if I scroll right a bit so that the leaf column Col 0 scrolls out of view, I see this:
image
What tells me that my Group 0 spanning cell isn't being rendered is that I don't see the border anymore. This is confirmed by looking at the DOM.

I understand this behavior from the Grid's POV, but at this moment I am scratching my head. I guess I will investigate @bvaughn 's suggestion of using cellRangeRenderer. Haven't had any experience with that yet...

Yes, I have done this. cellRangeRenderer is the solution I used and it works well. Just adjust the columnStartIndex to make sure it captures the first 'real' column.

I also started with hiding cells, then returned null to prevent them rendering at all. Ends up quite a neat solution.

@MarkBarbieri Thanks, Mark. Yes, I am starting down that path of using cellRangeRenderer. To render my grouped column headers above the leaf column, I think my strategy has to be to examine the columnStartIndex passed to my override of defaultCellRangeRenderer for the MultiGrid._topRightGridRef. Find the leaf column at that index and then walk up its ancestor chain of columns and if the "cell slot" for those ancestor columns have a columnIndex that is less than columnStartIndex, then I have to manually rerender those (in my screen shots above that means the 2 cells for Group 0 and Group 0.0) and push them on to the children returned from defaultCellRangeRenderer. Making progress... What could go wrong? (A common saying in our little pod of developers as the code then crashes and burns...)

I used this approach to create a TV guide layout with the whole grid full of irregular blocks. I pre-processed the data and stored a 2D array with indices matching the column/row indices of the grid. Each array object stores whether the cell should be rendered, the width (which is really a columnSpan), the column index of the first visible cell for this element and the column index of the next visible cell. This provides easy manipulation of the focused element.

For example, a cell the spans columns 8-12, it stores

    {
        render: false (except for column 8)
        thisProgramColumn: 8
        nextProgramColumn: 13
        width: 5
    }

For cellRangeRendered, I then looped through the rows to be rendered,

               for (var rowIndex = rowStartIndex; rowIndex < rowStopIndex; rowIndex++) {

                    thisProgramColumn = gridCellParams[rowIndex][columnIndex].thisProgramColumn;

                    if (typeof thisProgramColumn !== 'undefined') {
                        columnStartIndex = Math.min(columnStartIndex, thisProgramColumn);
                    }

                }

                ref.columnStartIndex = columnStartIndex - fixedColumnCount;

            }

I used this approach to create a TV guide layout with the whole grid full of irregular blocks. I pre-processed the data and stored a 2D array with indices matching the column/row indices of the grid. Each array object stores whether the cell should be rendered, the width (which is really a columnSpan), the column index of the first visible cell for this element and the column index of the next visible cell. This provides easy manipulation of the focused element.

For example, a cell the spans columns 8-12, it stores

    {
        render: false (except for column 8)
        thisProgramColumn: 8
        nextProgramColumn: 13
        width: 5
    }

For cellRangeRendered, I then looped through the rows to be rendered,

               for (var rowIndex = rowStartIndex; rowIndex < rowStopIndex; rowIndex++) {

                    thisProgramColumn = gridCellParams[rowIndex][columnIndex].thisProgramColumn;

                    if (typeof thisProgramColumn !== 'undefined') {
                        columnStartIndex = Math.min(columnStartIndex, thisProgramColumn);
                    }

                }

                ref.columnStartIndex = columnStartIndex - fixedColumnCount;

            }

Wow this sounds like a really useful modification!

I was looking for a component which can create timetables. I'd really appreciate any tips you could give me about modifying rect-virtualized to support this timetable approach!

If any one is still interested in this. I have created a component with colspan and rowspan support with dynamic cell height.

https://github.com/pisharodySrikanth/virtualized-table

The component takes in data prop in a format similar to how we create the table in html. The dummy cells are hidden behind the rowspan div. This component has some caveats like the one mentioned by @pete-moss, border goes when the colspan / rowspan div is removed from the DOM. I handled this by showing border of the hidden dummy cells. This works because I don't need to vertically center align by rowspan div and knew beforehand that the content of the rowspanned cell is relatively smaller.

Also, I have created a wrapper around cell measurer and I calculate the height of the cell with rowspan by using the sum of cache.rowHeight for the next rowSpan no.of rows.

https://github.com/pisharodySrikanth/virtualized-table/blob/master/src/js/components/VirtualizedTable/CellMeasureWrapper.js

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mccambridge picture mccambridge  路  3Comments

bee0060 picture bee0060  路  3Comments

johnnyji picture johnnyji  路  3Comments

zllc picture zllc  路  3Comments

SBoudrias picture SBoudrias  路  3Comments