-
-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reusable table component and example #149
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { Table } from 'core/table/table'; | ||
|
||
export const Table = { Table }; |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,268 @@ | ||||||||||
import React, { useState, useMemo } from 'react'; | ||||||||||
import PropTypes from 'prop-types' | ||||||||||
import _ from 'lodash'; | ||||||||||
|
||||||||||
import Box from '@mui/material/Box'; | ||||||||||
import { | ||||||||||
Table as MuiTable, | ||||||||||
TableBody, | ||||||||||
TableCell, | ||||||||||
TableContainer, | ||||||||||
TableHead, | ||||||||||
TablePagination, | ||||||||||
TableRow, | ||||||||||
TableSortLabel, | ||||||||||
Paper, | ||||||||||
Checkbox, | ||||||||||
Radio, | ||||||||||
CircularProgress | ||||||||||
} from '@mui/material'; | ||||||||||
import { visuallyHidden } from '@mui/utils'; | ||||||||||
|
||||||||||
/* | ||||||||||
@columns: Array of objects | ||||||||||
@column: { | ||||||||||
id: Unique identifier | ||||||||||
accessor: Property to be accessed | ||||||||||
label: Header label | ||||||||||
template: custom template | ||||||||||
} | ||||||||||
*/ | ||||||||||
|
||||||||||
export const Table = ({ | ||||||||||
data, | ||||||||||
columns = [], | ||||||||||
loading = false, | ||||||||||
onRowClick = _.noop, | ||||||||||
onRowSelect = _.noop, | ||||||||||
defaultOrder = 'asc', | ||||||||||
defaultOrderBy = '', | ||||||||||
multiSelect = false | ||||||||||
}) => { | ||||||||||
const [order, setOrder] = useState(defaultOrder); | ||||||||||
const [orderBy, setOrderBy] = useState(defaultOrderBy); | ||||||||||
const [selected, setSelected] = React.useState([]); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. useState is already imported
Suggested change
|
||||||||||
const [page, setPage] = React.useState(0); | ||||||||||
const [rowsPerPage, setRowsPerPage] = React.useState(5); | ||||||||||
|
||||||||||
const processedData = useMemo(() => { | ||||||||||
return data.map(entry => { | ||||||||||
return { | ||||||||||
original: entry, | ||||||||||
_id: _.uniqueId('table__entry__') | ||||||||||
}; | ||||||||||
}); | ||||||||||
}, [data]); | ||||||||||
|
||||||||||
const processedColumns = useMemo(() => columns.filter(({ show = true }) => show)); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This hook will always run because it has no dependencies, making memoizing not useful |
||||||||||
|
||||||||||
const handleRequestSort = (event, property) => { | ||||||||||
const isAsc = orderBy === property && order === 'asc'; | ||||||||||
setOrder(isAsc ? 'desc' : 'asc'); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could there be an "enum" for const orderType = {
asc: 'asc',
desc: 'desc'
}; |
||||||||||
setOrderBy(property); | ||||||||||
}; | ||||||||||
|
||||||||||
const handleClick = (event, rawInfo) => { | ||||||||||
const selectedIndex = selected.findIndex(entry => entry._id === rawInfo._id); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could use the |
||||||||||
let newSelected = []; | ||||||||||
|
||||||||||
if (selectedIndex === -1) { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this logic be moved to a different function |
||||||||||
newSelected = newSelected.concat(selected, rawInfo); | ||||||||||
} else if (selectedIndex === 0) { | ||||||||||
newSelected = newSelected.concat(selected.slice(1)); | ||||||||||
} else if (selectedIndex === selected.length - 1) { | ||||||||||
newSelected = newSelected.concat(selected.slice(0, -1)); | ||||||||||
} else if (selectedIndex > 0) { | ||||||||||
newSelected = newSelected.concat( | ||||||||||
selected.slice(0, selectedIndex), | ||||||||||
selected.slice(selectedIndex + 1) | ||||||||||
); | ||||||||||
} | ||||||||||
|
||||||||||
if (!multiSelect) { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check should be run before the rest, as from the reader standpoint doesnt make sense to go through 4 checks so that in the end is reasigned |
||||||||||
newSelected = [rawInfo]; | ||||||||||
} | ||||||||||
|
||||||||||
setSelected(newSelected); | ||||||||||
onRowSelect(newSelected); | ||||||||||
onRowClick(rawInfo); | ||||||||||
}; | ||||||||||
|
||||||||||
const createSortHandler = (property) => (event) => { | ||||||||||
handleRequestSort(event, property); | ||||||||||
}; | ||||||||||
|
||||||||||
const isSelected = (id) => selected.findIndex((entry) => entry._id === id) !== -1; | ||||||||||
|
||||||||||
const emptyRows = page > 0 ? Math.max(0, (1 + page) * rowsPerPage - processedData.length) : 0; | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Math.max is already going establish a lower limit to
Suggested change
|
||||||||||
|
||||||||||
const renderTableHead = () => { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Many of these functions could be placed in a separate file, as it clogs this whole file |
||||||||||
return ( | ||||||||||
<TableHead> | ||||||||||
<TableRow> | ||||||||||
<TableCell></TableCell> | ||||||||||
{processedColumns.map(({ id, accessor = '', label, style }) => { | ||||||||||
return ( | ||||||||||
<TableCell | ||||||||||
sx={{ ...style }} | ||||||||||
key={id} | ||||||||||
sortDirection={orderBy === accessor ? order : false}> | ||||||||||
<TableSortLabel | ||||||||||
active={orderBy === accessor} | ||||||||||
direction={orderBy === accessor ? order : 'asc'} | ||||||||||
onClick={createSortHandler(accessor)} | ||||||||||
> | ||||||||||
{label} | ||||||||||
{orderBy === accessor ? ( | ||||||||||
<Box component="span" sx={visuallyHidden}> | ||||||||||
{order === 'desc' ? 'sorted descending' : 'sorted ascending'} | ||||||||||
</Box> | ||||||||||
) : null} | ||||||||||
</TableSortLabel> | ||||||||||
</TableCell> | ||||||||||
) | ||||||||||
})} | ||||||||||
</TableRow> | ||||||||||
</TableHead> | ||||||||||
) | ||||||||||
} | ||||||||||
|
||||||||||
const handleChangePage = (event, newPage) => { | ||||||||||
setPage(newPage); | ||||||||||
}; | ||||||||||
|
||||||||||
const handleChangeRowsPerPage = (event) => { | ||||||||||
setRowsPerPage(parseInt(event.target.value, 10)); | ||||||||||
setPage(0); | ||||||||||
}; | ||||||||||
|
||||||||||
function descendingComparator(a, b, orderBy) { | ||||||||||
if (b[orderBy] < a[orderBy]) { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic could be simplified to
Suggested change
But further, the below function could be simplified to const scalar = order === "desc" ? 1 : -1;
return scalar * (b.original[orderBy] - a.original[orderBy]); And this logic as they are pure functions, they could be entirely moved to a helper file with some test suite |
||||||||||
return -1; | ||||||||||
} | ||||||||||
if (b[orderBy] > a[orderBy]) { | ||||||||||
return 1; | ||||||||||
} | ||||||||||
return 0; | ||||||||||
} | ||||||||||
|
||||||||||
function getComparator(order, orderBy) { | ||||||||||
return order === "desc" | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see there is a code repetition, it could be shortened to:
Suggested change
|
||||||||||
? (a, b) => descendingComparator(a.original, b.original, orderBy) | ||||||||||
: (a, b) => -descendingComparator(a.original, b.original, orderBy); | ||||||||||
} | ||||||||||
|
||||||||||
function stableSort(array, comparator) { | ||||||||||
const stabilizedThis = array.map((el, index) => [el, index]); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is possible to chain these methods, such as (also added an alternative to the double return) array
.map((el, index) => [el, index])
.sort((a, b) => {
const elementOrder = comparator(a[0], b[0]);
const decisiveOrder = elementOrder === 0 ? a[1] - b[1] : elementOrder;
return decisiveOrder;
})
.map((el) => el[0]); |
||||||||||
stabilizedThis.sort((a, b) => { | ||||||||||
const order = comparator(a[0], b[0]); | ||||||||||
if (order !== 0) { | ||||||||||
return order; | ||||||||||
} | ||||||||||
return a[1] - b[1]; | ||||||||||
}); | ||||||||||
return stabilizedThis.map((el) => el[0]); | ||||||||||
} | ||||||||||
|
||||||||||
const renderSelectCell = (checked, labelId) => { | ||||||||||
const multiSelectCell = ( | ||||||||||
<TableCell padding="checkbox"> | ||||||||||
<Checkbox | ||||||||||
color="primary" | ||||||||||
checked={checked} | ||||||||||
inputProps={{ | ||||||||||
'aria-labelledby': labelId, | ||||||||||
}} | ||||||||||
/> | ||||||||||
</TableCell> | ||||||||||
); | ||||||||||
|
||||||||||
const singleSelectCell = ( | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure if by assigning this component straight to the const React will execute (not necessary render) code that is inside of it, it is worth to check. Also the code |
||||||||||
<TableCell padding="checkbox"> | ||||||||||
<Radio | ||||||||||
checked={checked} | ||||||||||
name="radio-button" | ||||||||||
inputProps={{ 'aria-labelledby': labelId }} | ||||||||||
/> | ||||||||||
</TableCell> | ||||||||||
); | ||||||||||
return multiSelect ? multiSelectCell : singleSelectCell; | ||||||||||
} | ||||||||||
|
||||||||||
const renderTableBody = () => { | ||||||||||
return ( | ||||||||||
<TableBody> | ||||||||||
{stableSort(processedData, getComparator(order, orderBy)) | ||||||||||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | ||||||||||
.map((row, index) => { | ||||||||||
const isItemSelected = isSelected(row._id); | ||||||||||
const labelId = `enhanced-table-checkbox-${index}`; | ||||||||||
|
||||||||||
return ( | ||||||||||
<TableRow | ||||||||||
hover | ||||||||||
onClick={(event) => handleClick(event, row)} | ||||||||||
role="checkbox" | ||||||||||
aria-checked={isItemSelected} | ||||||||||
tabIndex={-1} | ||||||||||
key={row.name} | ||||||||||
selected={isItemSelected} | ||||||||||
sx={{ height: '100px' }} | ||||||||||
> | ||||||||||
{onRowSelect !== _.noop && renderSelectCell(isItemSelected, labelId)} | ||||||||||
{processedColumns.map(column => { | ||||||||||
const Template = column.template; | ||||||||||
|
||||||||||
|
||||||||||
return Template ? ( | ||||||||||
<TableCell> | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TableCell could wrap around the actual alternatives
Suggested change
|
||||||||||
<Template row={row} /> | ||||||||||
</TableCell> | ||||||||||
) : ( | ||||||||||
<TableCell> | ||||||||||
{row.original[column.accessor]} | ||||||||||
</TableCell> | ||||||||||
) | ||||||||||
})} | ||||||||||
</TableRow> | ||||||||||
); | ||||||||||
})} | ||||||||||
{emptyRows > 0 && ( | ||||||||||
<TableRow> | ||||||||||
<TableCell colSpan={6} /> | ||||||||||
</TableRow> | ||||||||||
)} | ||||||||||
</TableBody> | ||||||||||
) | ||||||||||
} | ||||||||||
|
||||||||||
const tableContainer = ( | ||||||||||
<> | ||||||||||
<TableContainer> | ||||||||||
<MuiTable sx={{ minWidth: 750, height: 500 }}> | ||||||||||
{renderTableHead()} | ||||||||||
{renderTableBody()} | ||||||||||
</MuiTable> | ||||||||||
</TableContainer> | ||||||||||
<TablePagination | ||||||||||
rowsPerPageOptions={[5, 10, 25]} | ||||||||||
component="div" | ||||||||||
count={processedData.length} | ||||||||||
rowsPerPage={rowsPerPage} | ||||||||||
page={page} | ||||||||||
onPageChange={handleChangePage} | ||||||||||
onRowsPerPageChange={handleChangeRowsPerPage} | ||||||||||
/> | ||||||||||
</> | ||||||||||
) | ||||||||||
|
||||||||||
return ( | ||||||||||
<Box sx={{ width: '100%' }}> | ||||||||||
<Paper sx={{ width: '100%', mb: 2 }}> | ||||||||||
{loading ? <div style={{height: 500, display: 'flex', justifyContent: 'center', alignItems: 'center'}}><CircularProgress /></div> : tableContainer} | ||||||||||
</Paper> | ||||||||||
</Box> | ||||||||||
) | ||||||||||
}; | ||||||||||
|
||||||||||
Table.propTypes = { columns: PropTypes.array, data: PropTypes.array } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import React from 'react'; | ||
import { Link } from '@mui/material'; | ||
import { Table } from './table'; | ||
|
||
export const TableExample = () => { | ||
const genericAction = (event, action, row) => { | ||
console.log(`${action} `, row) | ||
event.stopPropagation(); | ||
} | ||
|
||
const columns = [ | ||
{ | ||
id: 'lessonName', | ||
accessor: 'lessonName', | ||
label: 'DENUMIRE LECTIE' | ||
}, | ||
{ | ||
id: 'category', | ||
accessor: 'category', | ||
label: 'CATEGORIE', | ||
}, | ||
{ | ||
id: 'status', | ||
accessor: 'status', | ||
label: 'STATUS', | ||
}, | ||
{ | ||
id: 'actions', | ||
label: 'ACTIUNI', | ||
template: ({row}) => { | ||
return ( | ||
<div style={{ | ||
display: 'flex', | ||
justifyContent: 'space-between', | ||
}}> | ||
<Link underline="none" onClick={(e) => genericAction(e, 'EDIT: ', row)}>Editeaza</Link> | ||
<Link underline="none" onClick={(e) => genericAction(e, 'DUPLICATE: ', row)}>Duplica lectie</Link> | ||
<Link underline="none" onClick={(e) => genericAction(e, 'DELETE: ', row)}>Sterge</Link> | ||
</div> | ||
) | ||
}, | ||
style: { | ||
minWidth: '180px', | ||
} | ||
} | ||
]; | ||
|
||
const data = [ | ||
{ | ||
lessonName: 'Asertivitate si managementul conflictelor1', | ||
category: 'One', | ||
status: 'Publicata', | ||
}, | ||
{ | ||
lessonName: 'Asertivitate si managementul conflictelor2', | ||
category: 'One', | ||
status: 'Publicata', | ||
}, | ||
{ | ||
lessonName: 'Asertivitate si managementul conflictelor3', | ||
category: 'One', | ||
status: 'Publicata', | ||
}, | ||
{ | ||
lessonName: 'Asertivitate si managementul conflictelor4', | ||
category: 'One', | ||
status: 'Publicata', | ||
}, | ||
{ | ||
lessonName: 'Asertivitate si managementul conflictelor5', | ||
category: 'One', | ||
status: 'Publicata', | ||
}, | ||
{ | ||
lessonName: 'Asertivitate si managementul conflictelor6', | ||
category: 'One', | ||
status: 'Publicata', | ||
} | ||
] | ||
|
||
return ( | ||
<Table | ||
columns={columns} | ||
data={data} | ||
onRowSelect={rows => console.log(rows)} | ||
onRowClick={row => console.log(row)} | ||
loading={false} | ||
defaultOrder={'asc'} | ||
defaultOrderBy={'lessonName'} | ||
multiSelect={false} | ||
/> | ||
) | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If possible we could import functions individually, so that it can be tree-shaked