vyas-react-table
Version:
A simple yet highly configurable table component
275 lines (251 loc) • 8.66 kB
JavaScript
import { useState, useEffect } from "react";
import "./Table.css";
// utils
import { checkInclusion } from "../../utils/filter.utils";
import { downloadData } from "../../utils/download.utils";
/**
*
* @author tanishqvyas
* @param tableName - Name of the table. Defaults to "Data"
* @param {Array} data An array of objects, where each object is a row of the table. This is required.
* @param {Object} tableHeaders An object with the headers & their configuration. This is required.
* @param {Boolean} [allowFilters] Whether to allow filtering or not. Default is true.
* @param {Boolean} [showSerialNo] Whether to show serial number or not. Default is false.
* @param {String} [borderSpacing] The spacing between the cells. Default is "4px".
* @param {Number} [itemsPerPage] The number of items to be shown per page. Default is 10.
* @param {Boolean} [allowDownload] Whether to allow downloading or not. Default is false.
* @param {String} [filename] The name of the file. Defaults to "data.csv"
* @param {String} [nullDataPlaceholder] The placeholder to be shown when the value of a cell is null or undefined. Defaults to "-"
* @param {Function} [onRowClick] The function to be called when a row is clicked.
* @see {@link https://www.npmjs.com/package/vyas-react-table| vyas-react-table documentation} for more information.
* @returns Custom table component
*/
export const Table = ({
tableName = "Data",
data,
tableHeaders,
allowFilters = true,
showSerialNo = false,
borderSpacing = "4px",
itemsPerPage = 10,
allowDownload = false,
filename = "data",
nullDataPlaceholder = "-",
onRowClick = (data) => {},
}) => {
// Data States
const [filteredData, setFilteredData] = useState(data);
// Filter States
const [filterState, setFilterState] = useState({});
// Pagination States
const [rowsPerPage, setRowsPerPage] = useState(itemsPerPage);
const [curPage, setCurPage] = useState(1);
const [totalPages, setTotalPages] = useState(
Math.ceil(filteredData.length / rowsPerPage)
);
const [pageState, setPageState] = useState({
sliceStartIdx: 0,
sliceEndIdx: rowsPerPage,
});
/* Pagination Functions */
const handleRowsPerPageChange = (e) => {
if (e.target.value === "") {
setRowsPerPage(e.target.value);
setCurPage(1);
setPageState({
sliceStartIdx: 0,
sliceEndIdx: filteredData.length,
});
return;
}
if (Number.isInteger(parseInt(e.target.value)) && e.target.value > 0) {
setRowsPerPage(e.target.value);
setCurPage(1);
setPageState({
sliceStartIdx: 0,
sliceEndIdx: e.target.value,
});
setTotalPages(Math.ceil(filteredData.length / e.target.value));
return;
}
return;
};
const handleCurPageChange = (e) => {
if (e.target.value === "") {
setCurPage(e.target.value);
return;
}
if (Number.isInteger(parseInt(e.target.value)) && e.target.value > 0) {
setCurPage(e.target.value);
setPageState({
sliceStartIdx: (e.target.value - 1) * rowsPerPage,
sliceEndIdx: e.target.value * rowsPerPage,
});
return;
}
return;
};
useEffect(() => {
filterData();
}, [filterState]);
/* Filter Functions */
// Clear all filters
const clearFilters = () => {
setFilterState({});
};
// Function to handle Change in individual filter values
const handleColumnFilterChange = (e, header) => {
const newFilterState = { ...filterState };
// In case the filter is cleared
// Deleting empty filters
if (e.target.value === "") {
delete newFilterState[header.key];
setFilterState(newFilterState);
return;
}
setFilterState({
...newFilterState,
[header.key]: e.target.value,
});
};
// Function to actually filter data
const filterData = () => {
const filteredData = data.filter((item) => {
// Check for all the filters
for (const key in filterState) {
if (
!checkInclusion(item[key], filterState[key], tableHeaders[key].type)
) {
return false;
}
}
return true;
});
// Setting the filtered data
setFilteredData(filteredData);
};
/* Download functions */
const handleDownload = () => {
downloadData(filteredData, tableHeaders, filename);
};
/* handle rendering */
const renderData = (data) => data?.toString() ?? nullDataPlaceholder;
return (
<section className="container">
<div className="table-header">
<div className="table-name">{tableName}</div>
<span>Total {filteredData.length} rows</span>
</div>
<div className="statsContainer">
<div className="stats-div">
<div>
Show
<input
type="number"
min="1"
value={rowsPerPage}
onChange={handleRowsPerPageChange}
className="stats-input"
/>
rows per page
</div>
<div>
Show
<input
type="number"
min="1"
max={totalPages}
value={curPage}
onChange={handleCurPageChange}
className="stats-input"
/>
of {totalPages} pages
</div>
</div>
<div>
{allowDownload && (
<button onClick={handleDownload} className="button">
Download
</button>
)}
<button onClick={clearFilters} className="button">
Clear All Filters
</button>
</div>
</div>
<table style={{ borderSpacing }} className="table">
<thead className="thead">
<tr className="headRow">
{showSerialNo && (
<th className="th">
<div className="resizable">Serial No.</div>
</th>
)}
{Object.keys(tableHeaders).map((header, headerIdx) => (
<th key={headerIdx} className="th">
<div className="resizable">{tableHeaders[header].title}</div>
</th>
))}
</tr>
{allowFilters && (
<tr className="filterContainer">
{showSerialNo && <th></th>}
{Object.keys(tableHeaders).map((header, headerIdx) => (
<th key={headerIdx}>
{tableHeaders[header].options === undefined ? (
<input
className="vyasInput"
type={tableHeaders[header].type}
value={filterState[header] ?? ""}
placeholder={`Filter by ${tableHeaders[header].title}...`}
onChange={(e) =>
handleColumnFilterChange(e, tableHeaders[header])
}
/>
) : (
<select
value={filterState[header] ?? ""}
onChange={(e) =>
handleColumnFilterChange(e, tableHeaders[header])
}
className="vyasInput"
>
<option value="">None Selected</option>
{tableHeaders[header].options.map((option, optionIdx) => (
<option key={optionIdx} value={option.value}>
{option.label}
</option>
))}
</select>
)}
</th>
))}
</tr>
)}
</thead>
<tbody className="tbody">
{filteredData
.slice(pageState.sliceStartIdx, pageState.sliceEndIdx)
.map((row, rowIdx) => (
<tr key={rowIdx} className="tr" onClick={() => onRowClick(row)}>
{showSerialNo && (
<td className="td">
{(curPage - 1) * rowsPerPage + rowIdx + 1}
</td>
)}
{Object.keys(tableHeaders).map((header, headerIdx) => (
<td key={headerIdx} className="td">
{Array.isArray(row[tableHeaders[header].key])
? row[tableHeaders[header].key].map((item, itemIdx) => (
<div key={itemIdx}>{renderData(item)}</div>
))
: renderData(row[tableHeaders[header].key])}
</td>
))}
</tr>
))}
</tbody>
</table>
</section>
);
};