├── AdvancedTable.css ├── AdvancedTable.js ├── AdvancedTable.module.css ├── README.md ├── ResponsiveTable.css ├── package.json └── super-responsive-table ├── SuperResponsiveTableStyle.css ├── components ├── Table.js ├── Tbody.js ├── Td.js ├── TdInner.js ├── Th.js ├── Thead.js ├── Tr.js └── TrInner.js ├── index.js └── utils ├── allowed.js └── tableContext.js /AdvancedTable.css: -------------------------------------------------------------------------------- 1 | .c-datatable-filter { 2 | display: flex; 3 | flex-flow: row nowrap; 4 | justify-content: flex-start; 5 | align-items: center; 6 | } 7 | .c-datatable-filter label { 8 | margin-bottom: 0; 9 | } 10 | .c-datatable-items-per-page { 11 | display: flex; 12 | flex-flow: row nowrap; 13 | justify-content: flex-end; 14 | align-items: center; 15 | } 16 | .c-datatable-items-per-page label { 17 | margin-bottom: 0; 18 | } 19 | -------------------------------------------------------------------------------- /AdvancedTable.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useMemo, useEffect } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | // import CPagination from '../pagination/CPagination' 5 | // import CElementCover from '../element-cover/CElementCover' 6 | import CIcon from '@coreui/icons-react' 7 | import { cilArrowTop, cilBan, cilFilterX } from '@coreui/icons' 8 | import style from './AdvancedTable.module.css' 9 | import './AdvancedTable.css' 10 | import { Table, Thead, Tbody, Tr, Th, Td } from '../super-responsive-table'; 11 | import './ResponsiveTable.css'; 12 | import { CElementCover, CPagination, CCol, CLabel, CInput } from '@coreui/react/lib' 13 | import { useMediaQuery } from 'react-responsive' 14 | 15 | //component - CoreUI / CTable 16 | const accentsMap = { 17 | a: 'á|à|ã|â|À|Á|Ã|Â', 18 | e: 'é|è|ê|É|È|Ê', 19 | i: 'í|ì|î|Í|Ì|Î', 20 | o: 'ó|ò|ô|õ|Ó|Ò|Ô|Õ', 21 | u: 'ú|ù|û|ü|Ú|Ù|Û|Ü', 22 | c: 'ç|Ç', 23 | n: 'ñ|Ñ', 24 | }; 25 | 26 | export const slugify = str => str.normalize('NFD').replace(/([\u0300-\u036f]|[^0-9a-zA-Z\s])/g, '') 27 | export const AdvancedTable = props => { 28 | const { 29 | // 30 | innerRef, 31 | overTableSlot, 32 | columnHeaderSlot, 33 | sortingIconSlot, 34 | columnFilterSlot, 35 | noItemsViewSlot, 36 | noItemsView, 37 | captionSlot, 38 | footerSlot, 39 | underTableSlot, 40 | theadTopSlot, 41 | loadingSlot, 42 | scopedSlots, 43 | loading, 44 | fields, 45 | pagination, 46 | activePage, 47 | itemsPerPage, 48 | items, 49 | sorter, 50 | header, 51 | clickableRows, 52 | columnFilter, 53 | tableFilterValue, 54 | tableFilter, 55 | cleaner, 56 | addTableClasses, 57 | size, 58 | dark, 59 | striped, 60 | hover, 61 | border, 62 | outlined, 63 | responsive, 64 | footer, 65 | itemsPerPageSelect, 66 | sorterValue, 67 | columnFilterValue, 68 | onRowClick, 69 | onSorterValueChange, 70 | onPaginationChange, 71 | onColumnFilterChange, 72 | onPagesChange, 73 | onTableFilterChange, 74 | onPageChange, 75 | onFilteredItemsChange 76 | } = props 77 | 78 | const compData = useRef( 79 | { firstRun: true, columnFiltered: 0, changeItems: 0 }).current 80 | 81 | 82 | // 83 | const [perPageItems, setPerPageItems] = useState(itemsPerPage) 84 | const [sorterState, setSorterState] = useState(sorterValue || {}) 85 | const [tableFilterState, setTableFilterState] = useState(tableFilterValue) 86 | const [columnFilterState, setColumnFilterState] = useState(columnFilterValue || {}) 87 | const [page, setPage] = useState(activePage || 1) 88 | const [passedItems, setPassedItems] = useState(items || []) 89 | const isMobile = useMediaQuery({ maxWidth: '40em' }) 90 | // functions 91 | 92 | const cellClass = (item, colName, index) => { 93 | let classes = [] 94 | if (item._cellClasses && item._cellClasses[colName]) { 95 | classes.push(item._cellClasses[colName]) 96 | } 97 | if (fields && fields[index]._classes) { 98 | classes.push(fields[index]._classes) 99 | } 100 | return classes 101 | } 102 | 103 | const pretifyName = (name) => { 104 | return name.replace(/[-_.]/g, ' ') 105 | .replace(/ +/g, ' ') 106 | .replace(/([a-z0-9])([A-Z])/g, '$1 $2') 107 | .split(' ') 108 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 109 | .join(' ') 110 | } 111 | 112 | const headerClass = i => fields && fields[i]._classes && fields[i]._classes 113 | 114 | const isSortable = i => { 115 | const isDataColumn = itemsDataColumns.includes(rawColumnNames[i]) 116 | return sorter && (!fields || fields[i].sorter !== false) && isDataColumn 117 | } 118 | 119 | const headerStyles = (index) => { 120 | let style = { verticalAlign: 'middle', overflow: 'hidden' } 121 | if (isSortable(index)) { 122 | style.cursor = 'pointer' 123 | } 124 | if (fields && fields[index] && fields[index]._style) { 125 | return { ...style, ...fields[index]._style } 126 | } 127 | return style 128 | } 129 | 130 | const getIconState = index => { 131 | const direction = sorterState.asc ? 'asc' : 'desc' 132 | return rawColumnNames[index] === sorterState.column ? direction : 0 133 | } 134 | 135 | const iconClasses = index => { 136 | const state = getIconState(index) 137 | return [ 138 | 'position-absolute', style['icon-transition'], style['arrow-position'], 139 | !state && style['transparent'], 140 | state === 'desc' && style['rotate-icon'] 141 | ] 142 | } 143 | 144 | const rowClicked = (item, index, e, detailsClick = false) => { 145 | onRowClick && onRowClick(item, index, getClickedColumnName(e, detailsClick), e) 146 | } 147 | 148 | const changeSort = (column, index) => { 149 | if (!isSortable(index)) { 150 | return 151 | } 152 | //if column changed or sort was descending change asc to true 153 | const state = sorterState 154 | const columnRepeated = state.column === column 155 | if (!sorter || !sorter.resetable) { 156 | state.column = column 157 | } else { 158 | state.column = columnRepeated && state.asc === false ? null : column 159 | } 160 | state.asc = !(columnRepeated && state.asc) 161 | setSorterState({ ...state }) 162 | } 163 | 164 | useEffect(() => { 165 | onSorterValueChange && onSorterValueChange(sorterState) 166 | }, [JSON.stringify(sorterState)]) 167 | 168 | const paginationChange = e => { 169 | onPaginationChange && onPaginationChange(Number(e.target.value)) 170 | !itemsPerPageSelect.external && setPerPageItems(Number(e.target.value)) 171 | } 172 | 173 | const columnFilterEvent = (colName, value, type) => { 174 | const isLazy = columnFilter && columnFilter.lazy === true 175 | if (isLazy && type === 'input' || !isLazy && type === 'change') { 176 | return 177 | } 178 | const newState = { ...columnFilterState, [`${colName}`]: value } 179 | setColumnFilterState(newState) 180 | } 181 | 182 | useEffect(() => { 183 | onColumnFilterChange && onColumnFilterChange(columnFilterState) 184 | }, [JSON.stringify(columnFilterState)]) 185 | 186 | const tableFilterChange = (value, type) => { 187 | const isLazy = tableFilter && tableFilter.lazy === true 188 | if (isLazy && type === 'input' || !isLazy && type === 'change') { 189 | return 190 | } 191 | setTableFilterState(value) 192 | } 193 | 194 | useEffect(() => { 195 | onTableFilterChange && onTableFilterChange(tableFilterState) 196 | }, [tableFilterState]) 197 | 198 | const getClickedColumnName = (e, detailsClick) => { 199 | if (detailsClick) { 200 | return 'details' 201 | } else { 202 | const children = Array.from(e.target.closest('tr').children) 203 | const clickedCell = children.filter(child => child.contains(e.target))[0] 204 | return rawColumnNames[children.indexOf(clickedCell)] 205 | } 206 | } 207 | 208 | const clean = () => { 209 | setTableFilterState('') 210 | setColumnFilterState({}) 211 | setSorterState({ 212 | column: "", 213 | asc: true 214 | }) 215 | } 216 | 217 | // computed 218 | 219 | const genCols = Object.keys(passedItems[0] || {}).filter(el => el.charAt(0) !== '_') 220 | 221 | const rawColumnNames = fields ? fields.map(el => el.key || el) : genCols 222 | 223 | const itemsDataColumns = rawColumnNames.filter(name => genCols.includes(name)) 224 | 225 | useMemo(() => { 226 | compData.columnFiltered++ 227 | }, [ 228 | JSON.stringify(columnFilter), 229 | JSON.stringify(columnFilterState), 230 | itemsDataColumns.join(''), 231 | compData.changeItems 232 | ]) 233 | 234 | const columnFiltered = useMemo(() => { 235 | let items = passedItems 236 | if (columnFilter && columnFilter.external) { 237 | return items 238 | } 239 | Object.entries(columnFilterState).forEach(([key, value]) => { 240 | const columnFilter = String(value).toLowerCase() 241 | if (columnFilter && itemsDataColumns.includes(key)) { 242 | items = items.filter(item => { 243 | return slugify(String(item[key]).toLowerCase()).includes(columnFilter) 244 | }) 245 | } 246 | }) 247 | return items 248 | }, [compData.columnFiltered]) 249 | 250 | const tableFiltered = useMemo(() => { 251 | let items = columnFiltered 252 | if (!tableFilterState || (tableFilter && tableFilter.external)) { 253 | return items 254 | } 255 | const filter = tableFilterState.toLowerCase() 256 | const valueContainFilter = val => String(val).toLowerCase().includes(filter) 257 | items = items.filter(item => { 258 | return !!itemsDataColumns.find(key => valueContainFilter(item[key])) 259 | }) 260 | return items 261 | }, [ 262 | compData.columnFiltered, 263 | tableFilterState, 264 | JSON.stringify(tableFilter) 265 | ]) 266 | 267 | const sortedItems = useMemo(() => { 268 | const col = sorterState.column 269 | 270 | if (!col || !itemsDataColumns.includes(col) || (sorter && sorter.external)) { 271 | return tableFiltered 272 | } 273 | //if values in column are to be sorted by numeric value they all have to be type number 274 | const flip = sorterState.asc ? 1 : -1 275 | const sorted = tableFiltered.slice().sort((item, item2) => { 276 | const value = item[col] 277 | const value2 = item2[col] 278 | const a = typeof value === 'number' ? value : String(value).toLowerCase() 279 | const b = typeof value2 === 'number' ? value2 : String(value2).toLowerCase() 280 | return a > b ? 1 * flip : b > a ? -1 * flip : 0 281 | }) 282 | return sorted 283 | }, [ 284 | JSON.stringify(tableFiltered), 285 | JSON.stringify(sorterState), 286 | JSON.stringify(sorter) 287 | ]) 288 | 289 | useEffect(() => { 290 | !compData.firstRun && onFilteredItemsChange && onFilteredItemsChange(sortedItems) 291 | }, [JSON.stringify(sortedItems)]) 292 | 293 | const tableClasses = [ 294 | 'table', 295 | { 296 | [`table-${size}`]: size, 297 | 'table-dark': dark, 298 | 'table-striped': striped, 299 | 'table-hover': hover, 300 | 'table-bordered': border, 301 | 'border': outlined 302 | }, 303 | addTableClasses 304 | ] 305 | 306 | 307 | const columnNames = useMemo(() => { 308 | if (fields) { 309 | return fields.map(f => { 310 | return f.label !== undefined ? f.label : pretifyName(f.key || f) 311 | }) 312 | } 313 | return rawColumnNames.map(el => pretifyName(el)) 314 | }, [fields, rawColumnNames]) 315 | 316 | const sortingIconStyles = sorter && 'position-relative pr-4' 317 | 318 | const colspan = rawColumnNames.length 319 | 320 | const totalPages = Math.ceil((sortedItems.length) / perPageItems) || 1 321 | useMemo(() => { 322 | !compData.firstRun && onPagesChange && onPagesChange(totalPages) 323 | }, [totalPages]) 324 | 325 | const computedPage = useMemo(() => { 326 | const compPage = pagination ? page : activePage 327 | !compData.firstRun && onPageChange && onPageChange(compPage) 328 | return compPage 329 | }, [page, activePage, pagination]) 330 | 331 | const firstItemIndex = (computedPage - 1) * perPageItems || 0 332 | const paginatedItems = sortedItems.slice( 333 | firstItemIndex, 334 | firstItemIndex + perPageItems 335 | ) 336 | const currentItems = computedPage ? paginatedItems : sortedItems 337 | 338 | const tableFilterData = { 339 | label: (tableFilter && tableFilter.label) || 'Filter:', 340 | placeholder: (tableFilter && tableFilter.placeholder) || 'type string...' 341 | } 342 | 343 | const paginationSelect = { 344 | label: (itemsPerPageSelect && itemsPerPageSelect.label) || 'Items per page:', 345 | values: (itemsPerPageSelect && itemsPerPageSelect.values) || [5, 10, 20, 50] 346 | } 347 | 348 | const noItemsText = (() => { 349 | const customValues = noItemsView || {} 350 | if (passedItems.length) { 351 | return customValues.noResults || 'No filtering results' 352 | } 353 | return customValues.noItems || 'No items' 354 | })() 355 | 356 | const isFiltered = tableFilterState || sorterState.column || 357 | Object.values(columnFilterState).join('') 358 | 359 | const cleanerProps = { 360 | content: cilFilterX, 361 | className: `mfs-2 ${isFiltered ? 'text-danger' : 'transparent'}`, 362 | role: isFiltered ? 'button' : null, 363 | tabIndex: isFiltered ? 0 : null, 364 | } 365 | 366 | // watch 367 | useMemo(() => setPerPageItems(itemsPerPage), [itemsPerPage]) 368 | 369 | useMemo(() => setSorterState({ ...sorterValue }), [sorterValue]) 370 | 371 | useMemo(() => setTableFilterState(tableFilterValue), [tableFilterValue]) 372 | 373 | useMemo(() => setColumnFilterState({ ...columnFilterValue }), [columnFilterValue]) 374 | 375 | //items 376 | useMemo(() => { 377 | if ( 378 | items && 379 | !compData.firstRun && 380 | (items.length !== passedItems.length || 381 | JSON.stringify(items) !== JSON.stringify(passedItems)) 382 | ) { 383 | setPassedItems(items) 384 | compData.changeItems++ 385 | } 386 | }) 387 | 388 | 389 | 390 | // render 391 | compData.firstRun = false 392 | 393 | const paginationProps = typeof pagination === 'object' ? pagination : null 394 | 395 | const headerContent = ( 396 | 397 | { 398 | columnNames.map((name, index) => { 399 | return ( 400 | { changeSort(rawColumnNames[index], index) }} 402 | className={classNames([headerClass(index), sortingIconStyles])} 403 | style={headerStyles(index)} 404 | key={index} 405 | > 406 | {columnHeaderSlot[`${rawColumnNames[index]}`] || 407 |
{name}
408 | } 409 | { 410 | isSortable(index) && 411 | ((sortingIconSlot && sortingIconSlot(getIconState(index), iconClasses(index))) || 412 | ) 417 | } 418 | 419 | ) 420 | }) 421 | } 422 | ) 423 | 424 | return ( 425 | 426 |
427 | { 428 | (itemsPerPageSelect || tableFilter || cleaner) && 429 |
430 | { 431 | (tableFilter || cleaner) && 432 |
433 | { 434 | tableFilter && 435 | <> 436 | 437 | { tableFilterChange(e.target.value, 'input') }} 442 | onChange={(e) => { tableFilterChange(e.target.value, 'change') }} 443 | value={tableFilterState || ''} 444 | aria-label="table filter input" 445 | /> 446 | 447 | } 448 | { 449 | cleaner && ( 450 | typeof cleaner === 'function' ? cleaner(clean, isFiltered, cleanerProps) : 451 | { if (event.key === 'Enter') clean() }} 455 | /> 456 | ) 457 | } 458 | 459 |
460 | } 461 | { 462 | itemsPerPageSelect && 463 |
464 |
465 | 466 | 483 |
484 |
485 | } 486 |
487 | } 488 |
489 | 490 | {overTableSlot} 491 | 492 |
493 | { 494 | isMobile && columnFilter &&
495 | { 496 | rawColumnNames.map((colName, index) => { 497 | return ( 498 |
499 | 500 | {columnFilterSlot[`${rawColumnNames[index]}`] || 501 | ((!fields || fields[index].filter !== false) && 502 |
503 | {fields[index].label} 504 | { columnFilterEvent(colName, e.target.value, 'input') }} 507 | onChange={e => { columnFilterEvent(colName, e.target.value, 'change') }} 508 | value={columnFilterState[colName] || ''} 509 | aria-label={`column name: '${colName}' filter input`} 510 | /> 511 |
) 512 | } 513 |
514 | ) 515 | }) 516 | } 517 | 518 |
519 | } 520 | 521 | {!isMobile ? 522 | 523 | 524 | {theadTopSlot} 525 | {header && headerContent} 526 | { 527 | columnFilter && 528 | { 529 | rawColumnNames.map((colName, index) => { 530 | return ( 531 | 543 | ) 544 | }) 545 | } 546 | 547 | 548 | } 549 | 550 | 551 | : 552 | 553 | {header && headerContent} 554 | 555 | } 556 | 557 | {currentItems.map((item, itemIndex) => { 558 | return ( 559 | 560 | { rowClicked(item, itemIndex + firstItemIndex, e) }} 564 | > 565 | { 566 | rawColumnNames.map((colName, index) => { 567 | return ( 568 | scopedSlots[colName] && 569 | React.cloneElement( 570 | scopedSlots[colName](item, itemIndex + firstItemIndex), 571 | { 'key': index } 572 | ) 573 | ) || 574 | 580 | }) 581 | } 582 | 583 | { 584 | scopedSlots.details && 585 | { rowClicked(item, itemIndex + firstItemIndex, e, true) }} 587 | className="p-0" 588 | style={{ border: 'none !important' }} 589 | key={'details' + itemIndex} 590 | > 591 | 598 | 599 | } 600 | 601 | ) 602 | })} 603 | { 604 | !currentItems.length && 605 | 606 | 620 | 621 | } 622 | 623 | {footer && currentItems.length > 0 && {headerContent}} 624 | {footerSlot} 625 | {captionSlot} 626 |
532 | {columnFilterSlot[`${rawColumnNames[index]}`] || 533 | ((!fields || fields[index].filter !== false) && 534 | { columnFilterEvent(colName, e.target.value, 'input') }} 537 | onChange={e => { columnFilterEvent(colName, e.target.value, 'change') }} 538 | value={columnFilterState[colName] || ''} 539 | aria-label={`column name: '${colName}' filter input`} 540 | />) 541 | } 542 |
578 | {String(item[colName])} 579 |
596 | {scopedSlots.details(item, itemIndex + firstItemIndex)} 597 |
607 | {noItemsViewSlot || 608 |
609 |

610 | {noItemsText + ' '} 611 | 617 |

618 |
} 619 |
627 | {loading && 628 | (loadingSlot || 629 | ) 635 | } 636 |
637 | 638 | {underTableSlot} 639 | 640 | {pagination && 641 | 1 ? 'inline' : 'none' }} 643 | onActivePageChange={(page) => { setPage(page) }} 644 | pages={totalPages} 645 | activePage={page} 646 | {...paginationProps} 647 | /> 648 | } 649 |
650 | ) 651 | } 652 | 653 | AdvancedTable.propTypes = { 654 | // 655 | innerRef: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), 656 | overTableSlot: PropTypes.node, 657 | columnHeaderSlot: PropTypes.object, 658 | sortingIconSlot: PropTypes.func, 659 | columnFilterSlot: PropTypes.object, 660 | noItemsViewSlot: PropTypes.node, 661 | noItemsView: PropTypes.object, 662 | captionSlot: PropTypes.node, 663 | footerSlot: PropTypes.node, 664 | underTableSlot: PropTypes.node, 665 | scopedSlots: PropTypes.object, 666 | theadTopSlot: PropTypes.node, 667 | loadingSlot: PropTypes.node, 668 | loading: PropTypes.bool, 669 | fields: PropTypes.array, 670 | pagination: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), 671 | activePage: PropTypes.number, 672 | itemsPerPage: PropTypes.number, 673 | items: PropTypes.array, 674 | sorter: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), 675 | clickableRows: PropTypes.bool, 676 | columnFilter: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), 677 | tableFilterValue: PropTypes.string, 678 | tableFilter: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), 679 | cleaner: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), 680 | addTableClasses: PropTypes.oneOfType([PropTypes.string, PropTypes.array, PropTypes.object]), 681 | size: PropTypes.string, 682 | dark: PropTypes.bool, 683 | striped: PropTypes.bool, 684 | hover: PropTypes.bool, 685 | border: PropTypes.bool, 686 | outlined: PropTypes.bool, 687 | responsive: PropTypes.bool, 688 | footer: PropTypes.bool, 689 | itemsPerPageSelect: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), 690 | sorterValue: PropTypes.object, 691 | columnFilterValue: PropTypes.object, 692 | header: PropTypes.bool, 693 | onRowClick: PropTypes.func, 694 | onSorterValueChange: PropTypes.func, 695 | onPaginationChange: PropTypes.func, 696 | onColumnFilterChange: PropTypes.func, 697 | onPagesChange: PropTypes.func, 698 | onTableFilterChange: PropTypes.func, 699 | onPageChange: PropTypes.func, 700 | onFilteredItemsChange: PropTypes.func 701 | } 702 | 703 | AdvancedTable.defaultProps = { 704 | itemsPerPage: 10, 705 | responsive: true, 706 | columnHeaderSlot: {}, 707 | columnFilterSlot: {}, 708 | scopedSlots: {}, 709 | sorterValue: {}, 710 | header: true 711 | } 712 | 713 | export default AdvancedTable 714 | -------------------------------------------------------------------------------- /AdvancedTable.module.css: -------------------------------------------------------------------------------- 1 | .transparent { 2 | opacity: 0.4; 3 | } 4 | .icon-transition { 5 | -webkit-transition: transform 0.3s; 6 | transition: transform 0.3s; 7 | } 8 | .arrow-position { 9 | right: 0; 10 | top: 50%; 11 | -ms-transform: translateY(-50%); 12 | transform: translateY(-50%); 13 | } 14 | .rotate-icon { 15 | -ms-transform: translateY(-50%) rotate(-180deg); 16 | transform: translateY(-50%) rotate(-180deg); 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-super-responsive-data-table 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/coston/react-super-responsive-data-table/badge.svg?branch=master)](https://coveralls.io/github/coston/react-super-responsive-data-table?branch=master) 4 | [![npm downloads](https://img.shields.io/npm/dm/react-super-responsive-data-table.svg)](https://www.npmjs.com/package/react-super-responsive-data-table) 5 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io) 6 | 7 | react-super-responsive-data-table converts your table data to a user-friendly list in mobile view with high performance data rendering and smart filter and sorting. 8 | 9 | - [Installation](#installation) 10 | - [Usage](#usage) 11 | - [Contributors](#Contributors) 12 | - [Contributing](#contributing) 13 | - [License](#license) 14 | 15 | 16 | 17 | 18 | ## Installation 19 | 20 | ``` 21 | npm install react-super-responsive-data-table --save 22 | ``` 23 | 24 | ## Usage 25 | 26 | 1. `import AdvancedTable from 'react-super-responsive-data-table'` 27 | 3. Write your html table with the imported components. 28 | 29 | ```jsx 30 | import React from 'react'; 31 | import AdvancedTable from 'react-super-responsive-data-table'; 32 | 33 | 34 | () => { 35 | const [details, setDetails] = useState([]) 36 | const columns = [ 37 | { 38 | key: 'name', 39 | _style: { width: '40%' }, 40 | _props: { color: 'primary', className: 'fw-semibold' }, 41 | }, 42 | 'registered', 43 | { key: 'role', filter: false, sorter: false, _style: { width: '20%' } }, 44 | { key: 'status', _style: { width: '20%' } }, 45 | { 46 | key: 'show_details', 47 | label: '', 48 | _style: { width: '1%' }, 49 | filter: false, 50 | sorter: false, 51 | _props: { color: 'primary', className: 'fw-semibold' }, 52 | }, 53 | ] 54 | const usersData = [ 55 | { 56 | id: 0, 57 | name: 'John Doe', 58 | registered: '2018/01/01', 59 | role: 'Guest', 60 | status: 'Pending', 61 | }, 62 | { 63 | id: 1, 64 | name: 'Samppa Nori', 65 | registered: '2018/01/01', 66 | role: 'Member', 67 | status: 'Active', 68 | _props: { color: 'primary', align: 'middle' }, 69 | }, 70 | { 71 | id: 2, 72 | name: 'Estavan Lykos', 73 | registered: '2018/02/01', 74 | role: 'Staff', 75 | status: 'Banned', 76 | _cellProps: { 77 | all: { className: 'fw-semibold' }, 78 | name: { color: 'info' }, 79 | }, 80 | }, 81 | { 82 | id: 3, 83 | name: 'Chetan Mohamed', 84 | registered: '2018/02/01', 85 | role: 'Admin', 86 | status: 'Inactive', 87 | }, 88 | { 89 | id: 4, 90 | name: 'Derick Maximinus', 91 | registered: '2018/03/01', 92 | role: 'Member', 93 | status: 'Pending', 94 | }, 95 | { 96 | id: 5, 97 | name: 'Friderik Dávid', 98 | registered: '2018/01/21', 99 | role: 'Staff', 100 | status: 'Active', 101 | }, 102 | { 103 | id: 6, 104 | name: 'Yiorgos Avraamu', 105 | registered: '2018/01/01', 106 | role: 'Member', 107 | status: 'Active', 108 | }, 109 | { 110 | id: 7, 111 | name: 'Avram Tarasios', 112 | registered: '2018/02/01', 113 | role: 'Staff', 114 | status: 'Banned', 115 | _props: { color: 'warning', align: 'middle' }, 116 | }, 117 | { 118 | id: 8, 119 | name: 'Quintin Ed', 120 | registered: '2018/02/01', 121 | role: 'Admin', 122 | status: 'Inactive', 123 | }, 124 | { 125 | id: 9, 126 | name: 'Enéas Kwadwo', 127 | registered: '2018/03/01', 128 | role: 'Member', 129 | status: 'Pending', 130 | }, 131 | { 132 | id: 10, 133 | name: 'Agapetus Tadeáš', 134 | registered: '2018/01/21', 135 | role: 'Staff', 136 | status: 'Active', 137 | }, 138 | { 139 | id: 11, 140 | name: 'Carwyn Fachtna', 141 | registered: '2018/01/01', 142 | role: 'Member', 143 | status: 'Active', 144 | }, 145 | { 146 | id: 12, 147 | name: 'Nehemiah Tatius', 148 | registered: '2018/02/01', 149 | role: 'Staff', 150 | status: 'Banned', 151 | }, 152 | { 153 | id: 13, 154 | name: 'Ebbe Gemariah', 155 | registered: '2018/02/01', 156 | role: 'Admin', 157 | status: 'Inactive', 158 | }, 159 | { 160 | id: 14, 161 | name: 'Eustorgios Amulius', 162 | registered: '2018/03/01', 163 | role: 'Member', 164 | status: 'Pending', 165 | }, 166 | { 167 | id: 15, 168 | name: 'Leopold Gáspár', 169 | registered: '2018/01/21', 170 | role: 'Staff', 171 | status: 'Active', 172 | }, 173 | { 174 | id: 16, 175 | name: 'Pompeius René', 176 | registered: '2018/01/01', 177 | role: 'Member', 178 | status: 'Active', 179 | }, 180 | { 181 | id: 17, 182 | name: 'Paĉjo Jadon', 183 | registered: '2018/02/01', 184 | role: 'Staff', 185 | status: 'Banned', 186 | }, 187 | { 188 | id: 18, 189 | name: 'Micheal Mercurius', 190 | registered: '2018/02/01', 191 | role: 'Admin', 192 | status: 'Inactive', 193 | }, 194 | { 195 | id: 19, 196 | name: 'Ganesha Dubhghall', 197 | registered: '2018/03/01', 198 | role: 'Member', 199 | status: 'Pending', 200 | }, 201 | { 202 | id: 20, 203 | name: 'Hiroto Šimun', 204 | registered: '2018/01/21', 205 | role: 'Staff', 206 | status: 'Active', 207 | }, 208 | { 209 | id: 21, 210 | name: 'Vishnu Serghei', 211 | registered: '2018/01/01', 212 | role: 'Member', 213 | status: 'Active', 214 | }, 215 | { 216 | id: 22, 217 | name: 'Zbyněk Phoibos', 218 | registered: '2018/02/01', 219 | role: 'Staff', 220 | status: 'Banned', 221 | }, 222 | { 223 | id: 23, 224 | name: 'Aulus Agmundr', 225 | registered: '2018/01/01', 226 | role: 'Member', 227 | status: 'Pending', 228 | }, 229 | { 230 | id: 42, 231 | name: 'Ford Prefect', 232 | registered: '2001/05/25', 233 | role: 'Alien', 234 | status: "Don't panic!", 235 | }, 236 | ] 237 | const getBadge = status => { 238 | switch (status) { 239 | case 'Active': 240 | return 'success' 241 | case 'Inactive': 242 | return 'secondary' 243 | case 'Pending': 244 | return 'warning' 245 | case 'Banned': 246 | return 'danger' 247 | default: 248 | return 'primary' 249 | } 250 | } 251 | const toggleDetails = index => { 252 | const position = details.indexOf(index) 253 | let newDetails = details.slice() 254 | if (position !== -1) { 255 | newDetails.splice(position, 1) 256 | } else { 257 | newDetails = [...details, index] 258 | } 259 | setDetails(newDetails) 260 | } 261 | return ( 262 | { 266 | console.log(item) 267 | console.log(index) 268 | console.log(columnName) 269 | console.log(event) 270 | }} 271 | tableProps={{ 272 | striped: true, 273 | hover: true, 274 | }} 275 | tableHeadProps={{ 276 | color: 'danger', 277 | }} 278 | activePage={3} 279 | footer 280 | items={usersData} 281 | columns={columns} 282 | columnFilter 283 | tableFilter 284 | cleaner 285 | itemsPerPageSelect 286 | itemsPerPage={5} 287 | columnSorter 288 | pagination 289 | scopedColumns={{ 290 | status: item => ( 291 | 292 | {item.status} 293 | 294 | ), 295 | show_details: item => { 296 | return ( 297 | 298 | { 304 | toggleDetails(item.id) 305 | }} 306 | > 307 | {details.includes(item.id) ? 'Hide' : 'Show'} 308 | 309 | 310 | ) 311 | }, 312 | details: item => { 313 | return ( 314 | 315 | 316 |

{item.username}

317 |

User since: {item.registered}

318 | 319 | User Settings 320 | 321 | 322 | Delete 323 | 324 |
325 |
326 | ) 327 | }, 328 | }} 329 | /> 330 | ) 331 | } 332 | ``` 333 | ## API 334 | | Props | Type | Default values | 335 | | ------------- | ------------- | ------------- | 336 | | activePage | number OR undefined | 1 | 337 | | className | string or undefined | | 338 | | clickableRows | boolean | | 339 | | columnFilterValue | ColumnFilterValue |undefined | 340 | | footer | boolean |undefined | 341 | | header | boolean | undefined | 342 | | itemsPerPage | number | | 343 | | itemsPerPageLabel | string | 10 | 344 | | itemsPerPageOptions | number[] | [5, 10, 20, 50] | 345 | | loading | boolean | | 346 | | noItemsLabel | ReactNode | | 347 | | onActivePageChange | ((value: number) => void) | | 348 | | onColumnFilterChange | ((value: ColumnFilterValue) => void) | | 349 | | onFilteredItemsChange | ((items: Item[]) => void) | | 350 | | onItemsPerPageChange | ((value: number) => void) | | 351 | | onRowClick | ((item: Item, index: number, columnName: string, event: boolean => void) | | 352 | | onSorterChange | ((value: SorterValue) => void) | | 353 | | onTableFilterChange | ((value?: string | undefined) => void) | | 354 | | pagination | boolean | | 355 | | tableFilterLabel | string | | 356 | | tableFilterPlaceholder | string | | 357 | | tableFilterValue | string | | 358 | 359 | 360 | ## Contributors 361 | 362 | Super Responsive Data table Tables are made possible by these great community members: 363 | 364 | - [dzungdinh94](https://github.com/dzungdinh94) 365 | 366 | ## Contributing 367 | 368 | Please help turn the tables on unresponsive data! Submit an issue and/or make a pull request. Check the [projects board](https://github.com/coston/react-super-responsive-data-table/projects) for tasks to do. 369 | 370 | ## License 371 | 372 | Licensed under the MIT license. -------------------------------------------------------------------------------- /ResponsiveTable.css: -------------------------------------------------------------------------------- 1 | /* inspired by: https://css-tricks.com/responsive-data-tables/ */ 2 | .responsiveTable { 3 | width: 100%; 4 | } 5 | 6 | .responsiveTable td .tdBefore { 7 | display: none; 8 | } 9 | 10 | @media screen and (max-width: 40em) { 11 | /* 12 | Force table elements to not behave like tables anymore 13 | Hide table headers (but not display: none;, for accessibility) 14 | */ 15 | 16 | .responsiveTable table, 17 | .responsiveTable thead, 18 | .responsiveTable tbody, 19 | .responsiveTable th, 20 | .responsiveTable td, 21 | .responsiveTable tr { 22 | display: block; 23 | } 24 | 25 | .responsiveTable thead tr { 26 | position: absolute; 27 | top: -9999px; 28 | left: -9999px; 29 | border-bottom: 2px solid #333; 30 | } 31 | 32 | .responsiveTable tbody tr { 33 | flex-direction: column; 34 | min-width: 0; 35 | margin-bottom: 1.5rem; 36 | word-wrap: break-word; 37 | background-clip: border-box; 38 | border: 1px solid; 39 | border-radius: 0.25rem; 40 | background-color: #fff; 41 | border-color: #d8dbe0; 42 | } 43 | 44 | .responsiveTable td.pivoted { 45 | /* Behave like a "row" */ 46 | border: none !important; 47 | position: relative; 48 | padding-left: calc(50% + 10px) !important; 49 | text-align: left !important; 50 | white-space: pre-wrap; 51 | overflow-wrap: break-word; 52 | } 53 | 54 | .responsiveTable td .tdBefore { 55 | /* Now like a table header */ 56 | position: absolute; 57 | display: block; 58 | 59 | /* Top/left values mimic padding */ 60 | left: 1rem; 61 | width: calc(50% - 20px); 62 | white-space: pre-wrap; 63 | overflow-wrap: break-word; 64 | text-align: left !important; 65 | font-weight: 600; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-super-responsive-data-table", 3 | "version": "1.0.0", 4 | "description": "Dynamic table component.", 5 | "main": "AdvancedTable.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/dzungdinh94/react-super-responsive-data-table.git" 12 | }, 13 | "keywords": [ 14 | "react-super-responsive-data-table", 15 | "react-table", 16 | "responsive-table" 17 | ], 18 | "dependencies": { 19 | "@coreui/icons": "2.0.0-beta.4", 20 | "@coreui/react": "^3.0.1" 21 | }, 22 | "author": "dzungdinh94", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/dzungdinh94/react-super-responsive-data-table/issues" 26 | }, 27 | "homepage": "https://github.com/dzungdinh94/react-super-responsive-data-table#readme" 28 | } 29 | -------------------------------------------------------------------------------- /super-responsive-table/SuperResponsiveTableStyle.css: -------------------------------------------------------------------------------- 1 | /* inspired by: https://css-tricks.com/responsive-data-tables/ */ 2 | .responsiveTable { 3 | width: 100%; 4 | } 5 | 6 | .responsiveTable td .tdBefore { 7 | display: none; 8 | } 9 | 10 | @media screen and (max-width: 50em) { 11 | /* 12 | Force table elements to not behave like tables anymore 13 | Hide table headers (but not display: none;, for accessibility) 14 | */ 15 | 16 | .responsiveTable table, 17 | .responsiveTable thead, 18 | .responsiveTable tbody, 19 | .responsiveTable th, 20 | .responsiveTable td, 21 | .responsiveTable tr { 22 | display: block; 23 | } 24 | 25 | .responsiveTable thead tr { 26 | position: absolute; 27 | top: -9999px; 28 | left: -9999px; 29 | border-bottom: 2px solid #333; 30 | } 31 | 32 | .responsiveTable tbody tr { 33 | border: 1px solid #000; 34 | padding: .25em; 35 | } 36 | 37 | .responsiveTable td.pivoted { 38 | /* Behave like a "row" */ 39 | border: none !important; 40 | position: relative; 41 | padding-left: calc(50% + 10px) !important; 42 | text-align: left !important; 43 | white-space: pre-wrap; 44 | overflow-wrap: break-word; 45 | } 46 | 47 | .responsiveTable td .tdBefore { 48 | /* Now like a table header */ 49 | position: absolute; 50 | display: block; 51 | 52 | /* Top/left values mimic padding */ 53 | left: 1rem; 54 | width: calc(50% - 20px); 55 | white-space: pre-wrap; 56 | overflow-wrap: break-word; 57 | text-align: left !important; 58 | font-weight: 600; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /super-responsive-table/components/Table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import T from 'prop-types'; 3 | 4 | import { Provider } from '../utils/tableContext'; 5 | 6 | import allowed from '../utils/allowed'; 7 | 8 | class Table extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | headers: {}, 13 | }; 14 | } 15 | 16 | render() { 17 | const { headers } = this.state; 18 | const { className, forwardedRef } = this.props; 19 | const classes = `${className || ''} responsiveTable`; 20 | 21 | return ( 22 | 23 | 29 | 30 | ); 31 | } 32 | } 33 | 34 | Table.propTypes = { 35 | className: T.string, 36 | forwardedRef: T.oneOfType([ 37 | T.func, 38 | T.shape({ current: T.instanceOf(global.Element) }), 39 | ]), 40 | }; 41 | 42 | Table.defaultProps = { 43 | className: undefined, 44 | forwardedRef: undefined, 45 | }; 46 | 47 | const TableForwardRef = React.forwardRef((props, ref) => ( 48 |
49 | )); 50 | 51 | TableForwardRef.displayName = Table.name; 52 | 53 | export default TableForwardRef; 54 | -------------------------------------------------------------------------------- /super-responsive-table/components/Tbody.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import allowed from '../utils/allowed'; 4 | 5 | const Tbody = (props) => ; 6 | 7 | export default Tbody; 8 | -------------------------------------------------------------------------------- /super-responsive-table/components/Td.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Consumer } from '../utils/tableContext'; 3 | import TdInner from './TdInner'; 4 | 5 | const Td = (props) => ( 6 | {(headers) => } 7 | ); 8 | 9 | export default Td; 10 | -------------------------------------------------------------------------------- /super-responsive-table/components/TdInner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import T from 'prop-types'; 3 | 4 | import allowed from '../utils/allowed'; 5 | 6 | const TdInner = (props) => { 7 | const { headers, children, columnKey, className, colSpan } = props; 8 | 9 | const classes = `${className || ''} pivoted`; 10 | if (colSpan) { 11 | return 20 | ); 21 | }; 22 | 23 | TdInner.propTypes = { 24 | children: T.node, 25 | headers: T.shape({}), 26 | columnKey: T.number, 27 | className: T.string, 28 | colSpan: T.oneOfType([T.number, T.string]), 29 | }; 30 | 31 | TdInner.defaultProps = { 32 | children: undefined, 33 | headers: undefined, 34 | columnKey: undefined, 35 | className: undefined, 36 | colSpan: undefined, 37 | }; 38 | 39 | export default TdInner; 40 | -------------------------------------------------------------------------------- /super-responsive-table/components/Th.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import allowed from '../utils/allowed'; 4 | 5 | const Th = (props) => 11 | {React.cloneElement(children, { inHeader: true })} 12 | 13 | ); 14 | }; 15 | 16 | Thead.propTypes = { 17 | children: T.node, 18 | }; 19 | 20 | Thead.defaultProps = { 21 | children: undefined, 22 | }; 23 | 24 | export default Thead; 25 | -------------------------------------------------------------------------------- /super-responsive-table/components/Tr.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Consumer } from '../utils/tableContext'; 4 | import TrInner from './TrInner'; 5 | 6 | const Tr = (props) => ( 7 | {(headers) => } 8 | ); 9 | 10 | export default Tr; 11 | -------------------------------------------------------------------------------- /super-responsive-table/components/TrInner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import T from 'prop-types'; 3 | 4 | import allowed from '../utils/allowed'; 5 | 6 | class TrInner extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | const { headers } = props; 10 | if (headers && props.inHeader) { 11 | React.Children.map(props.children, (child, i) => { 12 | if (child) { 13 | headers[i] = child.props.children; 14 | } 15 | }); 16 | } 17 | } 18 | 19 | render() { 20 | const { children } = this.props; 21 | return ( 22 | 23 | {children && 24 | React.Children.map( 25 | children, 26 | (child, i) => 27 | child && 28 | React.cloneElement(child, { 29 | // eslint-disable-next-line react/no-array-index-key 30 | key: i, 31 | columnKey: i, 32 | }) 33 | )} 34 | 35 | ); 36 | } 37 | } 38 | 39 | TrInner.propTypes = { 40 | children: T.node, 41 | headers: T.shape({}), 42 | inHeader: T.bool, 43 | }; 44 | 45 | TrInner.defaultProps = { 46 | children: undefined, 47 | headers: undefined, 48 | inHeader: undefined, 49 | }; 50 | 51 | export default TrInner; 52 | -------------------------------------------------------------------------------- /super-responsive-table/index.js: -------------------------------------------------------------------------------- 1 | import Table from './components/Table'; 2 | import Tbody from './components/Tbody'; 3 | import Td from './components/Td'; 4 | import Th from './components/Th'; 5 | import Thead from './components/Thead'; 6 | import Tr from './components/Tr'; 7 | 8 | export { Table, Tbody, Td, Th, Thead, Tr }; 9 | -------------------------------------------------------------------------------- /super-responsive-table/utils/allowed.js: -------------------------------------------------------------------------------- 1 | const omit = (obj, omitProps) => 2 | Object.keys(obj) 3 | .filter((key) => omitProps.indexOf(key) === -1) 4 | .reduce((returnObj, key) => ({ ...returnObj, [key]: obj[key] }), {}); 5 | 6 | const allowed = (props) => 7 | omit(props, ['inHeader', 'columnKey', 'headers', 'forwardedRef']); 8 | 9 | export default allowed; 10 | -------------------------------------------------------------------------------- /super-responsive-table/utils/tableContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { Provider, Consumer } = React.createContext({}); 4 | export { Provider, Consumer }; 5 | --------------------------------------------------------------------------------
; 12 | } 13 | return ( 14 | 15 |
16 | {headers[columnKey]} 17 |
18 | {children ??
 
} 19 |
; 6 | 7 | export default Th; 8 | -------------------------------------------------------------------------------- /super-responsive-table/components/Thead.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import T from 'prop-types'; 3 | 4 | import allowed from '../utils/allowed'; 5 | 6 | const Thead = (props) => { 7 | const { children } = props; 8 | 9 | return ( 10 |