reactjstypescriptmaterial-tabledynamic-columns

How to set material-table dynamic columns in react / typescript


I have a table that has to display an arbitrary number of columns which are dynamically generated from the received data.

I have created a column interface that will hold all the properties I need for each column, such as whether the column can be minimized (made narrow), whether it is a column for which I have to render an icon from a structure or maybe render specific data for the group-row, etc.

In material-table I am trying to map these columns, but always get the error: Error: this.tableContainerDiv.current is null

This happens even if the list of objects are exactly the same as when I hard-code the columns.

My (cut-down) code:

interface IColumnObject {
  field: string;
  title: string;
  sorting: boolean;
  minimizing: boolean;
  minimized: boolean;
  hidden: boolean;
  width: string;
  grouprender: boolean;
  iconrender: boolean;
}

interface State {
  detailData: ITableData[];
  filteredData: ITableData[];
  headers: IHeaderObject[];
  columns: IColumnObject[];
  filterList: IFilterList;
  anchorEl: Element;
  csvHeader: ICsvHeader[];
  csvData: ICsvData[];
}

class DetailAllRoute extends React.Component<Props, State> {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private csvLink: React.RefObject<any> = React.createRef();
...
  public generateTableDetailData = () => {
    const tableData: ITableData[] = [];
    let createHeader = true;
    let createColumn = true;
    const headers: IHeaderObject[] = [];
    const columns: IColumnObject[] = [];
...
    // Add the Application, Filename and Location column objects to the columns list
    const applicationColumn: IColumnObject = {
      field: 'application',
      title: this.i18n.translateToString('Column_Application'),
      sorting: true,
      minimizing: false,
      minimized: false,
      hidden: false,
      width: '100px',
      grouprender: false,
      iconrender: false,
    };

    columns.push(applicationColumn);

    const filenameColumn: IColumnObject = {
      field: 'filename',
      title: this.i18n.translateToString('Column_Filename'),
      sorting: true,
      minimizing: false,
      minimized: false,
      hidden: false,
      width: '270px',
      grouprender: true,
      iconrender: false,
    };

    columns.push(filenameColumn);

    const locationColumn: IColumnObject = {
      field: 'location',
      title: this.i18n.translateToString('Column_Location'),
      sorting: true,
      minimizing: true,
      minimized: false,
      hidden: false,
      width: '350px',
      grouprender: false,
      iconrender: false,
    };

    columns.push(locationColumn);
  ...
  }

  render() {
    const { classes } = this.props;
    const {
      compData,
      filteredData,
      columns,
      filterList,
      anchorEl,
    } = this.state;

    return (
      <React.Fragment>
        <Paper className={classes.muiListRoot} style={{ backgroundColor: '#fff' }}>
          <MaterialTable
            title={
              <span style={{ fontSize: '2.0em', fontWeight: 'bold', color: '#19768B' }}>
                {`${this.i18n.translateToString('Table_DetailData')} Test`}
              </span>
            }
            actions={[
              {
                icon: FilterList,
                tooltip: 'Filter',
                position: 'toolbar',
                onClick: event => {
                  this.handlePopoverClick(event);
                },
              },
            ]}
            components={{
              // eslint-disable-next-line react/display-name
              Header: headerprops => (
                <React.Fragment>
                  {this.getTableHeader()}
                  <MTableHeader {...headerprops} />
                </React.Fragment>
              ),
            }}
            columns={columns.map(c => {
              return {
                title: c.title,
                field: c.field,
                sorting: c.sorting,
                width: c.width,
                hidden: c.hidden,
              } as Column<any>;
            })}
            data={filteredData}
            parentChildData={(row, rows) => rows.find(a => a.application === row.parent)}
            options={{
              toolbar: true,
              sorting: true,
              exportButton: { csv: true },
              exportCsv: () => {
                this.customExportCSV();
              },
              headerStyle: {
                backgroundColor: '#19768B',
                color: 'white',
                borderBottom: '1px solid black',
              },
              // eslint-disable-next-line @typescript-eslint/no-explicit-any
              rowStyle: (_data: any, index: number) => {
                return index % 2 ? { backgroundColor: '#ecf2f9' } : {};
              },
            }}
            localization={{
              pagination: {
                labelDisplayedRows: this.i18n.translateToString('String_RowFromToCount'),
                labelRowsSelect: this.i18n.translateToString('String_Rows'),
                labelRowsPerPage: this.i18n.translateToString('String_RowsPerPage'),
                firstAriaLabel: this.i18n.translateToString('String_FirstPage'),
                firstTooltip: this.i18n.translateToString('String_FirstPage'),
                previousAriaLabel: this.i18n.translateToString('String_PrevPage'),
                previousTooltip: this.i18n.translateToString('String_PrevPage'),
                nextAriaLabel: this.i18n.translateToString('String_NextPage'),
                nextTooltip: this.i18n.translateToString('String_NextPage'),
                lastAriaLabel: this.i18n.translateToString('String_LastPage'),
                lastTooltip: this.i18n.translateToString('String_LastPage'),
              },
              toolbar: {
                nRowsSelected: this.i18n.translateToString('String_RowsSelected'),
                searchTooltip: this.i18n.translateToString('String_Search'),
                searchPlaceholder: this.i18n.translateToString('String_Search'),
                exportTitle: this.i18n.translateToString('String_Export'),
                exportCSVName: this.i18n.translateToString('String_ExportAs'),
              },
              body: {
                emptyDataSourceMessage: this.i18n.translateToString('String_NoData'),
              },
            }}
          />
        </Paper>
        <div>
          <CSVLink
            data={csvData}
            headers={csvHeader}
            filename="CSV_File.csv"
            ref={this.csvLink}
            target="_blank"
          />
        </div>
      </React.Fragment>
    );
  }
}

Any idea what I am doing wrong in the mapping? Or does material-table not like it when one use map to generate the column-array?

When I hard-code the columns with:

    ...
            columns={[
              {
                title: this.i18n.translateToString('Column_Application'),
                field: 'application',
                sorting: true,
                width: '100px',
                hidden: true,
              },
              {
                title: this.i18n.translateToString('Column_Filename'),
                field: 'filename',
                sorting: true,
                width: '270px',
                // eslint-disable-next-line react/display-name
                render: rowData =>
                  !!!rowData.filename ? (
                    <span>
                      <b>{this.i18n.translateToString('Column_Application')}: </b>
                      {rowData.application}
                    </span>
                  ) : (
                    rowData.filename
                  ),
              },
              {
                title: this.i18n.translateToString('Column_Location'),
                field: 'location',
                sorting: true,
              },
            ]}
    ...

everything works great. But I need to map the columns since the columns are dynamically generated depending on the received data.


Solution

  • The problem was, as is mostly the case, programmer-error. It took me quite a while to figure it out, but ended up coming right with the help from a colleague. With his guidance I finally managed to get a rough idea of where the problem originated.

    In the end I found that the system threw an error since it could not find the column name. The (much higher up) error displayed in the console was very misleading as it pointed to the use of an invalid (null) reference, which turned out not to be the case. I had inadvertently, for some unknown reason, not used the generated unique column name in the last step where I added each row's data, but a hard-coded column name. Easy to overlook, but biting very hard ...