mayo-s
mayo-s

Reputation: 65

How to create a dynamic table in React JS depending on keys?

I am trying to create a dynamic table component in React JS. The component currently only has a static header including the most common result keys. Some results also hold more information i.e. phone_number, degree. How do I dynamically extend the table with an additional column depending on presents of key/value? Should I work with state and make visible when present, or should I pre-process the table (but how do I ensure the correct order)? Are there better ways to do so?

import React, { Component } from 'react';

class ResultTable extends Component {

  addTableRow = (result) => {
    return (
      <tr key={result._id}>
        <td>{result.lastname}</td>
        <td>{result.firstname}</td>
        <td>{result.city}</td>
        <td>{result.zip}</td>
        <td>{result.street} {result.street_number}</td>
      </tr>)
  }

  createTable = (results) => {
    return (
      <table className="striped highlight ">
        <thead>
          <tr>
            <th>Lastname</th>
            <th>Firstname</th>
            <th>City</th>
            <th>ZIP</th>
            <th>Street</th>
          </tr>
        </thead>
        <tbody>
          {results.map((result, index) => {
            return this.addTableRow(result)
          })}
        </tbody>
      </table>
    )
  }

  render() {
    return (
      <div>
        {this.props.results.length ? (
          <div className="card">
            {this.createTable(this.props.results)}
          </div>
        ) : null}
      </div>
    )
  }
}

export default ResultTable

results look like that:

[
    { "_id": 1, "area_code": "555", "city": "Berlin", "firstname": "John", "lastname": "Doe", "phonenumber": "12345678", "zip": "12345"},
    { "_id": 2, "area_code": "555", "city": "Frankfurt", "firstname": "Arnold", "lastname": "Schwarzenegger", "phonenumber": "12121212", "street": "Main Street", "street_number": "99", "zip": "545454"},
    { "_id": 3, "area_code": "123", "firstname": "Jane", "lastname": "Doe", "phonenumber": "777777", "fav_color": "blue"},
    { "_id": 4, "area_code": "456", "firstname": "Scooby", "lastname": "Doo",  "phonenumber": "444444"}, "note": "Lazy but cool"
]

Upvotes: 3

Views: 18636

Answers (3)

Achintya Ashok
Achintya Ashok

Reputation: 539

Another way to do it is to have a "private" function that sets the state of the component. The table can render based on a pre-computed DOM stored in this state.

The computing function that infers the DOM elements can be dynamically set based on the prop data passed to the component. Ex. you can have a table with 1 column, 4 columns, etc.

A side-benefit of this approach is that you can set the computed state again whenever this component's props change.

Here's an example:


interface ITableCellData {
    value: any
}

interface IFooProps {
    rowValues: Array<Array<ITableCellData>>
}

interface IFooState {
    computedRowValues: Array<Array<any>>
}

class Foo extends Component<IFooProps, IFooState> {

    constructor(props: IFooProps) {
        super(props)
        this.state = { computedRowValues: [] }
        this._computeAndSetState(props)
    }

    _computeAndSetState(props: IFooProps) {
        const computedRows = []
        for (let rowData of props.rowValues) {
            const computedRowCells: Array<any> = []

            for (let i = 0; i < rowData.length; i++) {
                const cell = rowData[i]

                // Logic for constructing your cell
                computedRowCells.push(<td>{cell.value}</td>)
            }

            computedRows.push(computedRowCells)
        }

        this.setState({ computedRows: computedRows })
    }

    render() {
       return (
          <table>
             <tbody>
             {this.state.computedRows.map((rowData, idx) => {
                 return <tr key={idx}>{rowData}</tr>
              })}
             </tbody>
          </table>
       )
    }

}

Upvotes: 0

mayo-s
mayo-s

Reputation: 65

Based on @DarioRega's answer I will post my solution underneath. The key to my solution is his mappDynamicColumns function. In this solution the headers state is pre-filled with those headers that I wish to have first when rendering the table. fetchTableHeaders() is looping through all results and adds headers as they appear in a result. It is also possible to ignore certain result keys if they are not wanted as columns (credits to @DarioRega).

import React, { Component } from 'react';

class ResultTable extends Component {

  state = {
    headers: ['lastname', 'firstname', 'city', 'zip', 'street'],
    ignore_headers: ['_id', 'flags', 'street_number'],
  }

  addTableRow = (headers, result) => {
    return (
      <tr key={result._id}>
        {headers.map((h) => {
          if(this.state.ignore_headers.includes(h)) return null
          if(h === 'street') return (<td>{result.street} {result.street_number}</td>)
          return (<td>{result[h]}</td>)
        })}
      </tr>)
  }

  createTable = (results) => {

    let headers = this.fetchTableHeaders(results);

    return (
      <table className="striped highlight ">
        <thead>
          <tr>
            {headers.map((h) => {
              return (<th>{this.makeHeaderStr(h)}</th>)
            })}
          </tr>
        </thead>
        <tbody>
          {results.map((result) => {
            return this.addTableRow(headers, result)
          })}
        </tbody>
      </table>
    )
  }

  fetchTableHeaders = (results) => {
    let headers = [...this.state.headers];
    results.forEach((result) => {
      Object.keys(result).forEach((r) => {
        if(!this.state.ignore_headers.includes(r) && !headers.includes(r)) {
          headers.push(r);
        }
      });
    });
    return headers
  }

  makeHeaderStr = (header_str) => {
    if(typeof header_str !== 'string') return header_str
    return (header_str.charAt(0).toUpperCase() + header_str.slice(1)).replace('_', ' ')
  }

