I am trying to sort a table (ascending/descending) when my table header data is placed inside a .map() method (see line 73 of the codesandbox link in this post).
I can get the hand emoji to change onClick, but no sorting takes place. I think it may have something to do with passing an object to the map method, and not an individual string or numeric value as found in the codesandbox that I have based this functionality on. Here is the original sandbox that I am modelling after:
https://codesandbox.io/embed/table-sorting-example-ur2z9?fontsize=14&hidenavigation=1&theme=dark
...and here is my codesandbox with my data structure that I need to sort:
https://codesandbox.io/s/table-sorting-example-forked-dofhj?file=/src/App.js
What can I change in order to get each column to be sortable? Any help/advice would be greatly appreciated.
App.js
import React from "react";
import "./styles.css";
import {
Button,
Table,
Thead,
Tbody,
Flex,
Tooltip,
Tr,
Th,
Td
} from "#chakra-ui/react";
//Table Headers
const TABLE_HEADERS = [
{ name: "ID Number" },
{ name: "User Type" },
{ name: "User Category" },
{ name: "User Interest" }
];
const useSortableData = (items, config = null) => {
const [sortConfig, setSortConfig] = React.useState(config);
const sortedItems = React.useMemo(() => {
let sortableItems = [...items];
if (sortConfig !== null) {
sortableItems.sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === "ascending" ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === "ascending" ? 1 : -1;
}
return 0;
});
}
return sortableItems;
}, [items, sortConfig]);
const requestSort = (key) => {
let direction = "ascending";
if (
sortConfig &&
sortConfig.key === key &&
sortConfig.direction === "ascending"
) {
direction = "descending";
}
setSortConfig({ key, direction });
};
return { items: sortedItems, requestSort, sortConfig };
};
const ProductTable = (props) => {
const { items, requestSort, sortConfig } = useSortableData(
props.myUserErrorTypes
);
const getClassNamesFor = (name) => {
if (!sortConfig) {
return;
}
return sortConfig.key === name ? sortConfig.direction : undefined;
};
return (
<Table>
<caption>User Error Types</caption>
<Thead>
<Tr>
{TABLE_HEADERS.map(({ name, description, isNumeric }) => (
<Th key={name} isNumeric={isNumeric}>
<Button
type="button"
onClick={() => requestSort(name)}
className={getClassNamesFor(name)}
>
<Tooltip label={description} aria-label={description}>
{name}
</Tooltip>
</Button>
</Th>
))}
</Tr>
</Thead>
<Tbody>
{items.map((error) => {
const { userNumber, userType, errorId, errorCategory } = error;
return (
<React.Fragment key={errorId}>
<Tr id={errorId} key={errorId}>
<Td>{userNumber}</Td>
<Td>{userType}</Td>
<Td>{errorId}</Td>
<Td>{errorCategory}</Td>
<Td textAlign="center">
<Flex justify="justifyContent"></Flex>
</Td>
</Tr>
</React.Fragment>
);
})}
</Tbody>
</Table>
);
};
export default function App() {
return (
<div className="App">
<ProductTable
myUserErrorTypes={[
{
userNumber: 1234567890,
userType: "SuperUser",
errorId: 406,
errorCategory: "In-Progress"
},
{
userNumber: 4859687937,
userType: "NewUser",
errorId: 333,
errorCategory: "Complete"
}
]}
/>
</div>
);
}
The items are being sorted by the table header name (e.g. 'ID Number') since you're calling requestSort with the name. Add an id property to the objects in the TABLE_HEADERS array matching the property name (e.g. userNumber) in the data objects and pass it as an argument to both the requestSort and getClassNamesFor functions.
const TABLE_HEADERS = [
{ name: 'ID Number', id: 'userNumber' },
{ name: 'User Type', id: 'userType' },
{ name: 'User Category', id: 'errorId' },
{ name: 'User Interest', id: 'errorCategory' },
]
{
TABLE_HEADERS.map(({ name, id }) => (
<Th key={id}>
<Button
type="button"
onClick={() => requestSort(id)}
className={getClassNamesFor(id)}
>
{name}
</Button>
</Th>
))
}
You're also trying to use the description and isNumeric values from the header object but they are both undefined. You might want to add those properties to the objects in the TABLE_HEADERS array.
Related
I'm using react-table v7.8.0 to create a table with 4 columns. I'd like the the table to be sorted by columns date and views. My problem is that the table is only being sorted by the date column.
I have some pagination and manual sorting that appears to be working as expected. I think I've followed the documentation and I'm unsure where my mistake is.
The data is configured in the following file before being passed to a table component:
ViewsPageDayTable.js:
const postDateTemplate = { year: "numeric", month: "short", day: "numeric" }
import Link from "#/components/Link"
import { useMemo } from "react"
import siteMetadata from "#/data/siteMetadata"
import Table from "#/components/homeBrewAnalytics/table"
import LocationCountItem from "#/components/homeBrewAnalytics/locationCountItem"
export default function ViewsPageDayTable({ data }) {
const parsedData = []
for (var i = 0; i < data.length; i++) {
const dataRow = {}
for (var j in data[i]) {
if (j === "date") {
var date = new Date(data[i][j])
dataRow["date"] = date.toLocaleDateString(siteMetadata.locale, postDateTemplate)
} else if (j === "page") {
dataRow["page"] = data[i][j]
} else if (j === "country_count") {
var value = 0
if (data[i][j] == null) {
value = "unknown"
dataRow["country_count"] = null
} else {
value = data[i][j]
value = value.replaceAll("(", "[").replaceAll(")", "]").replaceAll("'", '"')
value = JSON.parse(value)
dataRow["country_count"] = value
}
} else if (j === "views") {
dataRow["views"] = parseInt(data[i][j])
}
}
parsedData.push(dataRow)
}
const PageCellProcessor = ({ value, row: { index }, column: { id } }) => {
return (
<Link href={value} className="hover:underline">
{" "}
{value}{" "}
</Link>
)
}
const LocationCellProcessor = ({ value }) => {
if (value == null) return <div></div>
const result = []
for (var z = 0; z < value.length; z++) {
result.push(<LocationCountItem value={value[z]} />)
}
return (
<div key={value} className="flex flex-shrink">
{result}{" "}
</div>
)
}
const columns = useMemo(
() => [
{
Header: "Date",
accessor: "date",
sortType: (a, b) => {
return new Date(b) - new Date(a)
},
},
{
Header: "Page",
accessor: "page",
Cell: PageCellProcessor,
},
{
Header: "Views",
accessor: "views",
sortType: 'basic',
},
{
Header: "Location",
accessor: "country_count",
Cell: LocationCellProcessor,
},
],
[]
)
return (
<div
id="viewsPerPagePerDay"
className="min-h-32 col-span-3 border-separate border-2 border-slate-800 p-3"
>
<Table columns={columns} data={parsedData} />
</div>
)
}
Table.js:
import { React, useMemo } from "react"
import { useTable, useFilters, useSortBy, usePagination } from "react-table"
import { useState } from "react"
export default function Table({ columns, data, isPaginated = true }) {
const [filterInput, setFilterInput] = useState("")
const handleFilterChange = (e) => {
const value = e.target.value || undefined
setFilter("page", value)
setFilterInput(value)
}
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
prepareRow,
setFilter,
canPreviousPage,
canNextPage,
pageOptions,
nextPage,
previousPage,
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data,
initialState: {
defaultCanSort: true,
disableSortBy: false,
manualSortBy: true,
sortBy: useMemo(
() => [
{
id: "date",
desc: false,
},
{
id: "views",
desc: true,
},
],
[]
),
pageIndex: 0,
pageSize: 15,
manualPagination: true,
},
},
useFilters,
useSortBy,
usePagination
)
return (
<>
<div className="flex justify-between align-middle">
<div> Page Views</div>
<input
className="mr-0 rounded-sm border border-gray-700 dark:border-gray-500 "
value={filterInput}
onChange={handleFilterChange}
placeholder={" Page name filter"}
/>
</div>
<table {...getTableProps()}>
<thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<th
{...column.getHeaderProps(column.getSortByToggleProps())}
className={
column.isSorted ? (column.isSortedDesc ? "sort-desc" : "sort-asc") : ""
}
>
{column.render("Header")}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map((row, _) => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>
})}
</tr>
)
})}
</tbody>
</table>
{Boolean(isPaginated) && (
<div id="pagination">
<div id="pageNum">
page {pageIndex + 1} of {pageOptions.length}
</div>{" "}
<div id="nextPrevButtons">
{canPreviousPage ? (
<div id="backButton" onClick={() => previousPage()}>
Previous
</div>
) : null}
{canNextPage ? (
<div id="nextButton" onClick={() => nextPage()}>
Next{" "}
</div>
) : null}
</div>
</div>
)}
</>
)
}
In my column Show there is a switch button (Toggle doesn't seems working in sandbox, maybe because of tailwindcss? but it works in local...) when you click on it, it will turn the selected row into gray (as if the row is disabled but you can still view the content).
We may have also the possibility to switch again and the original row (without gray) appears.
The VisibilityIcon button above the table will remove from all the table the gray/disabled rows (not working either). And a VisibilityoffIcon button that resets all (we get the original table).
Here what I have done but when I click on the Toggle I get errors and all the table is hidden:
export default function MenuDisplay() {
const { menuId } = useParams();
const { match } = JsonRules;
const dataFindings = match.find((el) => el._id_menu === menuId)?._ids ?? [];
const [disabled, setDisabled] = useState(false);
const toggler_disabled = () => {
disabled ? setDisabled(false) : setDisabled(true);
};
const data = useMemo(
() => [
//some headers ....
{
Header: 'Show',
accessor: (row) =>
<Toggle onClick ={toggler_disabled} value={disabled} onChange=
{setDisabled} />
}
],[]
);
...
return (
{
disabled?
<Table
data = { dataFindings }
columns = { data }
/>
: null
}
);
}
You're using useMemo under row data that memoize all rows which have the same click event without dependencies. If you want to call useMemo with an updated state, you can implement it this way
//`show` is your state
//`data` is your rows
useMemo(() => data, [show])
And the second problem is you track the show state which is only a true/false value. If you want to have multiple row states, you need to keep it as an array.
Here is the full code with some explanation (You also can check this playground)
import Table from "./Table";
import React, { useState, useMemo } from "react";
import JsonData from "./match.json";
import { useParams } from "react-router-dom";
import { Link } from "react-router-dom";
import VisibilityOffIcon from "#mui/icons-material/VisibilityOff";
import VisibilityIcon from "#mui/icons-material/Visibility";
export default function MenuDisplay() {
const { menuId } = useParams();
const { match } = JsonData;
const [hiddenRows, setHiddenRows] = useState([]);
const matchData = match.find((el) => el._id_menu === menuId)?._ids ?? [];
//update hidden row list
const updateHiddenRows = (rowId) => {
if (hiddenRows.includes(rowId)) {
//remove the current clicked row from the hidden row list
setHiddenRows(hiddenRows.filter((row) => row !== rowId));
} else {
//add the current clicked row from the hidden row list
setHiddenRows([...hiddenRows, rowId]);
}
};
const data = useMemo(() => [
{
Header: "Name",
accessor: (row) =>
//check current row is in hidden rows or not
!hiddenRows.includes(row._id) && (
<Link to={{ pathname: `/menu/${menuId}/${row._id}` }}>
{row.name}
</Link>
)
},
{
Header: "Description",
//check current row is in hidden rows or not
accessor: (row) => !hiddenRows.includes(row._id) && row.description
},
{
Header: "Dishes",
//check current row is in hidden rows or not
accessor: (row) => !hiddenRows.includes(row._id) && row.dishes,
Cell: ({ value }) => value && Object.values(value[0]).join(", ")
},
{
Header: "Show",
accessor: (row) => (
<button onClick={() => updateHiddenRows(row._id)}>
{!hiddenRows.includes(row._id) ? (
<VisibilityIcon>Show</VisibilityIcon>
) : (
<VisibilityOffIcon>Show</VisibilityOffIcon>
)}
</button>
)
}
], [hiddenRows]);
const initialState = {
sortBy: [
{ desc: false, id: "id" },
{ desc: false, id: "description" },
{ desc: false, id: "dishes" }
]
};
return (
<div>
<Table
data={matchData}
columns={data}
initialState={initialState}
withCellBorder
withRowBorder
withSorting
withPagination
/>
</div>
);
}
Keep a map of item ids that are selected and toggle these values via the Toggle component.
Use separate state for the toggle button to filter the selected items.
Implement a row props getter.
Example:
MenuDisplay
function MenuDisplay() {
const { menuId } = useParams();
const { match } = JsonData;
// toggle show/hide button
const [hideSelected, setHideSelected] = useState(false);
// select rows by item id
const [selected, setSelected] = useState({});
const rowSelectHandler = (id) => (checked) => {
setSelected((selected) => ({
...selected,
[id]: checked
}));
};
const toggleShow = () => setHideSelected((hide) => !hide);
const matchData = (
match.find((el) => el._id_menu === menuId)?._ids ?? []
).filter(({ _id }) => {
if (hideSelected) {
return !selected[_id];
}
return true;
});
const getRowProps = (row) => {
return {
style: {
backgroundColor: selected[row.values.id] ? "lightgrey" : "white"
}
};
};
const data = [
{
// add item id to row data
Header: "id",
accessor: (row) => row._id
},
{
Header: "Name",
accessor: (row) => (
<Link to={{ pathname: `/menu/${menuId}/${row._id}` }}>{row.name}</Link>
)
},
{
Header: "Description",
accessor: (row) => row.description
},
{
Header: "Dishes",
accessor: (row) => row.dishes,
id: "dishes",
Cell: ({ value }) => value && Object.values(value[0]).join(", ")
},
{
Header: "Show",
accessor: (row) => (
<Toggle
value={selected[row._id]}
onChange={rowSelectHandler(row._id)}
/>
)
}
];
const initialState = {
sortBy: [
{ desc: false, id: "id" },
{ desc: false, id: "description" }
],
hiddenColumns: ["dishes", "id"] // <-- hide id column too
};
return (
<div>
<button type="button" onClick={toggleShow}>
{hideSelected ? <VisibilityOffIcon /> : <VisibilityIcon />}
</button>
<Table
data={matchData}
columns={data}
initialState={initialState}
withCellBorder
withRowBorder
withSorting
withPagination
rowProps={getRowProps} // <-- pass rowProps getter
/>
</div>
);
}
Table
export default function Table({
className,
data,
columns,
initialState,
withCellBorder,
withRowBorder,
withSorting,
withPagination,
withColumnSelect,
rowProps = () => ({}) // <-- destructure row props getter
}) {
...
return (
<div className={className}>
...
<div className="....">
<table className="w-full" {...getTableProps()}>
<thead className="....">
...
</thead>
<tbody {...getTableBodyProps()}>
{(withPagination ? page : rows).map((row) => {
prepareRow(row);
return (
<tr
className={....}
{...row.getRowProps(rowProps(row))} // <-- call row props getter
>
...
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}
I am trying to create a ASC & DESC effect on my table headers in my React app.
Example here.
When I consoled the arguments I discovered the function it self was firing twice:
Once with the correct values and again with another value :(
Here is a bit of the code:
function useActiveToggle(initialState = activeObj, bool = false) {
const [state, setState] = React.useState(initialState);
const toggle = React.useCallback((id) => {
return setState((previousState) => ({ ...previousState, [id]: !previousState[id] }))
}, [])
return [state, toggle]
}
function TableHeaders({ headerData, handleDataSort }) {
function convertCamelToTitleCase(text) {
const result = text.replace(/([A-Z])/g, " $1");
return result.charAt(0).toUpperCase() + result.slice(1);
}
const options = [
{ label: 'First name', value: 'First name' },
{ label: 'Last name', value: 'Last name' },
];
const [value, setValue] = React.useState('First name');
const [active, toggle] = useActiveToggle()
function handleChange(event) {
console.log('event.target.value', event.target.value)
event.target.value === 'First name' ? handleDataSort("firstName", 1) : handleDataSort("lastName", 1)
setValue(event.target.value);
};
function handleClick(e, id, headerName) {
e.preventDefault();
if (active[id] === false && value === 'First name') {
handleDataSort(headerName, -1)
} else {
handleDataSort(headerName, 1)
}
if (active[id] === false && value === 'Last name') {
handleDataSort(headerName, -1)
} else {
handleDataSort(headerName, 1)
}
toggle(id)
}
return (
<>
<thead>
<th>
<Dropdown
label="Sort by:"
options={options}
value={value}
setValue={setValue}
handleChange={handleChange}
handleDataSort={handleDataSort}
/>
</th>
{headerData.map((headerName, index) => {
return (
<th key={index + headerName} className={`${headerName === 'firstName' || headerName === 'lastName' ? "table-header" : null}`} scope="col" onClick={(e) => handleClick(e, index, headerName)} style={!active[index] ? { backgroundColor: 'white' } : { backgroundColor: 'grey' }} >
{convertCamelToTitleCase(headerName)}
</th>
);
})}
</thead>
</>
);
}
function TableData({ rowData }) {
return (
<tbody>
{rowData.map(
({
company,
dateJoined,
firstName,
id,
isSiteAdmin,
jobTitle,
lastName,
state
}) => {
return (
<tr key={id}>
<td></td>
<td>{id}</td>
<td>{firstName}</td>
<td>{lastName}</td>
<td>{company}</td>
<td>{jobTitle}</td>
<td>{state}</td>
<td>{isSiteAdmin}</td>
<td>{dateJoined}</td>
</tr>
);
}
)}
</tbody>
);
}
const MemoTableData = React.memo(TableData);
function App() {
const [userData, setUserData] = React.useState("data")
const [headerData, setHeaderData] = React.useState(() =>
Object.keys(userData[0])
);
const [rowData, setRowData] = React.useState([]);
const [sortType, setSortType] = React.useState("firstName");
const sortArray = (p, o) => {
console.log("p, o ", p, o);
const sorted = [...userData].sort((a, b) => a[p].localeCompare(b[p]) * o)
setRowData(sorted);
};
React.useEffect(() => {
sortArray(sortType, 1);
}, [sortType]);
return (
<table>
<TableHeaders headerData={headerData} handleDataSort={sortArray} />
<MemoTableData rowData={rowData} />
</table>
);
}
export default App;
I have tried different things to eliminate Encountered two children with the same key, NaN. Keys should be unique and this is partial log:
in DynamicFields (at TableHeader.js)
in th (at TableHeader.js)
in TableHeader
Which is basically this bit of the TableHeader.js component, the full code is pasted lower down on this page:
return (
<th key={cleanTitle + - + index}
ref={(th) => th = th}
style={{width:width}}
data-col={cleanTitle}
>
<span className="header-cell" key={index*11}>{title} </span>
<DynamicFields key={header.index+title} parentIndex={(index + 3) + title} />
</th>
);
I have read through this discussion about keys and reactjs , followed it but still the error did not stop.
Here are the 3 component involved in rendering the datatable:
MyDatatable.js
import React from "react";
import TableHeader from "./TableHeader";
const MyDatatable = (props) => {
columnHeaders = [
{title: "Id" , accessor: 'id' , index: 0},
{title: "Name" , accessor: 'name', width: "300px", index: 2}
]
rowData = [
{id: 1, name: 'a', age: 29, qualification: 'B.com', rating: 3, profile: 'ps'},
{id: 2, name: 'b', age: 35, qualification: 'B.Sc', rating: 5, profile: 'h'}
]
const [headers, setHeaders] = React.useState(columnHeaders);
const [data, setData] = React.useState(rowData)
const renderContent = () => {
let contentView = data.map((row, rowIdx) => {
let id = row[keyField];
let tds = headers.map((header, index) => {
let content = row[header.accessor];
return (
<td key={index} data-id={id} data-row={rowIdx}>
{content}
</td>
);
});
return (
<tr key={rowIdx}>
{tds}
</tr>
);
//
}); //closes contentView variable
return contentView;
}
const renderTable = () => {
let title = props.title || "DataTable";
let contentView = renderContent();
return (
<table className="data-inner-table table-responsive">
<caption className="data-table-caption">
{title}
</caption>
<thead>
<tr>
<TableHeader headers={headers} />
</tr>
</thead>
<tbody>
{contentView}
</tbody>
</table>
)}
return (
<React.Fragment>
<div className={props.className}>
{renderTable() }
</div>
</React.Fragment>
)
}
TableHeader.js
import React from "react";
import DynamicFields from "../DynamicFields";
const TableHeader = (props) => {
let FieldTypes = ["text", "dropdown"];
const renderTableHeader = () => {
headers.sort((a, b) => {
if (a.index > b.index) return 1;
return -1
});
const headerView = headers.map((header, index) => {
let title = header.title;
let cleanTitle = header.accessor;
let width = header.width;
return (
<th key={cleanTitle + - + index}
ref={(th) => th = th}
style={{width:width}}
data-col={cleanTitle}
>
<span className="header-cell" key={index*11}>{title} </span>
<DynamicFields key={header.index+title} parentIndex={(index + 3) + title} />
</th>
);
} );
return headerView;
}
return(
<React.Fragment>
{renderTableHeader()}
</React.Fragment>
)
}
DynamicFields.js
import React, { useState, useEffect, useRef } from "react"
const DynamicFields = (props) => {
const optionsHash = ['Checkbox', 'Dropdown', 'boolean', 'Single line text'];
const [showDynamicField, setShowDynamicField ] = useState(false);
// const dropdownRef = useRef();
const handleShowDynamicField = (event) => {
setShowDynamicField(!showDynamicField);
};
return (
<React.Fragment>
<i className="bi bi-chevron-compact-down" onClick={handleShowDynamicField}></i>
{showDynamicField &&
optionsHash.map( (val, idx) => {
return(
<li key={val-idx} value={val} className="dropdown-item"> {val} </li>
)
})
}
</React.Fragment>
)
}
- is the subtraction operator, which will cause problems with key={val-idx} (string - number => NaN).
Presumably you want to use - as a separator character, so you'd use it as a string: key={val + '-' + idx} or key={`${val}-${idx}`}
In this case, since optionsHash has all unique strings, you could get away with just using key={val}.
The reason key={cleanTitle + - + index} works is because it evaluates to cleanTitle + (- +index), adding a negative number to a string which is allowed (but confusing).
My current implementation:
export const SORT_ORDER = {
ASC: "ascending",
DESC: "descending",
OTHER: "other"
};
export default class Table extends React.Component {
constructor(props) {
super(props);
this.sortIcon = new Map();
this.sortIcon.set(SORT_ORDER.ASC, sortAsc);
this.sortIcon.set(SORT_ORDER.DESC, sortDesc);
this.sortIcon.set(SORT_ORDER.OTHER, sortOther);
this.state = {
sortField: this.props.defaultSortColumn,
sortOrder: this.props.defaultSortOrder
};
}
componentDidMount() {
this.sort(this.props.defaultSortColumn, this.props.defaultSortOrder)();
}
retrieveOrder = (columnId) => {
return columnId === this.state.sortField ? this.state.sortOrder : SORT_ORDER.OTHER;
};
nextOrder = (current) => {
if (current === SORT_ORDER.DESC) {
return SORT_ORDER.ASC;
} else if (current === SORT_ORDER.ASC) {
return SORT_ORDER.DESC;
} else {
return this.props.defaultSortOrder;
}
};
sort = (columnId, order) => () => {
let descriptor = this.props.structure[columnId];
let values = this.props.value.slice();
let orderFactor = order === SORT_ORDER.ASC ? 1 : -1;
values.sort((a, b) => {
let first = descriptor.render(a);
let second = descriptor.render(b);
return first > second ? orderFactor : first < second ? -orderFactor : 0;
});
this.setState({
sortField: columnId,
sortOrder: order
});
this.props.onSort(values);
};
renderHeader = (id, descriptor) => {
let order = this.retrieveOrder(id);
let iconSrc = this.sortIcon.get(order);
let nextOrder = this.nextOrder(this.retrieveOrder(id));
return (
<th key={id} className={descriptor.headStyle}>
<a href="#" aria-sort={order} onClick={this.sort(id, nextOrder)}>
<img src={iconSrc}/>
{descriptor.label}
</a>
</th>
);
};
render() {
return (
<table>
Table structure
</table>
);
}
}
Parent component declares it in next way:
<Table structure={this.tableHeader} value={this.state.tableValue} onSort={this.handleChange('tableValue')}
defaultSortColumn="created" defaultSortOrder={SORT_ORDER.DESC} />
The table value is defined in props as value. onSort is a function that changes the state of the parent component => it changes the table value. Also I have defaultSortColumn and defaultSortOrder to sort the table after it is filled.
The problem is that my table can be declared multiple times at the page.
So,
1) I'm not able to store the table value in its state. Should I?
2) How can I implement default sorting without using componentDidMount? With using current implementation default sorting occurred only once, when componentDidMount is invoked, but I have more than 1 <Table/> component at the page.
I tried use componentWillReceiveProps function, but it is also invoked when I change the <Table/> state in sort function. So I can't use it.
My final solution is:
export const SORT_ORDER = {
ASC: "ascending",
DESC: "descending",
OTHER: "other"
};
class TableRow extends React.Component {
render() {
return (
<tr>
{this.props.children}
</tr>
);
}
}
class TableHeader extends React.Component {
constructor(props) {
super(props);
this.sortIcon = new Map([
[SORT_ORDER.ASC, {icon: sortAsc, title: "Ascending"}],
[SORT_ORDER.DESC, {icon: sortDesc, title: "Descending"}],
[SORT_ORDER.OTHER, {icon: sortOther, title: "Unsorted"}]
]);
}
render() {
const {children, onClick, sortOrder} = this.props;
return (
<th>
{onClick ? (
<a href="#" aria-sort={sortOrder} onClick={onClick}>
<img src={this.sortIcon.get(sortOrder).icon} title={this.sortIcon.get(sortOrder).title} />
{children}
</a>
) : children}
</th>
);
}
}
export default class Table extends React.Component {
constructor(props) {
super(props);
this.state = {
sortField: props.defaultSortColumn,
sortOrder: props.defaultSortOrder
};
}
retrieveOrder = (columnId) => {
return columnId === this.state.sortField ? this.state.sortOrder : SORT_ORDER.OTHER;
};
nextOrder = (current) => {
if (current === SORT_ORDER.DESC) {
return SORT_ORDER.ASC;
} else if (current === SORT_ORDER.ASC) {
return SORT_ORDER.DESC;
} else {
return this.props.defaultSortOrder;
}
};
sortedRows = () => {
let descriptor = this.props.structure.find(d => d.attribute === this.state.sortField);
let values = this.props.value.slice();
let orderFactor = this.state.sortOrder === SORT_ORDER.ASC ? 1 : -1;
return values.sort((a, b) => {
let first;
let second;
// null and undefined values should be changed to empty string
if (typeof a[descriptor.attribute] === "number" || typeof b[descriptor.attribute] === "number") {
first = a[descriptor.attribute] || "";
second = b[descriptor.attribute] || "";
} else {
first = descriptor.render(a) || "";
second = descriptor.render(b) || "";
}
return first > second ? orderFactor : first < second ? -orderFactor : 0;
});
};
renderHeaders = () => {
return this.props.structure.map((descriptor, id) => {
let header;
if (this.props.sortable) {
const order = this.retrieveOrder(descriptor.attribute);
const nextOrder = this.nextOrder(order);
header = (
<TableHeader key={id} onClick={() => {this.setState({sortField: descriptor.attribute, sortOrder: nextOrder})}}
sortOrder={order}>
{descriptor.label}
</TableHeader>
)
} else {
header = (
<TableHeader key={id}>
{descriptor.label}
</TableHeader>
)
}
return header;
});
};
renderRows = () => {
const Row = this.props.customRow || TableRow;
const values = this.props.sortable ? this.sortedRows() : this.props.value;
return values.map((value, idx) => (
<Row key={idx} value={value}>
{this.props.structure.map((descriptor, id) => (
<td key={id}>
descriptor.render(value, idx)
</td>
))}
</Row>
));
};
render() {
return (
<table className={this.props.className}>
<thead>
<tr>
{this.renderHeaders()}
</tr>
</thead>
<tbody>
{this.renderRows()}
</tbody>
</table>
);
}
}
Example of the table usage:
this.tableStructure = [
{
attribute: "number", label: "Row Number"
render: (row) => {return row.number}
},
{
attribute: "created", label: "Creation time"
render: (row) => {return this.dateToString(row.created)}
},
{
attribute: "type", label: "Row Type"
render: (row) => {return row.type}
},
{
attribute: "state", label: "State",
render: (row) => {return row.state}
},
{
attribute: "action", label: "Action"
render: (row) => {
return (
<button onClick={this.doSomething}>
</button>
);
}
}
];
<Table structure={this.tableStructure} value={this.state.someValue} sortable
defaultSortColumn="created" defaultSortOrder={SORT_ORDER.DESC} />
The implementation is based on http://styleguide.cfapps.io/react_base_tables.html