Related
long story short I was asked to create a table component using react-table,
in that table by default it uses component input, which when double-clicked it can immediately type.
and secondly, I want for one of the column editableCell to use dropdown. i have tried it but failed.
depedencies:
{
"#tanstack/react-table": "^8.5.22",
"next": "^12.3.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
component used : SubTable
<SubTable
data={dataTable}
handleAdd={addData}
columns={DataColumns}
tableStyle="h-[250px]"
hiddenColumns={["id"]}
showAddButton={true}
showDeleteButton={true}
skipPageReset={skipPageReset}
updateData={updateMyData}
/>
SubTable.tsx
import { FC, useState } from "react";
import {
useFlexLayout,
usePagination,
useTable,
useGlobalFilter,
useSortBy,
} from "react-table";
import styles from "./styles.module.css";
import EditableCell from "./EditableCell";
import Image from "next/image";
export interface SubTableProps {
setIsForm?: stateData;
data: any[];
columns: any[];
headerComponent?: JSX.Element;
page?: (page: number) => void;
showAddButton?: boolean;
showDeleteButton?: boolean;
onClickRow?: (cell: Cell<any, unknown>, row: Row<any>) => void;
hiddenColumns?: string[];
updateData?: (row: any, columnId: string, value: any) => void;
skipPageReset?: boolean;
tableStyle?: string;
handleAdd?: () => void;
handleConfig?: () => void;
deleteLabel?: string;
showBedButton?: boolean;
showConfigButton?: boolean;
}
const SubTable: FC<SubTableProps> = ({
data,
columns,
hiddenColumns,
skipPageReset,
updateData,
setIsForm,
tableStyle,
showAddButton,
showDeleteButton,
showBedButton = false,
showConfigButton = false,
deleteLabel = "Delete",
handleAdd,
handleConfig,
}) => {
const [editableRowIndex, setEditableRowIndex] = useState(null);
const [active, setActive] = useState("");
const [selectedData, setSelectedData] = useState({});
const { getTableProps, getTableBodyProps, headerGroups, page, prepareRow } =
useTable(
{
columns,
data,
// Set our editable cell renderer as the default Cell renderer
defaultColumn: {
Cell: EditableCell,
},
autoResetPage: !skipPageReset ?? false,
initialState: {
pageIndex: 0,
hiddenColumns: hiddenColumns ?? [],
},
updateData,
editableRowIndex,
setEditableRowIndex,
},
useGlobalFilter,
useSortBy,
usePagination,
useFlexLayout ?? null
);
const clickedRow = (e: React.MouseEvent, result: any) => {
const { index, values } = result;
setActive(index);
const currentIndex = index;
if (e.detail == 2) {
if (editableRowIndex !== currentIndex) {
setEditableRowIndex(index);
} else {
setEditableRowIndex(null);
setSelectedData(values);
// data changed here console.log('update row', values)
}
}
};
return (
<>
<div className={`bg-white p-2 text-xs ${tableStyle ?? ""}`}>
{/* */}
<div className={styles.toolbar}>
{showAddButton && (
<>
<button className={styles.btn} type="button" onClick={handleAdd}>
<Image
src="/images/button/add.png"
className="ml-1"
alt="add_icon"
width={16}
height={16}
/>
Add
</button>
<div className={styles.separate}>|</div>
</>
)}
{showConfigButton && (
<>
<button
className={styles.btn}
type="button"
onClick={handleConfig}
>
<Image
src="/images/button/update.svg"
className="ml-1"
alt="add_icon"
width={16}
height={16}
/>
Configuration
</button>
<div className={styles.separate}>|</div>
</>
)}
{showDeleteButton && (
<button
className={styles.btn}
type="button"
onClick={() => {
console.log("delete");
}}
>
<Image
src="/images/button/delete.svg"
className="ml-1"
alt="delete_icon"
width={16}
height={16}
/>
{deleteLabel}
</button>
)}
{showBedButton && (
<button
className={styles.btn}
type="button"
onClick={() => {
console.log("delete");
}}
>
<Image
src="/images/button/edit-undo.png"
className="ml-1"
alt="delete_icon"
width={16}
height={16}
/>
Empty Bed
</button>
)}
</div>
<div className="overflow-x-auto border-l-[#e7e9ec] border-r-[#e7e9ec] print:block max-h-[200px] overflow-y-auto">
<table
className="table-fixed w-full border-x bg-white relative border-collapse"
{...getTableProps()}
>
<thead className="sticky top-0">
{headerGroups.map((headerGroup, idx) => (
<tr {...headerGroup.getHeaderGroupProps()} key={idx}>
{headerGroup.headers.map((column, idx) => (
<th
className="border border-solid font-normal text-lg text-left p-1 bg-green-100"
{...column.getHeaderProps(column.getSortByToggleProps())}
key={idx}
>
{column.render("Header")}
<span>
{column.isSorted
? column.isSortedDesc
? " 🔻"
: " 🔺"
: " ↕"}
</span>
</th>
))}
</tr>
))}
</thead>
<tbody className="overflow-y-auto" {...getTableBodyProps()}>
{page.map((row, idx) => {
prepareRow(row);
return (
<tr
{...row.getRowProps()}
key={idx}
className={`${
active == row.id ? "bg-bgGrey-1" : ""
} cursor-default`}
onClick={(e) => clickedRow(e, row)}
>
{row.cells.map((cell, idx) => {
return (
<td
className="whitespace-nowrap text-ellipsis overflow-hidden border-b border-r border-y p-1"
{...cell.getCellProps()}
key={idx}
>
{cell.render("Cell")}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</>
);
};
export default SubTable;
useSubTable.tsx
import { useState } from "react";
const useSubTable = () => {
const [dataTable, setDataTable] = useState<any>([]);
const [skipPageReset, setSkipPageReset] = useState(false);
const updateMyData = (rowIndex: number, columnId: any, value: any) => {
// We also turn on the flag to not reset the page
setSkipPageReset(true);
setDataTable((old: any[]) =>
old.map((row: any, index: number) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value,
};
}
return row;
})
);
};
return { dataTable, setDataTable, updateMyData, skipPageReset };
};
export default useSubTable;
EditableCell.tsx
import React, { useState, useEffect } from "react";
// Create an editable cell renderer
const EditableCell = (props: any) => {
const {
value: initialValue,
row: { index },
column: { id },
updateData, // This is a custom function that we supplied to our table instance
editableRowIndex, // index of the row we requested for editing
} = props;
// We need to keep and update the state of the cell normally
const [value, setValue] = useState(initialValue);
const onChange = (e: any) => {
setValue(e.target.value);
};
// We'll only update the external data when the input is blurred
const onBlur = () => {
updateData(index, id, value);
};
// If the initialValue is changed externall, sync it up with our state
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return index === editableRowIndex ? (
<input
style={{ width: "100%" }}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
) : (
<p>{value}</p>
);
};
export default EditableCell;
define a column: column.ts
import TypeDropdown from "./TypeDropdown";
export const dataColumns = [
{
Header: "Name",
accessor: "name",
sticky: "left",
},
{
Header: "Order",
accessor: "order",
sticky: "left",
},
{
Header: "Type",
accessor: "type",
sticky: "left",
Cell: TypeDropdown,
},
];
my custom dropdown component: TypeDropdown.tsx
import React, { useState, useEffect } from "react";
import { Select } from "#/client/components/Inputs";
const TypeDropdown = (props: any) => {
const {
value: initialValue,
row: { index },
column: { id },
updateData, // This is a custom function that we supplied to our table instance
editableRowIndex, // index of the row we requested for editing
} = props;
// We need to keep and update the state of the cell normally
const [value, setValue] = useState(initialValue);
const onChange = (e: any) => {
setValue(e.target.value);
updateData(index, id, e.target.value);
};
// If the initialValue is changed externall, sync it up with our state
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return id === "type" ? (
<Select
value={value}
id="typex"
option={[
{ id: 1, name: "TextField", code: "TextField" },
{ id: 2, name: "Combo", code: "Combo" },
]}
showRed={true}
onChange={(e) => onChange(e)}
required={true}
/>
) : (
<p>{value}</p>
);
};
export default TypeDropdown;
lastly my useSubTableProps.tsx
import React, { useMemo, useState } from "react";
import { dataColumns } from "./column";
interface customObject {
[key: string]: any;
}
const useSubTableProps = () => {
const DataColumns = useMemo(() => dataColumns, []);
const [dataTable, setDataTable] = useState<any>([]);
const [skipPageReset, setSkipPageReset] = useState<boolean>(false);
const updateMyData = (rowIndex: number, columnId: any, value: any) => {
// We also turn on the flag to not reset the page
setSkipPageReset(true);
setDataTable((old: customObject[]) =>
old.map((row: any, index: number) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value,
};
}
return row;
})
);
};
const addData = () => {
setDataTable((prev: any) => [
...prev,
{
name: `test`,
order: 0,
type: "TextField",
},
]);
};
return { DataColumns, dataTable, addData, updateMyData, skipPageReset };
};
export default useSubTableProps;
expected visual, what I want in column type is dropdown component
finally, I found the problem and the solution,
after I re-read the documentation about the column options section, it says to use a valid JSX element, after that, I try to use a simple JSX element and it works. Maybe my previous JSX element was not considered valid by react-table
link: React-table column options
I am working with react table where I am passing the data as prop to react table component which is a mobx state. Also I am using the pagination in for react table data. The issue is when I am updating the state value from the cell edit of react table the state is get updated and when if move to the next pagination and do some changes there and comeback to the first page and it shows only the old state value only. I have included my coding below. Can anyone please give me a solution?
Parent component where I am calling the React Table as child component.
`/** #format */
/* eslint-disable prefer-const */
/* eslint-disable #typescript-eslint/naming-convention */
import { toJS } from "mobx";
import { observer } from "mobx-react-lite";
import React, { useEffect, useState } from "react";
import { Button, Card, CardBody, Row } from "reactstrap";
import styled from "styled-components";
// eslint-disable-next-line import/no-extraneous-dependencies
import { useLocation } from "react-router";
import Rocket from "../../assets/img/raw/rocket-pngrepo-com.png";
import ReactTable from "../ReactTable";
import CategorySelector from "./CategorySelector";
import useStore from "../../mobx/UseStore";
import UserService from "../../services/UserService";
import NotificationManager from "../common/react-notifications/NotificationManager";
import IntlMessages from "../../helpers/IntlMessages";
import { REFACTORDATA } from "../../constants/refactorData";
import "./animation.scss";
import { START } from "../../constants";
import LogService from "../../services/LogService";
const CardLayout = styled(Card)`
/* border: 1px solid red; */
/* width: 50%; */
`;
const SaveButton = styled(Button)`
/* position: absolute; */
justify-content: end;
padding: 0.5rem 2rem 0.5rem 2rem;
span {
font-size: 2rem;
}
`;
// const TextArea = styled.textarea`
// /* background-color: #151a30; */
// color: #ffffff;
// `;
const OutPutViewSection = () => {
const { UIState, UserState } = useStore();
// const processedData = REFACTORDATA(UIState.processedData);
// finding current screen
function getCurrentScreenName() {
// eslint-disable-next-line react-hooks/rules-of-hooks
const location = useLocation();
//
const { pathname } = location;
const urlParts = pathname.split("/");
const lastPart = urlParts[urlParts.length - 1];
return lastPart;
}
const currentScreen = getCurrentScreenName();
// bring metadata id for updating database
const { metaDataId, metaData } = UIState;
// statelocal for controlling the category and set data origin
const [dataOrigin, setDataOrigin] = useState(metaData ? metaData.dataOrigin : "");
const [isSelectModalOpen, setIsSelectModalOpen] = useState(false);
const toggleSelectModal = () => {
setIsSelectModalOpen(!isSelectModalOpen);
};
// handling change in dataorigin
const onChangeDataOrigin = ({ target }) => {
const { value } = target;
setDataOrigin(value);
};
// table columns
const onSaveData = async () => {
if (!dataOrigin) {
NotificationManager.info("plase-add-data-origin", null, null, null, null, null);
return;
}
let response;
let uType;
let reqData;
if (currentScreen === "manageCVStatus" || currentScreen === "cvStatus") {
// eslint-disable-next-line #typescript-eslint/no-shadow
const { orgId, adminId, templateId, docPath, processType, totalPages } = metaData;
// while updating from metadata userId is populated
let { userId, _id } = metaData;
// eslint-disable-next-line no-underscore-dangle
userId = userId._id;
reqData = {
orgId,
adminId,
userId,
templateId,
metaDataId: UIState.metaDataId,
docPath,
processedData: UIState.processedData,
processType,
totalPages,
dataOrigin,
};
response = await UserService.saveProcessedData(reqData);
// updating only data origin and processed data which may change
UIState.cvMetaDataOperations("update", _id, {
dataOrigin,
processedDataId: { _id: UIState.currentProcessedDataId, processedData: UIState.processedData },
});
UIState.setProcessedData(response.processedData);
// update state with latest data
UIState.updateCvData(response.metaDataId, response.updatedData, response.processedData);
}
if (currentScreen === "cvProcessing" || currentScreen === "cvProcess") {
const { userId, orgId, adminId, userType } = UserState.userData;
uType = userType;
reqData = {
orgId,
adminId,
userId,
templateId: UIState.selectedTemplateId,
metaDataId: UIState.metaDataId,
docPath: UIState.currentFolderPath ? UIState.currentFolderPath : UIState.currentFilePath,
processedData: UIState.processedData,
processType: UIState.processType ? UIState.processType : "-",
totalPages: UIState.totalPages,
dataOrigin,
};
response = await UserService.saveProcessedData(reqData);
}
if (response.success) {
UIState.updateCurrentMetaDataId(response.metaDataId);
NotificationManager.success("Data Saved", "Saved", 1500);
// setting is data saved flag true to prevent deletion of image from server
UIState.updateState("isDataSaved", true);
// after saving data fetch new data
// fetching new data only when processed new cv
if (uType) UIState.fetchMetaData(uType);
}
if (response.error) {
let errorMessage: string = response.error;
if (errorMessage.includes("metaData validation failed")) {
errorMessage = "nofication-name-email-required";
} else if (errorMessage.includes("duplicate key error collection")) {
errorMessage = "notifcation-processed-by-other-user";
} else if (errorMessage.includes("Email is already exists")) {
errorMessage = "notification-email-duplication";
} else {
errorMessage = "notification-error-occured";
}
NotificationManager.error(errorMessage, null, 5000);
}
// set graph status
UserState.setUserGraphApiCallStatus = "false";
};
const validate = (e) => {
e.preventDefault();
if (!dataOrigin) {
NotificationManager.error("data-origin-is-required", null, 5000);
} else {
onSaveData();
}
};
// * cleanup
// eslint-disable-next-line
useEffect(() => {
return () => {
setDataOrigin("");
};
}, []);
const action = "extractProcess";
const templateId = undefined;
// multipage
const onMultiImageProcess = async () => {
// const action = "tempProcess";
const markedData: any = { ...UIState.annotatePages };
const reqData = {
socketId: UIState.socketId,
userId: UserState.userData.userId,
orgId: UserState.userData.orgId,
multiPageInvoiceClasses: markedData,
processType: UIState.processType,
action,
totalPages: UIState.totalPages,
folderPath: UIState.currentFolderPath,
...UIState.templateConfigurations,
templateId,
};
await UserService.processImage(reqData);
};
// PROCESSING SINGLE PAGE
const onSingleImageProcess = async () => {
const markedData = UIState.annotatePages[UIState.currentSelectedPage];
// const action = "tempProcess";
const reqData = {
socketId: UIState.socketId,
userId: UserState.userData.userId,
orgId: UserState.userData.orgId,
imagePath: UIState.currentFilePath,
singlePageInvoiceClasses: [...markedData],
processType: UIState.processType,
action,
totalPages: UIState.totalPages,
...UIState.templateConfigurations,
templateId,
};
await UserService.processImage(reqData);
};
const onProcess = async () => {
try {
// set processing only for cv processing not on create edit
// if (isCreateEditScreen() === false)
UIState.setLoadingState("isProcessing", true);
if (UIState.totalPages === 1) {
await onSingleImageProcess();
}
if (UIState.totalPages > 1) {
await onMultiImageProcess();
}
UIState.increamentCVProcessingProgress(START);
LogService.info("CLASS COORDINATES 🔭 ", toJS(UIState.annotatePages));
// if (isCreateEditScreen()) {
// UIState.setLoadingState("isProcessing", false);
// history.push("/user/manageTemplate");
// }
} catch (error) {
LogService.error(error);
}
};
return (
<>
{/* Rocket animation code here */}
{!UIState.processedData && currentScreen === "cvProcess" && (
<Row>
{/* until auto processing is on going don't show extract data button */}
<div className="d-flex flex-column justify-content-center align-items-center m-auto mb-3">
{/* Extract Data */}
{UIState.isAutoProcessDone && !UIState.processedData && (
<Button color="success" className="font-weight-bold" onClick={onProcess}>
<IntlMessages id="extract-data" />
</Button>
)}
</div>
<div className="OutputViewSection__filler_div" />
<div className="d-flex justify-content-center align-items-center p-5">
<div
className={`w-50 ${UIState.isProcessing ? "vibrate-1" : ""} ${
UIState.isProcessedDataEmmited ? "slide-out-top" : ""
}`}
>
<img src={Rocket} alt="rocket" width="100%" style={{ transform: "rotate(-45deg)" }} />
</div>
<div>
<p className="h4 text-dark">{UIState.cvProcessingProgress}%</p>
</div>
</div>
</Row>
)}
{/* save button */}
<CardLayout className="mb-4" style={{ display: UIState.processedData ? "block" : "none" }}>
<CardBody>
<div className="d-flex flex-column justify-content-center align-items-center mt-3">
{!UIState.isDataSaved && currentScreen === "cvProcess" && (
<p className="h5">
<IntlMessages id="extraction-complated-save-data" />
</p>
)}
<SaveButton color="success" pill className="m-1" size="xl" outline onClick={onSaveData}>
<IntlMessages id="Save" />
</SaveButton>
</div>
<Row className="d-flex flex-column pl-4 pr-4 mb-3">
<p className="h6 font-weight-bold">
<IntlMessages id="cvstatus.data-origin" />
</p>
<textarea value={dataOrigin} onChange={onChangeDataOrigin} />
</Row>
{/* categorey and data origin fields show only when metadataid is set */}
{metaDataId && (
<div className="d-flex justify-content-end mb-3">
{/* <textarea className="textinput" value={category} onChange={onChangeCategory} onBlur={onCategoryFocusLoss} /> */}
{isSelectModalOpen && <CategorySelector isOpen={isSelectModalOpen} toggle={toggleSelectModal} />}
{/* <Select options={options} /> */}
<Button onClick={toggleSelectModal}>
<IntlMessages id="select-category" />
</Button>
</div>
)}
{UIState.processedData && <ReactTable data={UIState.processedData} />}
</CardBody>
</CardLayout>
</>
);
};
export default observer(OutPutViewSection);`
`
ReactTable.tsx
`/** #format */
/* eslint-disable react/jsx-props-no-spreading */
import { observer } from "mobx-react-lite";
import React from "react";
import { usePagination, useTable } from "react-table";
import { Pagination, PaginationItem, PaginationLink, Table } from "reactstrap";
// import Pagination from "../../containers/pages/Pagination";
import useStore from "../../mobx/UseStore";
import EditableCell from "./components/EditableCell";
import { ReactTableStyle } from "./Style";
// Set our editable cell renderer as the default Cell renderer
const defaultColumn = {
Cell: EditableCell,
};
// export default function Index({ columns, data, updateMyData, skipPageReset }) {
function Index({ data }) {
const { UIState } = useStore();
// table columns
const columns = React.useMemo(
() => [
{
Header: "Label",
accessor: "label",
},
{
Header: "Data",
accessor: "data",
},
// { Header: "Action", accessor: "action" },
],
[]
);
const [skipPageReset, setSkipPageReset] = React.useState(false);
const updateMyData = (rowIndex, columnId, value, isChecked) => {
// We also turn on the flag to not reset the page
setSkipPageReset(true);
UIState.updateProcessedData({ rowIndex, columnId, value, isChecked });
};
// Use the state and functions returned from useTable to build your UI
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
// Instead of using 'rows', we'll use page,
// which has only the rows for the active page
// The rest of these things are super handy, too ;)
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data,
defaultColumn,
autoResetPage: !skipPageReset,
initialState: { pageIndex: 0, pageSize: 5 },
pageCount: 2,
updateMyData,
},
usePagination
);
// useEffect(() => {
// setPageSize(2);
// }, []);
return (
<ReactTableStyle>
<div className="tableWrap">
<Table {...getTableProps()}>
{/* <thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column: any) => (
<th
{...column.getHeaderProps({
className: column.collapse ? "collapse" : "",
})}
>
{column.render("Header")}
</th>
))}
</tr>
))}
</thead> */}
<tbody {...getTableBodyProps()}>
{console.log()}
{page.map((row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell: any) => (
// <td
// {...cell.getCellProps({
// // className: cell.column.collapse ? "collapse" : "",
// className: "d-flex",
// })}
// >
<div>{cell.render("Cell")}</div>
// </td>
))}
{row.cells.map((cell) => {
console.log(cell);
return "";
})}
</tr>
);
})}
</tbody>
</Table>
</div>
<div className="Footer">
{/* <Pagination
currentPage={pageIndex - 1}
firstIsActive={!canPreviousPage}
lastIsActive={!canNextPage}
numberLimit={3}
totalPage={pageCount}
onChangePage={gotoPage}
/> */}
<Pagination aria-label="Page navigation " listClassName="justify-content-center" size="sm">
<PaginationItem>
<PaginationLink className="first" onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
<i className="simple-icon-control-start" />
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink className="prev" onClick={() => previousPage()} disabled={!canPreviousPage}>
<i className="simple-icon-arrow-left" />
</PaginationLink>
</PaginationItem>
{/* {[...new Array(pageOptions.length).keys()].map((pageNumber) => (
<PaginationItem active={pageIndex === pageNumber}>
<PaginationLink onClick={() => gotoPage(pageNumber)}>{pageNumber + 1}</PaginationLink>
</PaginationItem>
))} */}
<PaginationItem active>
<PaginationLink onClick={() => gotoPage(pageIndex)}>{pageIndex + 1}</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink className="next" onClick={() => nextPage()} disabled={!canNextPage}>
<i className="simple-icon-arrow-right" />
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink className="last" onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
<i className="simple-icon-control-end" />
</PaginationLink>
</PaginationItem>
</Pagination>
</div>
</ReactTableStyle>
);
}
export default observer(Index);
`
The child component that I am using as cell.
`/`** #format */
import { InputGroup, InputGroupAddon, InputGroupText, Input } from "reactstrap";
import React from "react";
import { observer } from "mobx-react-lite";
import styled from "styled-components";
import useStore from "../../../mobx/UseStore";
import IntlMessages from "../../../helpers/IntlMessages";
// Create an editable cell renderer
const EditableCell = ({
value: initialValue,
row: { index, original },
column: { id },
updateMyData, // This is a custom function that we supplied to our table instance
}) => {
const { UIState } = useStore();
// We need to keep and update the state of the cell normally
const [value, setValue] = React.useState(initialValue);
// const [check, setCheck] = React.useState(false);
const onChange = (e) => {
setValue(e.target.value);
};
// We'll only update the external data when the input is blurred
const onBlur = () => {
updateMyData(index, id, value, true);
// setCheck(true);
};
// If the initialValue is changed external, sync it up with our state
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return (
<>
{/* adding editable text only for data not for label */}
{id === "label" && UIState.labelKeyMap[value] && (
<div className="font-weight-bold m-3">
{/* <IntlMessages id={`${value}`} /> */}
<IntlMessages id={`${UIState.labelKeyMap[value]}`} />
</div>
)}
{id === "data" && (
<InputGroup className="mb-3">
<InputGroupAddon addonType="prepend">
<InputGroupText>
<Input
addon
color="secondary"
type="checkbox"
aria-label="Checkbox for following text input"
checked={original.isChecked}
/>
</InputGroupText>
</InputGroupAddon>
<Input type="textarea" className="text-dark" width="100" value={value} onChange={onChange} onBlur={onBlur} />
</InputGroup>
)}
</>
);
};
export default observer(EditableCell);
`
The function that update the mobx state in another file.
`updateProcessedData({ rowIndex, columnId, value, isChecked }) {
console.log(`processed data -> ${toJS(JSON.stringify(this.processedData))}`);
this.processedData.forEach((row: any, index) => {
if (index === rowIndex) {
// updating the index of process data obj as with map not working
this.processedData[index] = {
...this.processedData[rowIndex],
[columnId]: value,
isChecked,
};
}
console.log(`processed after -> ${toJS(JSON.stringify(this.processedData))}`);
return row;
});
}`
I want to persist the data that I made changes in the page even i am navigating to other pages
I am attempting to copy the global filter implementation from this example: https://react-table.tanstack.com/docs/examples/filtering I have copied all the code and the filtering is working correctly. However for some reason, whenever I type a character in the input box, it loses the focus and I need to click back in the box again to keep typing.
Here is the entire table file:
import React, { useState } from "react";
import {
useTable,
useSortBy,
useGlobalFilter,
useAsyncDebounce
} from "react-table";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import {
faSortUp,
faSortDown,
faCheck,
faEllipsisV
} from "#fortawesome/free-solid-svg-icons";
import { Scrollbars } from "rc-scrollbars";
const IngredientsTable = (props) => {
const { data, selectedIngredient } = props;
const [showIngredientCheckID, setShowIngredientCheckID] = useState(-1);
const columns = React.useMemo(
() => [
{
Header: "Name",
accessor: "col1" // accessor is the "key" in the data
},
{
Header: "",
accessor: "col2"
},
{
Header: "Item Number",
accessor: "col3" // accessor is the "key" in the data
},
{
Header: "EPA Number",
accessor: "col4"
},
{
Header: "Category",
accessor: "col5"
},
{
Header: "Modified",
accessor: "col6"
}
],
[]
);
// Define a default UI for filtering
const GlobalFilter = ({
preGlobalFilteredRows,
globalFilter,
setGlobalFilter
}) => {
const count = preGlobalFilteredRows.length;
const [value, setValue] = useState(globalFilter);
const onChange = useAsyncDebounce((value) => {
setGlobalFilter(value || undefined);
}, 200);
return (
<span>
Filter Ingredients:{" "}
<input
value={value || ""}
onChange={(e) => {
setValue(e.target.value);
onChange(e.target.value);
}}
placeholder={`${count} records...`}
style={{
fontSize: "1.1rem",
border: "0"
}}
/>
</span>
);
};
// Define a default UI for filtering
function DefaultColumnFilter({
column: { filterValue, preFilteredRows, setFilter }
}) {
const count = preFilteredRows.length;
return (
<input
value={filterValue || ""}
onChange={(e) => {
setFilter(e.target.value || undefined); // Set undefined to remove the filter entirely
}}
placeholder={`Search ${count} records...`}
/>
);
}
const defaultColumn = React.useMemo(
() => ({
// Let's set up our default Filter UI
Filter: DefaultColumnFilter
}),
[]
);
const filterTypes = React.useMemo(
() => ({
// Or, override the default text filter to use
// "startWith"
text: (rows, id, filterValue) => {
return rows.filter((row) => {
const rowValue = row.values[id];
return rowValue !== undefined
? String(rowValue)
.toLowerCase()
.startsWith(String(filterValue).toLowerCase())
: true;
});
}
}),
[]
);
const tableInstance = useTable(
{ columns, data, defaultColumn, filterTypes },
useGlobalFilter, // useGlobalFilter!
useSortBy
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
state,
preGlobalFilteredRows,
setGlobalFilter
} = tableInstance;
// const showUserCheck = (id) => {};
const thumbVertical = ({ style, ...props }) => {
const finalStyle = {
...style,
visibility: "hidden"
};
return <div style={finalStyle} {...props} />;
};
return (
<div className={"table-container"}>
<Scrollbars
autoHeight
autoHeightMin={0}
autoHeightMax={"calc(100vh - 40px)"}
renderThumbVertical={thumbVertical}
>
<>
<div className={"row mx-auto my-2"}>
<div className={"col-8"}>
<GlobalFilter
preGlobalFilteredRows={preGlobalFilteredRows}
globalFilter={state.globalFilter}
setGlobalFilter={setGlobalFilter}
/>
</div>
</div>
<table
{...getTableProps()}
className={
"table table-striped table-hover table-borderless ingredients-table"
}
>
<thead>
{headerGroups.map((headerGroup, i) => (
<tr {...headerGroup.getHeaderGroupProps()} key={i}>
{headerGroup.headers.map((column, thInd) => (
// Add the sorting props to control sorting. For this example
// we can add them into the header props
<th
{...column.getHeaderProps(column.getSortByToggleProps())}
key={thInd}
>
{column.render("Header")}
{/* Add a sort direction indicator */}
<span>
{column.isSorted ? (
column.isSortedDesc ? (
<FontAwesomeIcon icon={faSortDown} size={"lg"} />
) : (
<FontAwesomeIcon icon={faSortUp} size={"lg"} />
)
) : (
""
)}
</span>
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map((row, ind) => {
// console.log({ row });
prepareRow(row);
return (
<tr
{...row.getRowProps()}
onClick={(e) =>
setTheSelectedIngredient(e, row.values.col1)
}
onMouseEnter={() => setShowIngredientCheckID(row.id)}
onMouseLeave={() => setShowIngredientCheckID(-1)}
key={ind}
className={`${
selectedIngredient.name === row.values.col1
? "selected"
: ""
}`}
data-testid={"ingredient-row"}
>
{row.cells.map((cell, tdInd) => {
return (
<td {...cell.getCellProps()} key={tdInd}>
{tdInd === 0 ? (
selectedIngredient.name === row.values.col1 ? (
<>
<FontAwesomeIcon
icon={faCheck}
className={"white"}
/>{" "}
</>
) : showIngredientCheckID === row.id ? (
<>
<FontAwesomeIcon
icon={faCheck}
className={"gray"}
/>{" "}
</>
) : (
<>
<FontAwesomeIcon
icon={faCheck}
className={"clear"}
/>{" "}
</>
)
) : (
tdInd === 1 && (
<FontAwesomeIcon
icon={faEllipsisV}
className={"three-dots"}
/>
)
)}
{cell.render("Cell")}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</>
</Scrollbars>
</div>
);
};
export default IngredientsTable;
And here is a code sandbox where you can see the issue happening. https://codesandbox.io/s/relaxed-benz-co8ub?file=/src/components/pieces/IngredientsTable.js:0-7960
At the top, click where it says "100 records..." and try typing something. The focus leaves after each character. I can't figure out what would be causing that.
Just move GlobalFilter declaration outside IngredientsTable as re-rendering of parent is creating new instance of it every time, which is causing to loose focus.
Fixed CSB - https://codesandbox.io/s/bold-browser-mwuxd
I'm trying to combine react-table with react-query to get a dynamic, editable table that is initially populated by data from the database.
import React from 'react';
import Style from './CLGridStyle.js';
import axios from 'axios';
import { useTable, useBlockLayout, useResizeColumns } from 'react-table';
import { useQuery, QueryClient, QueryClientProvider, useQueryClient } from 'react-query'
const EditableCell = ({
value: initialValue,
row: { index },
column: { id, type, readonly },
updateMyData,
}) => {
// We need to keep and update the state of the cell normally
const [value, setValue] = React.useState(initialValue)
const onChange = e => {
setValue(e.target.value)
}
const onCheckboxChange = e => {
setValue(e.target.checked);
}
// We'll only update the external data when the input is blurred
const onBlur = () => {
updateMyData(type, index, id, value);
}
// If the initialValue is changed external, sync it up with our state
React.useEffect(() => {
setValue(initialValue)
}, [initialValue])
switch (type) {
case 'checkbox':
return (<input onChange={onCheckboxChange} onBlur={onBlur} type={type} readOnly={readonly}
autoComplete="off" checked={value || false} />);
case 'date':
return (<input onChange={onChange} onBlur={onBlur} type={type} readOnly={readonly}
autoComplete="off" value={(value || "").slice(0, 10)} />);
default:
return (<input onChange={onChange} onBlur={onBlur} type={type} readOnly={readonly}
autoComplete="off" value={value || ""} />);
}
}
function CLGrid({ columns }) {
const queryClient = useQueryClient();
const [data, setData] = React.useState([]);
const { apiResponse, isLoading } = useQuery('users', () => axios.get(`http://www.test.devel/users`));
React.useEffect(() => {
setData(apiResponse?.data);
}, [apiResponse]);
const updateMyData = (type, rowIndex, columnId, value) => {
setData(old =>
old.map((row, index) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value,
}
}
return row
})
)
}
const defaultColumn = React.useMemo(
() => ({
minWidth: 30,
width: 150,
maxWidth: 400,
Cell: EditableCell,
}),
[]
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
state,
} = useTable(
{
columns, data, defaultColumn, updateMyData
},
useBlockLayout,
useResizeColumns
);
React.useEffect(() => {
if (state.columnResizing.isResizingColumn === null) {
console.log('columnResizing', state.columnResizing);
}
}, [state.columnResizing]);
if (isLoading || !data) {
return (<div>Loading...</div>);
}
return (
<Style>
<div {...getTableProps()} className="table">
<div>
{headerGroups.map(headerGroup => (
<div {...headerGroup.getHeaderGroupProps()} className="tr">
{headerGroup.headers.map(column => (
<div {...column.getHeaderProps()} className="th">
{column.render('Header')}
<div {...column.getResizerProps()} className="resizer" />
</div>
))}
</div>
))}
</div>
<div {...getTableBodyProps()}>
{rows.map((row, i) => {
prepareRow(row)
return (
<div {...row.getRowProps()} className="tr">
{row.cells.map(cell => {
return (
<div {...cell.getCellProps()} className="td">
{cell.render('Cell')}
</div>
)
})}
</div>
)
})}
</div>
</div>
<pre>
<code>{JSON.stringify(state, null, 2)}</code>
</pre>
<pre>
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
</Style>
)
}
const client = new QueryClient();
const ReactQueryWithTable = ({columns}) => {
return (
<QueryClientProvider client={client}>
<CLGrid columns={columns} />
</QueryClientProvider>
);
}
export default ReactQueryWithTable;
When I try to run this, I get this error:
TypeError: Cannot read properties of undefined (reading 'forEach')
Which happens in the useTable hook at this location:
> 591 | data.forEach((originalRow, rowIndex) =>
592 | accessRow(originalRow, rowIndex, 0, undefined, rows)
593 | )
So far I've spent a few hours tweaking this to no avail. What am I missing?
useQuery doesn't have a property called apiResponse, so this is likely not working:
const { apiResponse, isLoading } = useQuery(...)
useQuery returns a field called data, where the response of your api request will be available.
Hello everyone :D I need your advise/tip. Right now I have a APIDataTable component. It has its rows, columns and etc. This component is responsible to show/build data table on frontend with search bar in it above the table. I have an search bar, which is not functional right now. I want it to search data from data table. What should I start from? How can i make it perform search in Table. Thank you for any advise and tip <3
import React, { useEffect, useState } from "react";
import { plainToClassFromExist } from "class-transformer";
import { Pagination } from "../../models/Pagination";
import {
DataTable,
DataTableHead,
DataTableHeadCell,
DataTableBody,
DataTableRow,
DataTableCell,
} from "../DataTable";
import { request } from "../../api";
import "./index.css";
import { MenuSurface } from "../MenuSurface";
import { IconButton } from "../IconButton";
import { Checkbox } from "../Checkbox";
import { Dialog } from "../Dialog";
import { GridCell, GridRow } from "../Grid";
import { Button } from "../Button";
export class Column<T> {
label: string;
width?: number;
filter?: JSX.Element;
render: (row: T) => JSX.Element | string;
constructor(column: Partial<Column<T>>) {
Object.assign(this, column);
}
}
type APIDataTableProps<T> = {
apiPath?: string;
params?: string;
columns?: Column<T>[];
type: Function;
onRowClick?: (row: T) => void;
};
export const APIDataTable = <T extends object>({
apiPath,
params,
columns,
type,
onRowClick,
}: APIDataTableProps<T>) => {
const [data, setData] = useState<Pagination<T>>(null);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(15);
const [isLoading, setIsLoading] = useState(false);
const [isDialogOpen, setDialogOpen] = useState(false);
const [isMenuSurFaceOpen, setMenuSurfaceOpen] = useState(false);
const [hiddenColumns, setHiddenColumns] = useState<number[]>(
JSON.parse(localStorage.getItem(`hiddenColumns-${apiPath + params}`)) || []
);
const fetchData = async () => {
const urlSearchParams = new URLSearchParams(params);
urlSearchParams.set("page", page.toString());
urlSearchParams.set("page_size", pageSize.toString());
const url = `${apiPath}?${urlSearchParams}`;
const response = await request(url);
const data = plainToClassFromExist(new Pagination<T>(type), response, {
excludeExtraneousValues: true,
});
setData(data);
setIsLoading(false);
};
useEffect(() => {
if (!!apiPath) {
setIsLoading(true);
fetchData();
}
}, [page, pageSize]);
const headCells = columns
.filter((e, i) => !hiddenColumns?.includes(i))
.map((column) => (
<DataTableHeadCell key={column.label} width={column.width}>
{column.label}
</DataTableHeadCell>
));
const rows = data?.results?.map((row, index) => (
<DataTableRow
key={"row-" + index}
onClick={() => !!onRowClick && onRowClick(row)}
>
{columns
.filter((e, i) => !hiddenColumns?.includes(i))
.map((column) => {
return (
<DataTableCell key={column.label} width={column.width}>
<div className="data-table-cell-text">{column.render(row)}</div>
</DataTableCell>
);
})}
</DataTableRow>
));
let uncheckedCheckboxes = hiddenColumns;
const onCheckboxChange = (index: number, value: boolean) => {
if (!value) {
uncheckedCheckboxes.push(index);
//setHiddenColumns(uncheckedCheckboxes);
} else {
const array = [...uncheckedCheckboxes];
array.splice(array.indexOf(index), 1);
uncheckedCheckboxes = array;
}
};
const [isOpen, setIsOpen] = useState(false);
return (
<div className="data-table-container">
<div className="search-test">
<div className="mdc-menu-surface--anchor">
<label
className="mdc-text-field mdc-text-field--filled mdc-text-field--no-label mdc-text-field--with-leading-icon mdc-text-field--with-trailing-icon"
htmlFor="input"
id="search-menu-surface"
>
<IconButton density={-1} icon="search" />
<input
className="mdc-text-field__input "
type="text"
placeholder="Поиск"
id="searchinput"
/>
<IconButton
density={-1}
icon="arrow_drop_down"
onClick={() => {
setMenuSurfaceOpen(true);
}}
/>
</label>
<MenuSurface
isOpen={isMenuSurFaceOpen}
onClose={() => setMenuSurfaceOpen(false)}
fullwidth
>
<div className="data-table-filters-container">
{columns.map(
(column) =>
!!column.filter && (
<div className="data-table-filter">
<div className="data-table-filter-label mdc-typography--subtitle1">
{column.label}
</div>
<div className="data-table-column-filter">
{column.filter}
</div>
</div>
// <GridRow>
// <GridCell span={3}>{column.label}</GridCell>
// <GridCell span={3}>{column.filter}</GridCell>
// </GridRow>
)
)}
{/* <GridCell span={2}> */}
{/* <Button label="Поиск" raised /> */}
{/* <Button
label="Отмена"
raised
onClick={() => {
setIsOpen(false);
}}
/> */}
{/* </GridCell> */}
</div>
</MenuSurface>
</div>
<IconButton
onClick={() => {
setDialogOpen(true);
}}
density={-1}
icon="settings"
/>
<Dialog
isOpen={isDialogOpen}
onOkClick={() => {
localStorage.setItem(
`hiddenColumns-${apiPath + params}`,
JSON.stringify(uncheckedCheckboxes)
);
setDialogOpen(false);
setHiddenColumns(uncheckedCheckboxes);
}}
onCloseClick={() => setDialogOpen(false)}
>
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
{columns.map((column, index) => (
<Checkbox
label={column.label}
onChange={(value) => onCheckboxChange(index, value)}
defaultChecked={!uncheckedCheckboxes.includes(index)}
/>
))}
</div>
</Dialog>
</div>
<DataTable
pagination={true}
count={data?.count}
rowsNumber={data?.results.length}
page={page}
next={data?.next}
previous={data?.previous}
isLoading={isLoading}
onNextClick={() => setPage(page + 1)}
onPreviosClick={() => setPage(page - 1)}
>
<DataTableHead>{headCells}</DataTableHead>
<DataTableBody>{rows}</DataTableBody>
</DataTable>
</div>
);
};
I'm guessing that you want to search bar to effectively filter out rows that don't match. in this case what you want to do is add a filter to the search text (naturally you'll add a state for the search value, but it looks like you'll have that handled.
You'll add your filter here const rows = data?.results?.filter(...).map
You filter function will look something like this
const rows = data?.results.filter((row) => {
// In my own code if I have other filters I just make them return false
// if they don't match
if (
searchText &&
!(
// exact match example
row.field === searchText ||
// case-insensitive example
row.otherField?.toLowerCase().includes(searchText)
// can continue with '||' and matching any other field you want to search by
)
)
return false;
return true;
}).map(...)