  render() {
    return (
      <div>
        {this.props.results.length ? (
          <div className="card">
            {this.createTable(this.props.results)}
          </div>
        ) : null}
      </div>
    )
  }
}

export default ResultTable

Upvotes: 1

DarioRega
DarioRega

Reputation: 1042

Alright alright, i took me around two hours to do it and to get back into react, it's been a while i haven't touched it. So if y'all see something to refactor, with pleasure you can edit !

The code first :

import React from "react";
import "./styles.css";

export default class App extends React.Component {
  state = {
    columns: [],
    columnsToHide: ["_id"],
    results: [
      {
        _id: 1,
        firstname: "Robert",
        lastname: "Redfort",
        city: "New York",
        zip: 1233,
        street: "Mahn Street",
        street_number: "24A",
        favoriteKebab: "cow"
      },
      {
        _id: 2,
        firstname: "Patty",
        lastname: "Koulou",
        city: "Los Angeles",
        zip: 5654,
        street: "Av 5th Central",
        street_number: 12
      },
      {
        _id: 3,
        firstname: "Matt",
        lastname: "Michiolo",
        city: "Chicago",
        zip: 43452,
        street: "Saint Usk St",
        street_number: 65,
        phoneNumber: "0321454545"
      },
      {
        _id: 4,
        firstname: "Sonia",
        lastname: "Remontada",
        city: "Buenos Aires",
        zip: "43N95D",
        street: "Viva la Revolution Paso",
        street_number: 5446,
        country: "Argentina"
      }
    ]
  };
  componentDidMount() {
    this.mappDynamicColumns();
  }

  mappDynamicColumns = () => {
    let columns = [];
    this.state.results.forEach((result) => {
      Object.keys(result).forEach((col) => {
        if (!columns.includes(col)) {
          columns.push(col);
        }
      });
      this.setState({ columns });
    });
  };

  addTableRow = (result) => {
    let row = [];
    this.state.columns.forEach((col) => {
      if (!this.state.columnsToHide.includes(col)) {
        row.push(
          Object.keys(result).map((item) => {
            if (result[item] && item === col) {
              return result[item];
            } else if (item === col) {
              return "No Value";
            }
          })
        );
        row = this.filterDeepUndefinedValues(row);
      }
    });

    return row.map((item, index) => {
      // console.log(item, "item ?");
      return (
        <td
          key={`${item}--${index}`}
          className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
        >
          {item}
        </td>
      );
    });
  };

  mapTableColumns = () => {
    return this.state.columns.map((col) => {
      if (!this.state.columnsToHide.includes(col)) {
        const overridedColumnName = this.overrideColumnName(col);
        return (
          <th
            key={col}
            scope="col"
            className="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
          >
            {overridedColumnName}
          </th>
        );
      }
    });
  };

  filterDeepUndefinedValues = (arr) => {
    return arr
      .map((val) =>
        val.map((deepVal) => deepVal).filter((deeperVal) => deeperVal)
      )
      .map((val) => {
        if (val.length < 1) {
          val = ["-"];
          return val;
        }
        return val;
      });
  };

  // if you want to change the text of the col you could do here in the .map() with another function that handle the display text

  overrideColumnName = (colName) => {
    switch (colName) {
      case "phoneNumber":
        return "Phone number";
      case "lastname":
        return "Custom Last Name";
      default:
        return colName;
    }
  };

  createTable = (results) => {
    return (
      <table class="min-w-full divide-y divide-gray-200">
        <thead>
          <tr>{this.mapTableColumns()}</tr>
        </thead>
        <tbody>
          {results.map((result, index) => {
            return <tr key={result._id}>{this.addTableRow(result)}</tr>;
          })}
        </tbody>
      </table>
    );
  };

  render() {
    return (
      <div class="flex flex-col">
        <div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
          <div class="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
            <div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
              {this.state.results.length ? (
                <div className="card">
                  {this.createTable(this.state.results)}
                </div>
              ) : null}
            </div>
          </div>
        </div>
      </div>
    );
  }
}

Why undefined ? Well, like i said, im kind of rotten on react (and maybe on codding ;) ) But i didn't find any other way to keep the orders of each value under her belonging column without it being at her current index in the array.

To come back to our code, we end up with a variable row populated with each values belonging to her current column name, so at the right index.

the payload of row before being sending back on the html is so :

As you see, it isn't clean. But i did not find any solution for that, i believe it's possible but it requires more time to think.

So since it's really deeply nested, i had to come up with some filters, to allow the user to see when the property doens't exist, or we could just left it blank.

After each push, i need to clean my deep nested values in arrays, so i've come up with a function filterDeepUndefinedValues row = this.filterDeepUndefinedValues(row)

And the function itself

filterDeepUndefinedValues = (arr) => {
  return arr
    .map((val) => val.map((deepVal) => deepVal).filter((deeperVal) => deeperVal))
    .map((val) => {
      if (val.length < 1) {
        val = ["-"];
        return val;
      }
      return val;
    });
};

To make short, i have arrays nested in array which contain undefined values, so i'll return a new array filtered out without undefined value, so it return an empty array.

How the array is received in the function initially enter image description here

In the second part, i just replace empty array content by a hyphen in array

First treatment with .map and filter() enter image description here

Replace empty array with a hyphen enter image description here

The return of addTableRow

return row.map((item, index) => {
  return (
    <td
      key={`${item}--${index}`}
      className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
    >
      {item}
    </td>
  );
});

Here we simply map all the values from rows and render a td for each array of row

Im running out of time to end the details, but all it's here and i will try to come back later to clean a bit this answer and it's grammar.

Codesandbox link: https://codesandbox.io/s/bold-mendel-vmfsk?file=/src/App.js

Upvotes: 7

Related Questions