/* React component to render a table with data and columns props, row are sortable, searchable and have pagination */
import { useState, useEffect } from "react";
import PropTypes from "prop-types";
import Pagination from "../Pagination/index.jsx";
import "./styles.scss";
import sort from "../../assets/sort_both.png";
import sortAsc from "../../assets/sort_asc.png";
import sortDesc from "../../assets/sort_desc.png";
/**
* Takes in data, columns and returns a table that can be sorted (if the column is declared sortable),
* filtered (can be disable), and paginated (can be disable).
* data and columns must be arrays of object.
*
* data must be an array of objects, each object will be a row in the table, the keys in the objects must be the id of the column.
* [{ idOfColumnOne: "dataOfColumnOne", idOfColumnTwo: "dataOfColumnTwo", idOfColumnThree: "dataOfColumnThree", ...},...]
*
* columns must be an array of objects with the following properties:
* [{ name: "First Name", id: "firstName", sortable: true, headColSpan: 2, bodyColSpan: 2 },...]
* name: the name of the column displayed in the table header.
* id: the id of the column
* sortable: boolean, if the column is sortable
* headColSpan: size of the column in the header
* bodyColSpan: size of the column in the body
*
* @category Components
* @component
* @returns {React.Component} - The table component.
*/
function DataTable({
data,
columns,
tableId,
sortId = columns[0].id,
itemsPerPageOptions = [10, 25, 50, 100],
itemsPerPageSelectionEnabled = true,
searchEnabled = true,
sortSelectionEnabled = true,
tableInfoEnabled = true,
paginationEnabled = true,
// Class names for the component.
dataTablesWrapperClassName = "data-tables-wrapper",
dataTablesLengthClassName = "data-tables-length",
dataTablesLengthLabelClassName = "data-tables-length-label",
dataTablesLengthSelectClassName = "data-tables-length-select",
dataTablesLengthOptionClassName = "data-tables-length-option",
dataTablesSearchWrapperClassName = "data-tables-search-wrapper",
dataTablesSearchLabelClassName = "form-label",
dataTablesSearchInputClassName = "form-input",
dataTablesInfoClassName = "data-tables-info",
dataTablesPaginateClassName = "data-tables-paginate",
dataTableClassName = "data-table",
dataTableHeaderClassName = "data-table-header",
dataTableHeaderTrClassName = "data-table-header-tr",
dataTableHeaderThClassName = "data-table-header-th",
dataTableHeaderSortedClassName = "sorting",
dataTableBodyClassName = "data-table-body",
dataTableBodyTrClassName = "data-table-body-tr",
dataTableBodyTdClassName = "data-table-body-td",
dataTableBodyTdSortedClassName = "sorting_1",
dataTableBodyTdEmptyClassName = "data-tables-empty",
dataTableBodyOddRowClassName = "odd",
dataTableBodyEvenRowClassName = "even",
}) {
/**
* It sorts the data by date if the value is a valid date, otherwise it sorts the data by string
* @returns the sorted data.
*/
const sortData = (data, column, direction) => {
/**
* Checks if the value is a valid date.
* @returns a boolean value.
*/
const isDate = (value) => {
return !isNaN(Date.parse(value));
};
// Create a copy of the data to sort.
const dataCopy = [...data];
if (dataCopy && dataCopy.length > 0) {
return dataCopy.sort((a, b) => {
// Sorting the data by date.
if (isDate(a[column]) && isDate(b[column])) {
const dateA = new Date(a[column]);
const dateB = new Date(b[column]);
return direction === "asc" ? dateA - dateB : dateB - dateA;
} // If the value is not a valid date, it will sort the data by string.
else {
const stringA = String(a[column]).toLowerCase();
const stringB = String(b[column]).toLowerCase();
return direction === "asc" ? stringA > stringB : stringB > stringA;
}
});
}
};
// State to track the current currentPage.
const [currentPage, setCurrentPage] = useState(1);
// State to track the number of items per page.
const [itemsPerPage, setItemsPerPage] = useState(itemsPerPageOptions[0]);
// State to track the user's searchInput query.
const [searchInput, setSearchInput] = useState("");
// State to track the current sort column.
const [sortColumn, setSortColumn] = useState(sortId);
// State to track the current sort direction.
const [sortOrder, setSortOrder] = useState("asc");
// State to track the first item of the current page.
const [firstItemOnPage, setFirstItemOnPage] = useState(1);
// State to track the last item of the current page.
const [lastItemOnPage, setLastItemOnPage] = useState(itemsPerPageOptions[0]);
// State to store the data that matches the searchInput query.
const [filteredData, setFilteredData] = useState(data);
// State to store the data that matches the searchInput query and are sorted.
const [sortedData, setSortedData] = useState(filteredData);
// State to store the data that matches the searchInput query, are sorted on the current page.
const [paginatedData, setPaginatedData] = useState(sortedData);
// State to track the count of all items that match the searchInput query.
const [totalFilteredItemsCount, setTotalFilteredCount] = useState(filteredData.length);
// Variable to track the count of all items.
const totalItemsCount = data.length;
useEffect(() => {
if (filteredData.length > 0) {
setSortedData(sortData(filteredData, sortColumn, sortOrder));
} else {
setSortedData([]);
}
}, [filteredData, sortColumn, sortOrder]);
useEffect(() => {
// Calculating the first item on the current page.
const firstItemOnPage = (currentPage - 1) * itemsPerPage;
// Checking if the last item on the page is less than the length of the sorted data. If it is, it will
// set the last item on the page to the first item on the page plus the number of items per page. If it
// is not, it will set the last item on the page to the length of the sorted data.
const lastItemOnPage = firstItemOnPage + itemsPerPage < sortedData.length ? firstItemOnPage + itemsPerPage : sortedData.length;
setFirstItemOnPage(firstItemOnPage + 1);
setLastItemOnPage(lastItemOnPage);
setPaginatedData(sortedData.slice(firstItemOnPage, lastItemOnPage));
// If the first item on the page is greater than the length of the sorted data, it will set the
// current page to 1.
if (firstItemOnPage > sortedData.length) {
setCurrentPage(1);
}
}, [filteredData, sortedData, sortColumn, sortOrder, currentPage, itemsPerPage]);
// Check if pagination is enabled, if not, the items per page is set to the length of the data.
useEffect(() => {
if (!paginationEnabled) {
setItemsPerPage(data.length);
}
}, [paginationEnabled, data]);
/** Takes the search input and filters the data based on the search input */
const handleSearch = (event) => {
const searchValue = event.target ? event.target.value : event;
setSearchInput(searchValue);
let filteredData = [...data];
const searchValueLowerCase = searchValue.toLowerCase();
if (searchValueLowerCase) {
filteredData = data.filter((item) => {
return Object.values(item).some((value) => {
return String(value).toLowerCase().includes(searchValueLowerCase);
});
});
}
setFilteredData(filteredData);
setTotalFilteredCount(filteredData.length);
};
/**
* If the column is the same as the current sort column, then toggle the sort order.
* Otherwise, set the sort order to ascending
*/
const handleSortChange = (column) => {
const currentColumn = columns.find((col) => col.id === column);
// Check if the column is sortable
if (currentColumn.sortable) {
let direction = "asc";
if (column === sortColumn) {
direction = sortOrder === "asc" ? "desc" : "asc";
}
setSortOrder(direction);
setSortColumn(column);
}
return;
};
/** Sets the items per page to the value of the event target and sets the current page to 1. */
const handleChangeItemsPerPage = (event) => {
setItemsPerPage(+event.target.value);
setCurrentPage(1);
};
return (
<div id={tableId + "-table-wrapper"} className={dataTablesWrapperClassName}>
{itemsPerPageSelectionEnabled && paginationEnabled && (
<div id={tableId + "-table-length"} className={dataTablesLengthClassName}>
<label className={dataTablesLengthLabelClassName}>
{"Show "}
<select className={dataTablesLengthSelectClassName} name={tableId + "-table-length"} aria-controls={tableId + "-table"} value={itemsPerPage} onChange={handleChangeItemsPerPage}>
{itemsPerPageOptions.map((option) => (
<option key={option} value={option} className={dataTablesLengthOptionClassName}>
{option}
</option>
))}
</select>
{" entries"}
</label>
</div>
)}
{searchEnabled && (
<div id={tableId + "-table-search-wrapper"} className={dataTablesSearchWrapperClassName}>
<label id={tableId + "-table-search-input-label"} htmlFor={tableId + "-table-search-input"} className={dataTablesSearchLabelClassName}>
Search:{" "}
</label>
<input
id={tableId + "-table-search"}
className={dataTablesSearchInputClassName}
name={tableId + "-table-search"}
type="search"
maxLength="128"
aria-controls={tableId + "-table"}
value={searchInput}
onChange={handleSearch}
aria-label={"Search in table " + tableId}
/>
</div>
)}
{sortSelectionEnabled && (
<div className="only-mobile">
{"Sort by: "}
<select value={sortColumn} onChange={(e) => setSortColumn(e.target.value)}>
{columns.map((option, index) => (
<option key={option + "-" + index} value={option.id}>
{option.name}
</option>
))}
</select>
<select value={sortOrder} onChange={(e) => setSortOrder(e.target.value)}>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</div>
)}
{paginationEnabled && (
<div id={tableId + "-table-paginate-top"} className={dataTablesPaginateClassName + " only-mobile"}>
<Pagination totalCount={totalFilteredItemsCount} pageSize={itemsPerPage} siblingCount={1} currentPage={currentPage} onPageChange={setCurrentPage} />
</div>
)}
<table id={tableId + "-table"} className={dataTableClassName} role="grid" aria-describedby="employee-table-info">
<thead className={dataTableHeaderClassName}>
<tr className={dataTableHeaderTrClassName}>
{columns.map((column) => (
<th
key={column.id}
// If the column is sortable, then it will add the sortable class. If the column is sorted, then it will add the sorted class.
className={
dataTableHeaderThClassName + (column.sortable && column.id !== sortColumn ? " " + dataTableHeaderSortedClassName : "") + (column.id === sortColumn ? " sorting-" + sortOrder : "")
}
tabIndex={0}
onClick={() => handleSortChange(column.id)}
// onKeyDown enter key
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
handleSortChange(column.id);
}
}}
scope="col"
rowSpan="1"
colSpan={column.headColSpan ? column.headColSpan : 1}
aria-label={"Sort table by " + column.name + " in " + (column.id === sortColumn ? (sortOrder === "asc" ? "descending" : "ascending") : "ascending") + " order"}
>
{column.name}
{column.sortable && (
<img
src={column.id === sortColumn ? (sortOrder === "asc" ? sortAsc : sortDesc) : sort}
alt={column.id === sortColumn ? (sortOrder === "asc" ? "Sort ascending logo" : "Sort descending logo") : "Sort logo"}
/>
)}
</th>
))}
</tr>
</thead>
<tbody className={dataTableBodyClassName}>
{paginatedData.length === 0 && (
<tr className={dataTableBodyTrClassName + " " + dataTableBodyOddRowClassName}>
<td colSpan={columns.length} className={dataTableBodyTdClassName + dataTableBodyTdEmptyClassName} valign="top">
No data available in table
</td>
</tr>
)}
{paginatedData.length > 0 &&
paginatedData.map((row, index) => (
<tr
key={index}
// If the row is odd, add the odd row class name, otherwise add the even row class name
className={dataTableBodyTrClassName + (index % 2 ? " " + dataTableBodyEvenRowClassName : " " + dataTableBodyOddRowClassName)}
>
{columns.map((column) => (
<td
key={column.id}
// If the column is sorted, add the sorted class name
className={dataTableBodyTdClassName + " " + (column.id + (column.id === sortColumn ? " " + dataTableBodyTdSortedClassName : ""))}
colSpan={column.bodyColSpan ? column.bodyColSpan : 1}
data-label={column.name}
aria-label={row[column.id] + " in " + column.name + " column"}
>
{row[column.id]}
</td>
))}
</tr>
))}
</tbody>
</table>
{tableInfoEnabled && (
<div id={tableId + "-table-info"} className={dataTablesInfoClassName} role="status" aria-live="polite">
{`Showing ${paginatedData.length === 0 ? 0 : firstItemOnPage} to ${lastItemOnPage} of ${totalFilteredItemsCount} entries`}
{totalItemsCount !== totalFilteredItemsCount && " (filtered from " + totalItemsCount + " total entries)"}
</div>
)}
{paginationEnabled && (
<div id={tableId + "-table-paginate-footer"} className={dataTablesPaginateClassName}>
<Pagination totalCount={totalFilteredItemsCount} pageSize={itemsPerPage} siblingCount={1} currentPage={currentPage} onPageChange={setCurrentPage} />
</div>
)}
</div>
);
}
DataTable.propTypes = {
/** The data to be displayed in the table */
data: PropTypes.arrayOf(PropTypes.object).isRequired,
/** The columns to be displayed in the table */
columns: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, sortable: PropTypes.bool, headColSpan: PropTypes.number, bodyColSpan: PropTypes.number })).isRequired,
/** The id of the table */
tableId: PropTypes.string.isRequired,
/** The id of the column to be sorted */
sortId: PropTypes.string,
/** The options for the items per page dropdown */
itemsPerPageOptions: PropTypes.array,
/** Whether the items per page dropdown should be enabled */
itemsPerPageSelectionEnabled: PropTypes.bool,
/** Whether the search input should be enabled */
searchEnabled: PropTypes.bool,
/** Whether the sort dropdown should be enabled */
sortSelectionEnabled: PropTypes.bool,
/** Whether the table info should be enabled */
tableInfoEnabled: PropTypes.bool,
/** Whether the pagination should be enabled */
paginationEnabled: PropTypes.bool,
/** The class name of the wrapper div */
dataTablesWrapperClassName: PropTypes.string,
/** The class name of the length div */
dataTablesLengthClassName: PropTypes.string,
/** The class name of the length label */
dataTablesLengthLabelClassName: PropTypes.string,
/** The class name of the length select */
dataTablesLengthSelectClassName: PropTypes.string,
/** The class name of the length options */
dataTablesLengthOptionClassName: PropTypes.string,
/** The class name of the search wrapper div */
dataTablesSearchWrapperClassName: PropTypes.string,
/** The class name of the search label */
dataTablesSearchLabelClassName: PropTypes.string,
/** The class name of the search input */
dataTablesSearchInputClassName: PropTypes.string,
/** The class name of the info div */
dataTablesInfoClassName: PropTypes.string,
/** The class name of the paginate div */
dataTablesPaginateClassName: PropTypes.string,
/** The class name of the table */
dataTableClassName: PropTypes.string,
/** The class name of the table header */
dataTableHeaderClassName: PropTypes.string,
/** The class name of the table header tr */
dataTableHeaderTrClassName: PropTypes.string,
/** The class name of the table header th */
dataTableHeaderThClassName: PropTypes.string,
/** The class name of the table header th sorted */
dataTableHeaderSortedClassName: PropTypes.string,
/** The class name of the table body */
dataTableBodyClassName: PropTypes.string,
/** The class name of the table body tr */
dataTableBodyTrClassName: PropTypes.string,
/** The class name of the table body td */
dataTableBodyTdClassName: PropTypes.string,
/** The class name of the table body td sorted */
dataTableBodyTdSortedClassName: PropTypes.string,
/** The class name of the table body td empty */
dataTableBodyTdEmptyClassName: PropTypes.string,
/** The class name of the table body tr odd */
dataTableBodyOddRowClassName: PropTypes.string,
/** The class name of the table body tr even */
dataTableBodyEvenRowClassName: PropTypes.string,
};
export default DataTable;
Source