For Everyone

Extending React Data Table’s Functionality with Custom Components

5 min read
CloudAnswers photo
CloudAnswers
Share
TODO

At CloudAnswers, we use the react-data-table-component for managing data in tables. It is quite good when it comes to pagination, sorting, and other important features. However, there were missing features in the original component, so we decided to add some functionality that we needed on top of it.

In this article, you’ll see how you can enhance the features and usability of the react-data-table-component as we did for some of our customer applications.

Expanding All Rows

In the original react-data-table-component repository, you can select all the rows at once with the “Select All” checkbox. However, you cannot expand all the rows if you want. To solve this issue, we created a fork of the original repository and edited its source code to allow users to expand/collapse all the rows at the same time.

If you want to use our fork, you can use the following props to configure the behavior for expandable rows:

  • keepExpandableFirst — By default, the select checkbox is shown before the expand button. So, in case, you want to reverse their order, you can use the keepExpandableFirst prop.
  • expandableRowsNoExpandAll— In case, you want to disable the “Expand All” button, you can pass the expandableRowsNoExpandAll prop to do so.

Adding Footer Component

The original react-data-table-component provides support for adding a header but not a footer. A footer is often used in a table to summarise the data in the table, for example, you can add a “totals” row in the footer if your table contains some numeric data. So, we added a footer prop to which you can pass any functional component.

If you want to use our fork of the original repository, you can use the footer in the following manner:


<ReactDataTable
 ...
 footer={
   <ReactDataTable
   ...
   />
 }
/>

Configuring Table Columns

As the application’s business complexity increases, the fields/columns in the tables increase. Due to this, the UI becomes a little more clunky and difficult to interpret. One solution for this problem is to allow users to hide/display columns in the tables based on their own needs.

For this purpose, you can create a component called TableColumnsConfigurator with the following code:


const TableColumnsConfigurator = ({
  columns,
  hiddenColumns,
  setHiddenColumns,
  className,
}) => {
  const columnsWithValidIDs = columns.filter(
    (column) => column.id && typeof column.id === "string"
  );

  const isColumnHidden = (column) => {
    return column.id && hiddenColumns.includes(column.id);
  };

  const updateHiddenColumns = (column) => {
    setHiddenColumns((prevValue) => {
      let _hiddenColumns = [...prevValue];
      if (column.id) {
        if (_hiddenColumns.includes(column.id)) {
          _hiddenColumns = _hiddenColumns.filter(
            (_hiddenColumn) => _hiddenColumn !== column.id
          );
        } else {
          _hiddenColumns = [..._hiddenColumns, column.id];
        }
      }
      return _hiddenColumns;
    });
  };

  const clearHiddenColumns = () => {
    setHiddenColumns([]);
  };

  const id = useUniqueId();

  return (
    <div className={cx("d-flex", className)}>
      <Dropdown alignRight>
        <OverlayTrigger
          overlay={
            <Tooltip id={id}>
              Edit Columns{" "}
              {hiddenColumns.length > 0 && (
                <div className="small">
                  ({hiddenColumns.length} hidden column
                  {hiddenColumns.length > 1 && "s"})
                </div>
              )}
            </Tooltip>
          }
        >
          <Dropdown.Toggle variant="transparent" className="p-0 text-muted">
            <i className="fas fa-list" />
            {hiddenColumns.length > 0 && (
              <div className="visual-indicator-danger" />
            )}
          </Dropdown.Toggle>
        </OverlayTrigger>
        <Dropdown.Menu className="py-0" style={{ minWidth: "200px" }}>
          {columnsWithValidIDs.length > 0 ? (
            <>
              <div className="dropdown-header d-flex justify-content-between border-bottom p-h">
                <div className="font-weight-bold">Edit Columns</div>
                <Button
                  variant="link"
                  className="p-0"
                  onClick={clearHiddenColumns}
                >
                  Show All
                </Button>
              </div>
              <div style={{ maxHeight: "300px", overflowY: "auto" }}>
                {columnsWithValidIDs.map((column, index) => (
                  <div
                    key={column.id}
                    className="dropdown-item d-flex justify-content-between px-h py-q"
                    style={{ opacity: isColumnHidden(column) ? 0.5 : 1 }}
                  >
                    <span className="pr-h">{column.name}</span>
                    <input
                      type="checkbox"
                      checked={!isColumnHidden(column)}
                      onClick={() => updateHiddenColumns(column)}
                      readOnly
                    />
                  </div>
                ))}
              </div>
            </>
          ) : (
            <div className="px-h">No columns available to show/hide.</div>
          )}
        </Dropdown.Menu>
      </Dropdown>
    </div>
  );
};

Next, you can call the TableColumnsConfigurator component in a sub-header component and pass this to the subHeaderComponent prop in the ReactDataTable:


const subHeaderComponent = () => {
  return (
    <div className="mb-0">
      {headerComponent}
      <div className="d-flex justify-content-end align-items-center py-q">
        <div className="d-flex">
          <TableColumnsConfigurator
            columns={columns}
            hiddenColumns={hiddenColumns}
            setHiddenColumns={setHiddenColumns}
          />
        </div>
      </div>
    </div>
  );
};

Exporting to CSV and PDF

A common request from users of business applications is the ability to export the data to CSV and PDF formats.

