Search code examples
react-native

How to recreate html table functionality in React Native?


I'm trying to recreate html table functionality in React Native and I just cannot figure this out.

I have 1 column for item name (short), and another for item description (potentially very long).

I want the first column to take up only as much space as it needs and the second column to flex and then text-wrap when it runs out of room. That part's easy, but then the items in the first column all have different widths. And if I make the columns first instead in order to solve that problem, then getting the corresponding items in the first column to flex vertically to stay aligned is tripping me up. HTML tables do this effortlessly. Why is it so hard in Native?

Is there really no way to do a truly flexible table in this language?

I've tried different variations on flex, but I don't want it to be a fixed width or ratio for either column, since I want to leave the option for font sizes later, which would break it.

react-native-paper fails because DataTable.Cell doesn't allow for multiline, and adding in the functionality messes with the alignment, bringing me right back to where I started.

EDIT: In html, I would do it like this:

<html>
    <head>
        <style>
            td:first-child {white-space: nowrap; }
        </style>
    </head>
    <body>
        <table>
            <tr>
                <td>
                    I am some data!
                </td>
                <td>
                    Two households, both alike in dignity, in fair Verona, where we lay our scene, from ancient grudge break to new mutiny, where civil blood makes civil hands unclean.
                </td>
            </tr>
            <tr>
                <td>
                    I'm data too!
                </td>
                <td>
                    From forth the fatal loins of these two foes, a pair of star-cross'd lovers take their life; whose misadventured piteous overthrows do with their death bury their parents' strife.
                </td>
            </tr>
            <tr>
                <td>
                    I am also some data!
                </td>
                <td>
                    The fearful passage of their death-mark'd love, and the continuance of their parents' rage, which, but their children's end, nought could remove, is now the two hours' traffic of our stage; the which if you with patient ears attend, what here shall miss, our toil shall strive to mend.
                </td>
            </tr>
        </table>
    </body>
</html>

resulting in:

enter image description here


Solution

  • Okay, I think I got pretty much the functionality I wanted. There's probably a way more extensible way to do this, and this is probably wildly inefficient, but it seems to work.

    The gist is that I can use a state variable to store the desired width of each cell, and then in onLayout, I can call setColWidth to update that variable whenever the layout changes. Then, I can just use style to set the minimum width to the width of the biggest cell.

    Then there's an array that determines whether a given column gets shrunk to allow room for others.

    Finally, I can call alignItems in the parent View to shrink-wrap the tables to the minimum size that fits the data

    import React, {useState} from 'react';
    import {LayoutChangeEvent, useWindowDimensions, View} from 'react-native';
    
    const Table = ({
      data,
      rowStyle = undefined,
      priviledge = new Array(data.length).fill(false),
    }: {
      data: any[];
      rowStyle: Object | undefined;
      priviledge: boolean[];
    }) => {
      // Initialize list of widths
      const [colWidth, setColWidth] = useState<number[][]>(
        data.map(row => new Array(Object.keys(row).length).fill(0)),
      );
    
      // Get widows size
      const maxSize = useWindowDimensions();
    
      if (!colWidth || !maxSize) {
        return <></>;
      }
    
      // Fix issues of going off screen
      const onLayout = (event: LayoutChangeEvent, row: number, col: number) => {
        // Get current width
        var {width, __} = event.nativeEvent.layout;
    
        // Find total row width
        const sum =
          colWidth[row].reduce((partialSum, a) => partialSum + a, 0) -
          colWidth[row][col] +
          width;
    
        // Shrink unpriviledged components
        if (!priviledge[col] && sum > maxSize.width) {
          width = width - (sum - maxSize.width);
          if (width < 0) {
            width = 0;
          }
        }
    
        // Store width in colWidth array
        colWidth[row][col] = width;
        setColWidth([...colWidth]);
      };
    
      return (
        <View>
          {/* Map along rows */}
          {data.map((item, rowIndex) => (
            <View
              key={rowIndex}
              style={{
                flexDirection: 'row',
                maxWidth: maxSize.width,
              }}>
              {/* Map along columns */}
              {Object.keys(item).map((key, colIndex) => (
                <View
                  key={key}
                  onLayout={event => {
                    onLayout(event, rowIndex, colIndex);
                  }}
                  style={{
                    minWidth: Math.max(...colWidth.map(row => row[colIndex])),
                    flexShrink: 1,
                    ...rowStyle,
                  }}>
                  {item[key]}
                </View>
              ))}
            </View>
          ))}
        </View>
      );
    };
    
    export default Table;
    
    

    Here's how it looks with some example data from my project (the yellow boxes are the tables using this code):

    Android emulator showing the working code

    Right now, the only problem I can see is that it doesn't update when rotating from landscape to portrait (but portrait to landscape works fine???).