Attilio
Attilio

Reputation: 1722

React-Virtualized table to grow up to a maximal height

Problem: I would like to have a React Virtualized table that can take up a maximal space in height (let's say 500px). Immediately below the last visible row of the table, I would like to add another element. Somehow like this:

If the content of the table is shorter than 500px

+-----------------------------------------+    ----+
| Header1 | Header2 | ...                 |        |
+---------+---------+---------------------+        |  less than 500px,
| row1    | row1    | row1                |        |  say 200px
+---------+---------+---------------------+        |
| row2    | row2    | row2                |        |
+---------+---------+---------------------+    ----+
Other content


If the content of the table is longer than 500px

+-----------------------------------------+^   ----+
| Header1 | Header2 | ...                 ||       |
+---------+---------+---------------------+|       |  500px,
| row1    | row1    | row1                ||       |  notice the
+---------+---------+---------------------+|       |  scroll bar
| row2    | row2    | row2                ||       |
+---------+---------+---------------------+|       |
| row3    | row3    | row3                ||       |
+---------+---------+---------------------+|       |
| row4    | row4    | row4                |V   ----+
Other content

In other words: there should be no white gap between the table and "Other content", but also the table should take up a maximum of 500px.

Question: How can I achieve this?

My attempt: I've tried with AutoSizer, but the issue is:

Remark: I'm not convinced at all, that AutoSizer is the correct approach here, so I'm open to any other suggestion (if possible, without further third-party dependencies, but that is ok too, if there is no other way).

Source for reference:

 <div>
   <div style={{height: "100px", maxHeight: "500px"}}>
      <AutoSizer>
        {({height, width}) => (
          <Table
            ref="Table"
            disableHeader={false}
            headerClassName=""
            headerHeight={40}
            height={height}
            rowClassName=""
            rowHeight={40}
            rowGetter={rowGetter}
            rowCount={6}
            width={width}>
            <Column
                label="Index"
                cellDataGetter={({rowData}) => rowData.index}
                dataKey="index"
                width={60}
            />
            <Column
              label="Text1"
              cellDataGetter={({rowData}) => rowData.text1}
              dataKey="text1"
              width={90}
            />
            <Column
              label="Text2"
              cellDataGetter={({rowData}) => rowData.text2}
              dataKey="text2"
              width={90}
            />
          </Table>
        )}
      </AutoSizer>             
  </div>
  <div>Some other text</div>
 </div>

Updates

Clarification of constraints: The solution has to work with Virtualized-React table. It is not an option to use HTML table. (Above I only said that maybe AutoSizer is not the right solution, but I do need react table.)

Suggested approach in the answer: I tried to apply @jered's suggestion to my case, and again, got a table which extends to the full max-height. I'm not sure if I did something wrong, or if the suggested approach does not work at all for this situation

   <div style={{  display: 'flex',
                  border: '1px solid red',
                  flexDirection: 'column' }}>
    <div style={{
                  display: 'flex',
                  maxHeight: '500px',
                  border: '1px solid blue',
                  padding: '2px',
                  margin: '2px',
                  overflow: 'scroll'
          }}>
          <Table
            ref="Table"
            disableHeader={false}
            headerClassName=""
            headerHeight={40}
            height={900}
            rowClassName=""
            rowHeight={40}
            rowGetter={rowGetter}
            rowCount={6}
            width={400}>
            <Column
                label="Index"
                cellDataGetter={({rowData}) => rowData.index}
                dataKey="index"
                width={60}
            />
            <Column
              label="Text1"
              cellDataGetter={({rowData}) => rowData.text1}
              dataKey="text1"
              width={90}
            />
            <Column
              label="Text2"
              cellDataGetter={({rowData}) => rowData.text2}
              dataKey="text2"
              width={90}
            />
          </Table>
   </div>
  <div style={{   display: 'flex',
                  flexDirection: 'column',
                  border: '1px solid green',
                  padding: '2px',
                  margin: '2px' }}>Some other text</div>
 </div>

Related question: Basically React Virtualized table creates a div with 0px x 0px size and visible overflow of its content. Thus, if the size of the overflow can be considered, this would solve the issue. This is an interesting question in itself, so I'll open a follow-up, but leave this one open, as there might be a better solution for this specific case.

One more update: I noticed that the zero-size div is not created, if I put the table inside a flex-container.

Upvotes: 5

Views: 6258

Answers (3)

I was able to fix this by disabling height in Autosizer and calculating it manually like this:

const ROW_HEIGHT = 40
const MAX_HEIGHT = 500
<AutoSizer disableHeight>
  {({ width }) => (
    <List
      width={width}
      height={Math.min(MAX_HEIGHT, ROW_HEIGHT * data.length)}
      rowCount={data.length}
      rowHeight={ROW_HEIGHT}
      rowRenderer={rowRenderer}
      overscanRowCount={10}
    />
  )}
</AutoSizer>

Upvotes: 1

J. Schei
J. Schei

Reputation: 347

I was able to accomplish this for React virtualized List using rowRenderer and refs, but I believe it should work with Table as well.

  1. Create state to contain the height of the list, make the default your intended max height:

const [listHeight, setListHeight] = useState(500);

  1. When rendering the List, use the height from this state:

<List height={listHeight} .../>

  1. Create a list of refs for each item in your list (in this case the elements are divs):

const rowRefs = useRef(dataInList.map(() => React.createRef<HTMLDivElement>()));

  1. On the rowRenderer, attach the refs to the DOM elements that are representing the rows in your table (I'm using cellMeasurer as well)

const rowRenderer = useCallback(
  ({ key, index, style, parent }: ListRowProps) => {
    const item = dataInList[index];
    return (
      <CellMeasurer
        key={key}
        cache={cache.current}
        columnIndex={0}
        parent={parent}
        rowIndex={index}>
          <div ref={rowRefs.current[index]}>
              Your row info goes here 
          </div>
      </CellMeasurer>
    );
  },
  [formatted]
);

  1. Create an effect to trigger when the data source changes (or possibly just once when the component mounts). The effect will sum the heights of the rows using the refs offsetHeight and set the state listHeight to that or whatever max height you want (whichever is less of the two).
useEffect(() => {
    const domListHeight = rowRefs.current
        .filter((e: any) => e.current)
        .reduce((acc: any, e: any) => acc + e.current.offsetHeight, 0);
    
    const MAX_HEIGHT = 500;
    const usableHeight = domListHeight < MAX_HEIGHT ? domListHeight : MAX_HEIGHT;
    
    setListHeight(usableHeight);
}, [dataInList]);

Summary: The component will render the list at the max height and then calculate the actual height of all the rows combined, if that total is less than the max height intended, the component will re-render the list from scratch with that new lower height.

Upvotes: 0

jered
jered

Reputation: 11571

No need to resort to React or anything complicated. Just use FlexBox in simple CSS.

Give the container of your "Table" and "Other content" display: flex and set a max-height on the inner element that wraps the <table>. FlexBox children will try to expand to fill as much space as their contents require and no more, but won't expand past their max-height if set.

Note: Expand snippet to full screen to see the full effect.

function adjustrows() {
  let val = document.getElementById("slider").value;
  let table = document.getElementById("table");
  let inner = `
    <tr>
      <th>foo</th>
      <th>bar</th>
      <th>baz</th>
      <th>foo</th>
      <th>bar</th>
      <th>baz</th>
    </tr>
  `;
  for (let i = 0; i < val; i++) {
    inner += `
      <tr>
        <td>foo</td>
        <td>bar</td>
        <td>baz</td>
        <td>foo</td>
        <td>bar</td>
        <td>baz</td>
      </tr>
    `;
  }
  table.innerHTML = inner;
  document.getElementById("numrows").innerText = val;
}
adjustrows();
table, tr, th, td {
  box-sizing: border-box;
  border: 1px solid gray;
}

table {
  flex-grow: 1;
}

th {
  background: lightgray;
}

label {
  display: flex;
  justify-content: center;
  margin: 5px;
}

#numrows {
  margin: 0 10px;
}

#flex-container {
  display: flex;
  border: 1px solid red;
  flex-direction: column;
}

#table-container {
  display: flex;
  max-height: 500px;
  border: 1px solid blue;
  padding: 2px;
  margin: 2px;
  overflow: scroll;
}

#other-content-container {
  display: flex;
  flex-direction: column;
  border: 1px solid green;
  padding: 2px;
  margin: 2px;
}

.placeholder {
  background: darkgray;
  margin: 10px;
  height: 120px;
}
<div id="flex-container">
  <label>
    <strong>Number of table rows:<span id="numrows"></span> </strong>
    <input id="slider" onchange="adjustrows();" oninput="adjustrows();" type="range" min="0" max="100" value="1" />
  </label>
  <div id="table-container">
    <table id="table">
    </table>
  </div>
  <div id="other-content-container">
    <div class="placeholder"></div>
    <div class="placeholder"></div>
    <div class="placeholder"></div>
    <div class="placeholder"></div>
    <div class="placeholder"></div>
  </div>
</div>

Note that if you delete some of the <tr> elements from the table so that it is shorter than 500px, #table-container will simply shrink down to accommodate.

As always, Mozilla Developer Network has some pretty good info on FlexBox.

Upvotes: 0

Related Questions