09Sep
Create simple POS with React.js, Node.js, and MongoDB #14: Export PDF, Excel, CSV, Bulk Delete, Inline Editing
Create simple POS with React.js, Node.js, and MongoDB #14: Export PDF, Excel, CSV, Bulk Delete, Inline Editing

Defenition: POS – “Point of Sale”. At the point of sale, the merchant calculates the amount owed by the customer, indicates that amount, may prepare an invoice for the customer (which may be a cash register printout), and indicates the options for the customer to make payment.

In the previous chapter, we integrated grouping and sorting features to our react table along with pagination. These are some basic feature that makes out Table component intuitive and efficient.

In this chapter, we are going to continue from where we left off from the previous chapter. We are about to add more features to our react table to make it feature-rich and dynamic. Here, we are going to add features like document export, bulk delete, and inline editing. This will be a walkthrough tutorial for every important feature that a table should have. This may help us in other projects as well while implementing dynamic table components.

The idea is to start with the implementation of document export in which we are going to add a feature to export the table data in the form of PDF, EXCEL, or CSV. Then, we are going to move on to bulk data deletion and inline editing for which we need to add a backend API endpoint as well.

So, let’s get started!

Exporting to PDF, Excel, CSV

We are going to start by implementing a feature that allows us to export our table data in the form of PDF, EXCEL, or CSV. The react table package doesn’t have a built-in module for this feature. Hence, a third party plugin named react-table-plugin is required for this purpose.

So, we need to install the react-table-plugin package along with all its required dependencies. For that, we need to run the following command in our project terminal:

yarn add react-table-plugin papaparse xlsx jspdf jspdf-autotable

Then, we need to import the required components from the react-table-plugin package and dependent packages in our Table Component as directed in the code snippet below:

import {
 useExportData
} from 'react-table-plugins'
import Papa from "papaparse";
import XLSX from "xlsx";
import JsPDF from "jspdf";
import "jspdf-autotable";

Next, we need to create a function named getExportFileBlob to handle the file creation. We are going to pass columns, data, fileType and fileName parameter object to the function. The overall implementation of this function is provided in the code snippet below:

function getExportFileBlob({ columns, data, fileType, fileName }) {
        if (fileType === "csv") {
            // CSV example
            const headerNames = columns.filter((c) => c.Header != 'Action').map((col) => col.exportValue);
            const csvString = Papa.unparse({ fields: headerNames, data });
            return new Blob([csvString], { type: "text/csv" });
        } else if (fileType === "xlsx") {
            // XLSX example

            const header = columns.filter((c) => c.Header != 'Action').map((c) => c.exportValue);
            const compatibleData = data.map((row) => {
                const obj = {};
                header.forEach((col, index) => {
                    obj[col] = row[index];
                });
                return obj;
            });

            let wb = XLSX.utils.book_new();
            let ws1 = XLSX.utils.json_to_sheet(compatibleData, {
                header,
            });
            XLSX.utils.book_append_sheet(wb, ws1, "React Table Data");
            XLSX.writeFile(wb, `${fileName}.xlsx`);

            // Returning false as downloading of file is already taken care of
            return false;
        }
        //PDF example
        if (fileType === "pdf") {
            const headerNames = columns.filter((c) => c.Header != 'Action').map((column) => column.exportValue);
            const doc = new JsPDF();
            doc.autoTable({
                head: [headerNames],
                body: data,
                styles: {
                    minCellHeight: 9,
                    halign: "left",
                    valign: "center",
                    fontSize: 11,
                },
            });
            doc.save(`${fileName}.pdf`);

            return false;
        }

        // Other formats goes here
        return false;
    }

Here, we have used the components from the dependent packages and fileType variable to decide which file format to create and export and finally we are saving the document using the save method.

Now, we need to pass the plugin and custom function to our Table component using useTable hook as directed in the code snippet below:

const {
        getTableProps,
        getTableBodyProps,
        headerGroups,
        prepareRow,
        setFilter,
        page, // Instead of using 'rows', we'll use page,
        // which has only the rows for the active page
        exportData,
        // The rest of these things are super handy, too ;)
        canPreviousPage,
        canNextPage,
        pageOptions,
        pageCount,
        gotoPage,
        nextPage,
        previousPage,
        setPageSize,
        selectedFlatRows,
        state: { pageIndex, pageSize, selectedRowIds },
    } = useTable(

        {
            columns,
            data,
            defaultColumn,
            updateMyData, skipPageReset,
            initialState: { pageIndex: 2 },
            getExportFileBlob,
        }, useFilters, useGroupBy, useSortBy, useExpanded, usePagination, useExportData,

Now, we need buttons that trigger the function to create and export the file in the required format. We will implement the button components at the top of our Table component as shown in the code snippet below:

<div className="form-group input-group">
                <button class="btn btnexport mr-1"
                    onClick={() => {
                        exportData("csv", true);
                    }}
                ><i class="fa fa-file-csv"></i>{' '}
                  Export  as CSV
      </button>{' '}
                <button class="btn btnexport mr-1"
                    onClick={() => {
                        exportData("xlsx", true);
                    }}
                ><i class="fa fa-file-excel"></i>{' '}
                Export  as xlsx
      </button>{' '}
                <button class="btn btnexport mr-1"
                    onClick={() => {
                        exportData("pdf", true);
                    }}
                ><i class="fa fa-file-pdf"></i>
                  Export as PDF
      </button>
</div>

Here, each button has its ownexportData function on its onClick event. Along with function calls, we are also passing the required format based on buttons.

Hence, we will get the result as shown in the demo below:

Export pdf result`
Export pdf result

As we can see, as soon as we click on the ‘Export as PDF’ button, a dialog box appears which enables us to save our created PDF to our local drive.

This marks the completion of our document export feature in the react table. Now, we are going to move on to our next features which are bulk data delete and inline entry editing.

Bulk Delete

The bulk delete operation refers to the deletion of multiple selected rows from the react table. For the selection of columns, we are going to add a checkbox to each column and use the useRowSelect hook for checking and selecting the row to be deleted. We need to implement the frontend feature and backend feature for this operation.

First, we are going to start with the frontend.

Frontend

Here, we need to import useRowSelecthook from react-table as highlighted in the code snippet below:

import { useTable, useFilters, useGroupBy, useExpanded, usePagination, useSortBy, useRowSelect } from "react-table";

Next, we need to create a function named IndeterminateCheckbox in order to handle the checkbox selection. The implementation of this function is provided in the code snippet below:

const IndeterminateCheckbox = React.forwardRef(
    ({ indeterminate, ...rest }, ref) => {
        const defaultRef = React.useRef()
        const resolvedRef = ref || defaultRef

        React.useEffect(() => {
            resolvedRef.current.indeterminate = indeterminate
        }, [resolvedRef, indeterminate])

        return (
            <>
               <input type="checkbox" ref={resolvedRef} {...rest} />
            </>
        )
    }
)

Now, we need to create a custom column to display the checkboxes as directed in the code snippet below:

const {
        getTableProps,
        getTableBodyProps,
        headerGroups,
        prepareRow,
        setFilter,
        page, // Instead of using 'rows', we'll use page,
        // which has only the rows for the active page
        exportData,
        // The rest of these things are super handy, too ;)
        canPreviousPage,
        canNextPage,
        pageOptions,
        pageCount,
        gotoPage,
        nextPage,
        previousPage,
        setPageSize,
        selectedFlatRows,
        state: { pageIndex, pageSize, selectedRowIds },
    } = useTable(

        {
            columns,
            data,
            defaultColumn,
            updateMyData, skipPageReset,
            initialState: { pageIndex: 2 },
            getExportFileBlob,
        }, useFilters, useGroupBy, useSortBy, useExpanded, usePagination, useExportData, useRowSelect,
        hooks => {
            hooks.visibleColumns.push(columns => [
                // Let's make a column for selection
                {
                    id: 'selection',
                    // accessor: '_id',
                    // The header can use the table's getToggleAllRowsSelectedProps method
                    // to render a checkbox
                    Header: ({ getToggleAllRowsSelectedProps }) => (
                        <div>
                            <IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} />
                        </div>
                    ),
                    // The   cell can use the individual row's getToggleRowSelectedProps method
                    // to the render a checkbox
                    Cell: ({ row }) => (
                        <div>
                            <IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
                        </div>
                    )

                },
                ...columns,
            ])
        }
    );

Hence, we will get the checkbox column as displayed in the demo screenshot below:

Check box column
Check box column

As we can see, we have got the checkbox column using which we can select the table rows to be deleted.

Now, we add a new button to trigger function for bulk delete operation. We are going to use selectedRowIds data to toggle the checkboxes and send the selected row data to another function. The code for this is provided in the code snippet below:

{(Object.keys(selectedRowIds).length != 0) ?
                    <button class="btn btn-danger"
                        onClick={() => {
                        BulkDelete(selectedFlatRows, parent_action);
                        }}
                    ><i class="fa fa-trash"></i>
                  Delete {Object.keys(selectedRowIds).length} row
      </button> : ''}

Hence, we will get the result as displayed in the following demo:

Select multiple row in react table
Select multiple row in react table

Next, we need to pass the action data from the parent component to trigger the bulk delete function. The implementation of BulkDelete function is provided in the code snippet below:

const BulkDelete = (selectedFlatRows, parent_action) => {
        let selected_id = selectedFlatRows.map(data => {
            return data.values._id
        })
        swal({
            title: "Are you sure?",
            text: "Once deleted, you will not be able to recover this data!",
            icon: "warning",
            buttons: true,
            dangerMode: true,
        }).then((willDelete) => {
            if (willDelete) {
                dispatch(parent_action.bulk_delete(selected_id))
                swal("Poof! Your POS Machine data has been deleted!", {
                    icon: "success",
                });
            }
        });
    }

Here, we have passed the selectedFlatRows and parent_action data as a parameter to the function. Then, we map through the data and store the ids in the selected_id variable. Lastly, by using swal components we display the alert dialog for deletion and dispatch the parent action for bulk deletion upon affirmative delete signal from the user.

Remember to import dispatch method from redux.

Now, we need to import the useSelector and useDispatch hooks from react-redux package as directed in the code snippet below:

import { useSelector, useDispatch } from "react-redux";

Next, we need to pass the new action from the parent component to the Table component as highlighted in the code snippet below:

const Holdon = (columns) => {
        if (posmachineReducer.result) {
            return <Table columns={columns} data={posmachineReducer.result}
                parent_action={posmachineActions}
                updateMyData={updateMyData}
                skipPageReset={skipPageReset}
            />
        } else {
            return <img class="img-fluid img-rounded"
                src={loading} width="30%" />
        }
    }

Then, we need to create a new action to interact with the backend. Nothing is special about this process, we just need to change the endpoint URI as highlighted in the code snippet below:

export const bulk_delete = (id) => {
  return async (dispatch) => {
    dispatch(setPOSMachineStateToFetching());
    const response = await httpClient.delete(
      process.env.REACT_APP_API_URL + "pos_machine/bulk_delete/" + id
    );
    if (response.data.result == "success") {
      dispatch(setPOSMachineStateToSuccess());
      dispatch(index());
    } else if (response.data.result === "error") {
      dispatch(setPOSMachineStateToFailed());
      swal("Error!", response.data.message, "error");
    }
  };
};

Now, let’s move on to the creation of backend API for bulk deletion.

Backend

The implementation of backend API for bulk delete operation is quite simple. We just need to copy the code from the delete function that we have created before and replace some of its coding expressions with the code provided in the code snippet below:

router.delete("/pos_machine/bulk_delete/:id", async (req, res) => {
  let id = req.params.id.split(",");
  console.log(id)
  try {
  let response = await POS_Machine.deleteMany({ _id: { $in: id } }, function (err, data) {
    if (err) {
      console.log(err);
    } else {
      res.json({
        result: "success",
        message: "Bulk Delete Successfully",
        data: data,
      });
    }
  });

  } catch (err) {
    res.json({ result: "error", message: err.msg });
  }
});

Here, we need to fetch the ids sent from the frontend. The received ids are in the raw string format. Hence, we need to convert them into an array using split method. Then, we need to use deleteMany method from the POS_Machine module and pass the array of ids as a parameter to the method. This completes our implementation of API endpoint for bulk delete operation in the backend as well.

Hence, we will get the result as displayed in the following demo:

Bulk delete result
Bulk delete result

As we can notice, we can now select the rows that we want to delete and trigger the delete operation from dialog to delete the selected rows.

Inline Editing

The Inline editing is quite simple in the react table for the frontend part as it itself provides a function to implement it. We just need to add an API endpoint to update the function and we are done.

Frontend

First, we need to create a function to transform all cells in the table to the input type. This function will handle the input change and send the updated data to the parent component. The function is named EditableCell and its implementation is provided in the code snippet below:

const EditableCell = ({
        value: initialValue,
        row: { values },
        column: { id },
        updateMyData, // This is a custom function that we supplied to our table instance
    }) => {
        // We need to keep and update the state of the cell normally
        const [value, setValue] = React.useState(initialValue)

        const onChange = e => {
            setValue(e.target.value)
        }

        // We'll only update the external data when the input is blurred
        const onBlur = () => {
            updateMyData(values._id, id, value)
        }

        // If the initialValue is changed external, sync it up with our state
        React.useEffect(() => {
            setValue(initialValue)
        }, [initialValue])

        return <input value={value} onChange={onChange} onBlur={onBlur} />
    }

Then, we need to create a function to assign new cell type as directed in the code snippet below:

const defaultColumn = {
 Cell: EditableCell,
}

Next, we need to assign it to the main function in useTable hook as directed in the code snippet below:

= useTable(

        {
            columns,
            data,
            defaultColumn,
            updateMyData,
            initialState: { pageIndex: 2 },
            getExportFileBlob,
        },

Now, we get the input field in each table cell for the inline edition. It enables us to change or update the value in the table cell directly making the table more convenient to use.

The result is displayed in the following demo:

Change text cell to input cell
Change text cell to input cell

Now, in order to handle the change in table cell data, we assign a new function from the parent component.

In the parent component, we need to create a new function to handle data change. Here, we want to handle the following three data:

  1. id of database row
  2. column name
  3. value

These three data types are required to make an update on the database. When we get the data from the Table component, we will restructure and send to action as directed in the code snippet below:

const updateMyData = (rowIndex, columnId, value) => {
      let values = { _id: rowIndex, column: columnId, value: value }
        dispatch(posmachineActions.inline_update(values));

    }

Next, we pass the updateMyData function to the Table component as a prop as highlighted in the code snippet below:

const Holdon = (columns) => {
        if (posmachineReducer.result) {
            return <Table columns={columns} data={posmachineReducer.result}
                parent_action={posmachineActions}
                updateMyData={updateMyData}
            />
        } else {
            return <img class="img-fluid img-rounded"
                src={loading} width="30%" />
        }
    }

Next, we need to update the Table function in Table component and import the updateMyData function from props:

export default function Table({ columns, data, parent_action, updateMyData}) {

Then, we need to assign the subsequent function to our useTable hook as highlighted in the code snippet below:

= useTable(

        {
            columns,
            data,
            defaultColumn,
            updateMyData,
            initialState: { pageIndex: 2 },
            getExportFileBlob,
        },

Now, we are done with gathering updated data from the table. Next, we need to create a new action to send updated data to the server. The function is named inline_update and its implementation is provided in the code snippet below:

export const inline_update = (values, history) => {
  return async (dispatch) => {
    // dispatch(setPOSMachineStateToFetching());
    const response = await httpClient.put(
      process.env.REACT_APP_API_URL + "pos_machine/inline_update",
      values
    );
    if (response.data.result == "success") {
      // dispatch(setPOSMachineStateToClear());
    } else if (response.data.result === "error") {
      // dispatch(setPOSMachineStateToFailed());
      swal("Error!", response.data.message, "error");
    }
  };
};

Backend

For the Backend part, we need to implement a new API endpoint to handle the update. We need to accept the data from the frontend and make changes to the specific data using id and column value. The implementation of the API endpoint is provided in the code snippet below:

router.put("/pos_machine/inline_update", async (req, res) => {
  let id = req.body._id
  let column = req.body.column
  let value = req.body.value
  // console.log(column)
  try {
    let doc = await POS_Machine.update(
      { _id: id }, { $set: { [column]: value } }, function (err, result) {

        if (err) {
          res.json({ result: "error", message: err.msg });
        }
        else {
          res.json({
            result: result,
            message: "Update POS data Successfully",
          });
        }

      }
    );

  } catch (err) {
    res.json({ result: "error", message: err.msg });
  }
});

Now, we are done with implementing backend functionality for inline editing as well.

Hence, we will get the result as shown in the demo below:

Inline editable result
Inline editable result

As we can see, we can now directly change or update any table cell value directly from the input field at that cell.

Finally, we have successfully implemented the three major features in our React table. The document export, bulk deletion, and inline editing are some of the most useful features for any table that makes the table feature-rich and efficient for users.

Conclusion

As if the previous chapter to make our react table more power-packed was enough, we made the table more feature-rich and dynamic in this chapter. Some of the popular and convenient table features like bulk deletion and inline editing were added. These features make it easier to perform delete operation and updating of the table values.

Not to forget the document export feature available in three formats PDF, EXCEL, and CSV. This feature enables us to download the table value in the report formats and share it with others. One of the most essential features available in almost all the data and table-based applications nowadays. The stepwise guidance to implement these features will surely help not only to complete this chapter but also in other projects as well.

In the next chapter, we will secure our app for role-based access control to handle employees.

The coding implementations used in this tutorial chapter are available on Github for Frontend and Backend.

See you in the next chapter! Good day folks!

Developer Relation @instamobile.io

Leave a Reply