UNPKG

quite-simple-reactdatatable

Version:

A simple component to display data on a table, with pagination, search, column specific search, and column asc/desc sorting.

403 lines (378 loc) 20.7 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('prop-types')) : typeof define === 'function' && define.amd ? define(['exports', 'react', 'prop-types'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["quite-simple-reactdatatable"] = {}, global.React, global.PropTypes)); })(this, (function (exports, React, PropTypes) { 'use strict'; function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var PropTypes__namespace = /*#__PURE__*/_interopNamespaceDefault(PropTypes); function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } const TableRow = ({ item, columns, onRowClick, visibleColumns }) => { return /*#__PURE__*/React.createElement("tr", { onClick: () => onRowClick(item) }, columns.filter(col => visibleColumns.has(col.key)).map(column => /*#__PURE__*/React.createElement("td", { key: column.key }, item[column.key]))); }; function styleInject(css, ref) { if (ref === void 0) ref = {}; var insertAt = ref.insertAt; if (!css || typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css_248z = ".employee-filter{\r\n display:flex;\r\n flex-direction: row;\r\n margin-bottom: 20px;\r\n}\r\n\r\ntable {\r\n width: 100%;\r\n border-collapse: collapse;\r\n overflow-y: scroll;\r\n}\r\n\r\ntable, th, td {\r\n border: none;\r\n}\r\nth{\r\n border:none;\r\n}\r\ntable td {\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n}\r\n.tablecontent{\r\n width: 100%;\r\n border-collapse: collapse;\r\n}\r\nth, td {\r\n padding: 8px 12px;\r\n text-align: left;\r\n}\r\nthead{\r\n position: sticky;\r\n top: 0;\r\n z-index: 10;\r\n}\r\nth {\r\n position: relative;\r\n background-color: #333; \r\n color: white;\r\n height: 40px;\r\n padding-right:20px;\r\n}\r\n\r\nth:hover {\r\n cursor: pointer;\r\n}\r\n\r\n\r\ntd {\r\n min-width: 50px;\r\n max-width: 50px;\r\n}\r\n\r\ntbody{\r\n margin-top: 40px;\r\n}\r\ntbody tr:hover {\r\n background-color: #f9f9f9; \r\n cursor: pointer;\r\n}\r\n\r\ninput[type=\"text\"] {\r\n padding: 5px;\r\n box-sizing: border-box;\r\n border: 1px solid #ccc;\r\n border-radius: 4px;\r\n}\r\n\r\nselect {\r\n padding: 7.9px;\r\n box-sizing: border-box;\r\n border: 1px solid #ccc;\r\n border-radius: 4px;\r\n margin-left: 10px;\r\n}\r\n\r\nselect:hover {\r\n cursor: pointer;\r\n}\r\n\r\n.search{\r\n margin-left: 10px;\r\n}\r\n\r\n#startDate, #dateOfBirth:hover {\r\n cursor: pointer;\r\n}\r\n\r\n.pagination{\r\n margin-bottom: 40px;\r\n margin-top: 20px;\r\n}\r\n\r\n\r\n.employee-list, .employee-details, .employee-info {\r\n padding: 10px;\r\n box-sizing: border-box;\r\n}\r\n\r\n.datatable-container{\r\n height: 100%;\r\n}\r\n.datatable-table-overflow-container {\r\n overflow-y: scroll;\r\n border: 1px solid #ccc;\r\n margin-bottom: 2rem;\r\n}\r\n\r\n.datatable-header{\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n white-space: nowrap;\r\n}\r\n\r\n.datatable-header > label{\r\n margin-right: 10px;\r\n margin-bottom: 20px;\r\n}\r\n.dropdown > label {\r\n margin-right: 10px;\r\n \r\n}\r\n\r\n.pagination-text{\r\n margin-left: 10px;\r\n margin-right: 10px;\r\n}\r\n.sort-indicator {\r\n position: absolute;\r\n right: 5px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n}\r\n\r\n.sort-indicator.asc::before {\r\n content: \"\";\r\n}\r\n\r\n.sort-indicator.desc::before {\r\n content: \"\";\r\n}\r\n.sort-indicator {\r\n visibility: hidden; \r\n}\r\n\r\n.sort-indicator.active {\r\n visibility: visible;\r\n}\r\n\r\n\r\n.dropdown {\r\n position: relative;\r\n margin-right: 10px;\r\n margin-bottom: 15px; \r\n \r\n}\r\n\r\n.dropdown-content {\r\n display: none;\r\n position: absolute;\r\n background-color: #f9f9f9;\r\n min-width: 160px;\r\n box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);\r\n z-index: 20;\r\n}\r\n\r\n.dropdown-content div {\r\n color: black;\r\n padding: 8px 12px;\r\n text-decoration: none;\r\n display: block;\r\n}\r\n\r\n.dropdown-content div:hover {\r\n background-color: #f1f1f1;\r\n}\r\n\r\n.dropdown:hover .dropdown-content {\r\n display: block;\r\n}\r\n\r\n.dropdown > label {\r\n cursor: pointer;\r\n transition: background-color 0.3s;\r\n background-color: #eee;\r\n padding: 6.41px;\r\n border-radius: 4px;\r\n}\r\n\r\n.dropdown > label:hover {\r\n background-color: #ddd;\r\n}\r\n\r\n.dropdown > label::after {\r\n content: \"\";\r\n margin-left: 5px;\r\n}\r\n\r\n.dropdown-content input[type=\"checkbox\"] {\r\n margin-right: 10px;\r\n transform: scale(1.2); \r\n cursor: pointer;\r\n\r\n}\r\n\r\n.dropdown-content div {\r\n margin: 8px 0;\r\n padding: 4px 8px;\r\n cursor: pointer;\r\n}\r\n\r\n.search-input-container {\r\n position: relative;\r\n display: flex;\r\n align-items: center;\r\n}\r\n\r\nlabel .search-input {\r\n padding: 7.9px 7.9px 7.9px 24px;\r\n box-sizing: border-box;\r\n border: 1px solid #ccc;\r\n border-radius: 4px;\r\n width: 100%;\r\n}\r\n\r\n.search-icon {\r\n position: absolute;\r\n left: 5px; \r\n\r\n width: 10px;\r\n height: 10px;\r\n border: solid 2px #635757;\r\n border-radius: 50%;\r\n}\r\n\r\n.search-icon:after {\r\n content: '';\r\n position: absolute;\r\n width: 2px;\r\n height: 5.5px;\r\n border-radius: 2px 2px 0px 0px;\r\n background-color: #635757;\r\n \r\n bottom: -4px;\r\n right: -2.5px;\r\n transform: rotate(136deg);\r\n}\r\n\r\n.search-label{\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n}\r\n\r\nbutton{\r\n background-color: #333;\r\n color: #ffffff;\r\n border: transparent solid 1px;\r\n padding: 10px 20px;\r\n text-align: center;\r\n text-decoration: none;\r\n display: inline-block;\r\n font-size: 16px;\r\n margin: 4px 2px;\r\n cursor: pointer;\r\n border-radius: 4px;\r\n transition: background-color 0.3s;\r\n}\r\n\r\nbutton:hover {\r\n background-color: #ffffff;\r\n color: #333;\r\n border: #333 solid 1px;\r\n}\r\n"; styleInject(css_248z); //Memoized version of TableRow component to prevent unnecessary re-renders as memo does a shallow comparison of props const MemoizedTableRow = /*#__PURE__*/React.memo(TableRow); //Main component ReactDataTable //Uses useReducer hook to manage state function ReactDataTable({ data = [], columns = [], onRowClick = () => {}, defaultEntriesPerPage = 10, sortColumnParam = 'name', headerHeight = 'auto', tableBodyHeight = 'auto', paginationHeight = 'auto', headerFontSize = '1rem', tableBodyFontSize = '1rem', paginationFontSize = '1rem', fontFamily = 'Arial', containerWidth = '100%' }) { //Dispatch an action to set the filtered data when the data prop changes React.useEffect(() => { dispatch({ type: 'SET_FILTERED_DATA', payload: data }); }, [data]); //Object with every property needed for the table //Is used in the useReducer hook as the initial state //and will be updated by the reducer function depending on the action dispatched const initialState = { searchTerm: '', filteredData: data, searchColumn: 'all', currentPage: 1, entriesPerPage: defaultEntriesPerPage, sortColumn: { key: sortColumnParam, direction: 'neutral' }, visibleColumns: new Set(columns.map(col => col.key)) }; //Reducer, will return the corresponding new state when an action is dispatched //action type (ex: 'SET_SEARCH_TERM') determines which property of the state will be updated //action payload (ex: 'John') is the new value of the property function reducer(state, action) { switch (action.type) { case 'SET_SEARCH_TERM': //Search input return { ...state, searchTerm: action.payload }; case 'SET_FILTERED_DATA': //Search results (or received data when the search input is empty) return { ...state, filteredData: action.payload }; case 'SET_SEARCH_COLUMN': //Column selected to search in return { ...state, searchColumn: action.payload }; case 'SET_CURRENT_PAGE': //Page viewed in the table return { ...state, currentPage: action.payload }; case 'SET_ENTRIES_PER_PAGE': //Page size return { ...state, entriesPerPage: action.payload }; case 'SET_SORT_COLUMN': //Sort selected column in ascending, descending or neutral order return { ...state, sortColumn: action.payload }; default: return state; } } //useReducer hook which will update the state via the reducer function //function reducer is the first parameter, initialState (ex: //It updates the state with the results of the reducer function const [state, dispatch] = React.useReducer(reducer, initialState); //Function to search for a value in the data //It is used with a debounce in the handleSearch function const search = React.useCallback((name, items) => { if (!Array.isArray(items)) { console.error('Expected an array for items'); return []; } const results = new Set(); //A set is used to avoid duplicates const searchTerms = name.toLowerCase().split(/\s+/); //We split the search value to search for each word items.forEach(item => { let concatenatedData = ''; columns.forEach(column => { if (state.searchColumn === 'all' || state.searchColumn === column.key) { concatenatedData += ' ' + String(item[column.key]).toLowerCase(); } }); const dataSegments = concatenatedData.split(/\s+/); let termFound = false; for (let term of searchTerms) { if (dataSegments.some(segment => segment.includes(term))) { termFound = true; } else { termFound = false; break; } } if (termFound) { results.add(item); } }); return Array.from(results); }, [state.searchColumn, columns]); //Debounce function to call search function with a delay const debouncedSearch = React.useCallback(debounce(searchValue => { let results; if (searchValue === '') { results = data; } else { results = search(searchValue, data); } dispatch({ type: 'SET_FILTERED_DATA', payload: results }); }, 150), [data, dispatch, search]); //onRowClick is the function passed as a prop //only re-created when the onRowClick prop changes const memoizedOnRowClick = React.useCallback(item => onRowClick(item), [onRowClick]); //Used to send the search value to the search function when the search input changes const handleSearch = e => { dispatch({ type: 'SET_SEARCH_TERM', payload: e.target.value }); debouncedSearch(e.target.value); }; //Props Styles const containerStyle = { width: containerWidth }; const headerStyle = { height: headerHeight, fontSize: headerFontSize, fontFamily }; const tableBodyStyle = { height: tableBodyHeight, fontSize: tableBodyFontSize, fontFamily }; const paginationStyle = { height: paginationHeight, fontSize: paginationFontSize, fontFamily }; //Memoized version of the sorted data, will be used by totalPages and currentEntries //to calculate the number of pages and the entries to display in the current page //Sorts the data when: any data change or search input change const sortedData = React.useMemo(() => { let array = [...state.filteredData]; if (state.sortColumn.direction === 'neutral') { return array; } return array.sort((a, b) => { if (state.sortColumn.direction === 'asc') { return String(a[state.sortColumn.key]).localeCompare(String(b[state.sortColumn.key])); } else { return String(b[state.sortColumn.key]).localeCompare(String(a[state.sortColumn.key])); } }); }, [state.filteredData, state.sortColumn]); //Only re-sort when the filtered data or sort column changes //Function to handle the click on a column header //It will dispatch an action to update the sortColumn const handleHeaderClick = columnKey => { let newDirection = 'asc'; //Default to ascending on the first click //If the current sort column is the same as the clicked column, toggle the direction if (state.sortColumn.key === columnKey) { if (state.sortColumn.direction === 'asc') { newDirection = 'desc'; } else if (state.sortColumn.direction === 'desc') { newDirection = 'neutral'; //Reset to neutral/unsorted state } } dispatch({ type: 'SET_SORT_COLUMN', payload: { key: columnKey, direction: newDirection } }); }; //Calculate the total number of pages based on the number of entries per page const totalPages = Math.ceil(sortedData.length / state.entriesPerPage); //Get the entries to display based on the current page and entries per page const currentEntries = sortedData.slice((state.currentPage - 1) * state.entriesPerPage, state.currentPage * state.entriesPerPage); return /*#__PURE__*/React.createElement("div", { className: "datatable-container", style: containerStyle }, /*#__PURE__*/React.createElement("div", { className: "datatable-header", style: headerStyle }, /*#__PURE__*/React.createElement("label", null, "Show entries:", /*#__PURE__*/React.createElement("select", { value: state.entriesPerPage, onChange: e => dispatch({ type: 'SET_ENTRIES_PER_PAGE', payload: Number(e.target.value) }) }, /*#__PURE__*/React.createElement("option", { value: 10 }, "10"), /*#__PURE__*/React.createElement("option", { value: 25 }, "25"), /*#__PURE__*/React.createElement("option", { value: 50 }, "50"), /*#__PURE__*/React.createElement("option", { value: 100 }, "100"))), /*#__PURE__*/React.createElement("label", null, "Search Column:", /*#__PURE__*/React.createElement("select", { value: state.searchColumn, onChange: e => dispatch({ type: 'SET_SEARCH_COLUMN', payload: e.target.value }) }, /*#__PURE__*/React.createElement("option", { value: "all" }, "All"), columns.map(column => /*#__PURE__*/React.createElement("option", { key: column.key, value: column.key }, column.title)))), /*#__PURE__*/React.createElement("label", { className: "search-label search-input-container" }, /*#__PURE__*/React.createElement("input", { type: "text", value: state.searchTerm, onChange: handleSearch, className: "search-input" }), /*#__PURE__*/React.createElement("div", { className: "search-icon" }))), /*#__PURE__*/React.createElement("div", { className: "datatable-table-overflow-container", style: tableBodyStyle }, /*#__PURE__*/React.createElement("table", null, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, columns.filter(col => state.visibleColumns.has(col.key)).map(column => /*#__PURE__*/React.createElement("th", { key: column.key, onClick: () => handleHeaderClick(column.key), "data-testid": `column-${column.key}` }, column.title, /*#__PURE__*/React.createElement("span", { className: `sort-indicator ${state.sortColumn.key === column.key ? (state.sortColumn.direction !== 'neutral' ? 'active ' : '') + state.sortColumn.direction : ''}` }))))), /*#__PURE__*/React.createElement("tbody", null, currentEntries.map(item => /*#__PURE__*/React.createElement(MemoizedTableRow, { key: item.id, item: item, columns: columns, onRowClick: memoizedOnRowClick, visibleColumns: state.visibleColumns }))))), /*#__PURE__*/React.createElement("div", { className: "datatable-pagination", style: paginationStyle }, /*#__PURE__*/React.createElement("button", { onClick: () => dispatch({ type: 'SET_CURRENT_PAGE', payload: Math.max(state.currentPage - 1, 1) }) }, "Previous"), /*#__PURE__*/React.createElement("span", { className: "pagination-text" }, `Showing ${Math.min((state.currentPage - 1) * state.entriesPerPage + 1, state.filteredData.length)} to ${Math.min(state.currentPage * state.entriesPerPage, state.filteredData.length)} of ${state.filteredData.length} entries`), /*#__PURE__*/React.createElement("button", { onClick: () => dispatch({ type: 'SET_CURRENT_PAGE', payload: Math.min(state.currentPage + 1, totalPages) }) }, "Next"))); } //Proptypes to validate props passed to ReactDataTable ReactDataTable.propTypes = { data: PropTypes__namespace.array.isRequired, columns: PropTypes__namespace.arrayOf(PropTypes__namespace.shape({ key: PropTypes__namespace.string.isRequired, title: PropTypes__namespace.string.isRequired })).isRequired, onRowClick: PropTypes__namespace.func, defaultEntriesPerPage: PropTypes__namespace.number, sortColumnParam: PropTypes__namespace.string, headerHeight: PropTypes__namespace.string, tableBodyHeight: PropTypes__namespace.string, paginationHeight: PropTypes__namespace.string, headerWidth: PropTypes__namespace.string, tableWidth: PropTypes__namespace.string, paginationWidth: PropTypes__namespace.string, headerFontSize: PropTypes__namespace.string, tableBodyFontSize: PropTypes__namespace.string, paginationFontSize: PropTypes__namespace.string, fontFamily: PropTypes__namespace.string }; TableRow.propTypes = { item: PropTypes__namespace.object.isRequired, columns: PropTypes__namespace.array.isRequired, onRowClick: PropTypes__namespace.func.isRequired, visibleColumns: PropTypes__namespace.instanceOf(Set).isRequired }; exports.ReactDataTable = ReactDataTable; })); //# sourceMappingURL=bundle.js.map