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:
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 useRowSelect
hook 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:
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:
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:
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:
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:
- id of database row
- column name
- 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:
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!