For this purpose, you can create a component called ExportDataDropdown with the following code:


const ExportDataDropdown = ({ columns, hiddenColumns, data }) => {
  const [fileName, setFileName] = useState(
    `report-${new Date().toLocaleDateString()}`
  );

  const csvFileName = useMemo(() => `${fileName}.csv`, [fileName]);
  const pdfFileName = useMemo(() => `${fileName}.pdf`, [fileName]);

  const handleFileNameChange = (e) => setFileName(e.target.value);

  const viewColumns = useMemo(
    () => columns.filter((column) => !hiddenColumns.includes(column.id ?? "")),
    [columns, hiddenColumns]
  );

  const handleExportToCSV = useCallback(() => {
    const headers = viewColumns.map((column) => column.name);
    const csvData = data.map((row) =>
      viewColumns.map((column) => {
        return column.selector?.(row) ?? "";
      })
    );

    // write your logic for CSV export
    exportCsv({
      name: csvFileName,
      headers,
      body: csvData,
    });
  }, [csvFileName, data, viewColumns]);

  const handleExportToPDF = useCallback(() => {
    const headers = viewColumns.map((column) => column.name);
    const pdfData = data.map((row, rowIndex) =>
      viewColumns.map((column, colIndex) => {
        return column.selector?.(row) ?? "";
      })
    );

    // write your logic for PDF export
    exportPdf({
      name: pdfFileName,
      headers,
      body: pdfData,
    });
  }, [data, pdfFileName, viewColumns]);

  return (
    <Dropdown alignRight>
      <Dropdown.Toggle variant="transparent" className="p-0 text-muted">
        Export
      </Dropdown.Toggle>
      <Dropdown.Menu className="p-h" style={{ minWidth: "200px" }}>
        <div className="d-flex flex-column">
          <Form.Group className="mb-h">
            <Form.Control
              type="text"
              placeholder="File Name"
              value={fileName}
              onChange={handleFileNameChange}
            />
            <Form.Text className="text-muted">
              Enter file name without file extension
            </Form.Text>
          </Form.Group>
          <DropdownButton
            as={ButtonGroup}
            variant="outline-primary"
            title="Export As"
            alignRight
          >
            <Dropdown.Item onClick={handleExportToPDF}>
              Export as PDF
            </Dropdown.Item>
            <Dropdown.Item onClick={handleExportToCSV}>
              Export as CSV
            </Dropdown.Item>
          </DropdownButton>
        </div>
      </Dropdown.Menu>
    </Dropdown>
  );
};

Next, you can call the ExportDataDropdown component in a sub-header component and pass this to the subHeaderComponent prop in the ReactDataTable:


const subHeaderComponent = () => {
  return (
    <div className="mb-0">
      {headerComponent}
      <div className="d-flex justify-content-end align-items-center py-q">
        <div className="d-flex">
          <ExportDataDropdown
            columns={columns}
            hiddenColumns={hiddenColumns}
            data={data}
          />
        </div>
      </div>
    </div>
  );
};

Persisting User Settings

When your invest time in customizing columns and filters, you will expect the application to remember your preferences on page reload or when you return to the application later in the day.

This can easily be accomplished by storing user settings in the browser’s local storage. You can replace the use of the useState hook with the useLocalStorage custom hook. It works in the same way as useState works but allows you to save your state variables in local storage.

For example, you might want to persist the filename used in the ExportDataDropdown component in the local storage. For this, you can use the following code:


const ExportDataDropdown = () => {
  const [fileName, setFileName] = useLocalStorage('fileName', `report-${new Date().toLocaleDateString()}`);
  ...
};

The first parameter is the “key” and the second parameter is the default value in case the key doesn’t exist in local storage.

Conclusion

As a developer, there is always the temptation to create a new component from scratch, but extending an existing component that already solves 90% of your needs is a far more efficient approach. Here we are standing on the shoulders of giants, by starting with the react-data-table component. We’ve opened a pull request for them to include these features but if you want to use our version today, you can find it at https://github.com/cloudanswers/react-data-table-component.


CloudAnswers photo
CloudAnswers
Share

About CloudAnswers

Salesforce apps, powerful components, custom development, and consulting. Our experienced team helps you to create and modify workflow processes in salesforce.

Related Articles

For Everyone

Product Launch: CloudAnswers Shop Builder

Are you looking for an easy way to launch an ecommerce shop? Our new app, Shop Builder, is now in public beta! We’re looking for companies that want to build an online shop but don’t want to spend thousands building it out.

April 12, 2024

5 Min Read

For Everyone

A Day in the Life of a Project Manager at CloudAnswers

I'm Emily, and I've been a project manager at CloudAnswers for the last two years. It can be a nebulous role, but I like to say I act as a bridge between the product vision and tangible results, whether that is building a custom app for a client or one of our own Salesforce products. My typical day revolves around managing tasks, ensuring progress, and maintaining standards while adhering to project timelines.

March 22, 2024

5 Min Read

For Everyone

Create a Custom Lightning Component for Alert and Confirm Dialogs

As front-end developers, there can be numerous instances when we need to display a prompt, a confirm dialog, or a confirm dialog with some input control to accept some value from the user. If you happen to be a Salesforce developer reading this article, you must have had such a requirement while writing lightning components.

March 4, 2024

6 Min Read