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
Related
How in react-table to avoid cell rerender when changing data? In simple terms: each cell is an input, onBlur a patch to the backend is fired and again the data is retrieved from the server. It seems that only the cells with my custom component are being re-rendered, and, of course, the whole table, which is probably intended.
A basic table component built according to the examples from the official react-table documentation. Only added scrolling ScrollArea and styling from Table.
import React from "react";
import {
flexRender,
getCoreRowModel,
useReactTable,
} from "#tanstack/react-table";
import { Table } from "#mantine/core";
import { ScrollArea } from "#mantine/core";
const TableModel = ({ data, columns }) => {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<ScrollArea style={{ height: "50vh" }}>
<Table style={{ position: "relative" }}>
<thead
style={{ position: "sticky", top: "0", backgroundColor: "black" }}
>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</Table>
</ScrollArea>
);
};
export default TableModel;
Here I defined the columns and downloaded the data from the server. IsLoading fires only on the first render.
import React from "react";
import TableModel from "./TableModel";
import { createColumnHelper } from "#tanstack/react-table";
import { v4 as uuidv4 } from "uuid";
import { usePZItemsData } from "../hooks/usePZItemsData";
import { CustomInputHover } from "./CustomInputHover";
export const ItemList = ({ id }) => {
const { data: pzItemsData, isLoading } = usePZItemsData(id);
const columnHelper = createColumnHelper();
const columns = React.useMemo(
() => [
columnHelper.accessor("id"),
columnHelper.accessor("product.name", { header: "Nazwa" }),
columnHelper.accessor("quantity", {
header: "Nazwa",
cell: (i) => <CustomInputHover i={i} />,
}),
],
[]
);
return (
<>
{isLoading ? (
<h1>Loading...</h1>
) : (
<TableModel data={pzItemsData?.data} columns={columns} />
)}
</>
);
};
Custom input cell. Using input or input from the framework slowed down the rendering terribly. I used this solution, which reduces the rerender time drastically, but it is still long. In short, when you click on a div, a Input appears and then its validation.
import { NumberInput, TextInput } from "#mantine/core";
import { usePrevious } from "#mantine/hooks";
import React, { useEffect } from "react";
import { useUpdatePZItem } from "../hooks/usePZItemActions";
export const CustomInputHover = ({ i }) => {
const [clicked, setClicked] = React.useState(false);
const [value, setValue] = React.useState();
const previousValue = usePrevious(value);
const { mutate, isError, isSuccess } = useUpdatePZItem();
useEffect(() => {
setValue(i.row.original.quantity);
}, [i.row.original.quantity]);
const handleBlur = (e) => {
if (previousValue != value) {
mutate({
id: i.row.original.id,
data: { quantity: value },
});
} else {
if (!isError) {
setClicked(false);
}
}
};
React.useEffect(() => {
if (isSuccess) {
setClicked(false);
}
}, [isSuccess]);
React.useEffect(() => {
if (isError) {
setClicked(true);
}
}, [isError]);
const handleOnKeyDown = (e) => {
if (e.key !== "Tab" && e.key !== "Shift" && e.key !== "Alt") {
setClicked(true);
console.log(e);
}
};
return (
<>
{clicked ? (
<NumberInput
style={{ width: "180px" }}
autoFocus
value={value}
size="xs"
radius="xs"
variant="filled"
onBlur={(e) => handleBlur(e)}
async
error={isError && "Podaj poprawną wartość"}
onFocus={(e) => e.target.select()}
onChange={(val) => setValue(val)}
/>
) : (
<div
onClick={() => setClicked(true)}
tabIndex={0}
onKeyDown={(e) => handleOnKeyDown(e)}
>
{i.row.original.quantity}
</div>
)}
</>
);
};
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(...)
I try to sort the value and update the same of both columns by onclick of name and Lname(its TH)
Below is the link which am refering
https://codesandbox.io/s/customtablereact-forked-drnc0
am trying to sort both column, by ascending order by
what am missing here
https://codesandbox.io/s/customtablereact-forked-fnvlo
any suggestion, how to implement sorting here
please refer below snippet
import React from "react";
export const CustTable = ({ columns, data, setData, search, setSearch }) => {
const [editable, setEditable] = React.useState("");
const [sortType, setSortType] = React.useState("asc");
const cellEditable = (label) => {
data.map((l) => {
if (l.name === label.name) {
setEditable(label.name);
}
});
};
const changeLastNameBasedIndex = (name, e, index) => {
e.persist();
setData((prevData) => {
return [
...prevData.slice(0, index),
{ name, lname: e.target.value },
...prevData.slice(index + 1)
];
});
};
const changeLastName = (name, e, index) => {
let filterData = [];
e.persist();
setData((prevData) => {
const dummyData = [...prevData];
filterData = dummyData.filter((label) => {
return label.name !== name;
});
return [...filterData, { name, lname: e.target.value }];
});
};
const items = () => {
let resultItems = data;
if (search) {
resultItems = data.filter((item) =>
item.name.toLowerCase().includes(search.toLowerCase())
);
}
return resultItems.map((label, index) => {
return (
<tr style={{ display: "flex" }} key={index}>
<span style={{ width: "35%" }}>{label.name} </span>
{editable === label.name ? (
<input
type="text"
value={label.lname}
onChange={
search
? (e) => changeLastName(label.name, e, index)
: (e) => changeLastNameBasedIndex(label.name, e, index)
}
/>
) : (
<span onClick={() => cellEditable(label)}>{label.lname} </span>
)}
</tr>
);
});
};
const searchData = (e) => {
setSearch(e.target.value);
};
return (
<>
<div>
<input
type="search"
placeholder="search"
onChange={(e) => searchData(e)}
/>
</div>
<tr>
<th>First</th>
<th>Second</th>
</tr>
{items()}
</>
);
};
I made a short example of how to sort table data.
To sort string, we have a trick to avoid problems, you can understand how this works here.
App.jsx
import React from 'react';
import SortTable from './SortTable'
function App() {
const data = [
{
name: 'Wescley',
lname: 'Jeto',
},
{
name: 'Amanda',
lname: 'Nudes',
},
{
name: 'Gabriel',
lname: 'Pensador',
},
{
name: 'Allan',
lname: 'Brado',
},
{
name: 'Kelly',
lname: 'guiça',
},
];
return (
<div className="App">
<SortTable data={data}/>
</div>
);
}
export default App;
/components/SortTable/index.jsx
import React, {useState} from "react";
import generateUuid from 'generate-uuid';
export default ({data}) => {
const [tableData, setTableData] = useState(data)
const sortByName = () => {
const copy = [...tableData]
copy.sort((a, b) => (a.name > b.name) ? 1 : -1);
setTableData(copy)
}
const sortByLName = () => {
const copy = [...tableData]
copy.sort((a, b) => (a.lname > b.lname) ? 1 : -1);
setTableData(copy)
}
return(
<>
<table border="1" style={{ margin: '0 auto', marginTop: '100px' }}>
<thead>
<tr>
<td onClick={sortByName}>Name</td>
<td onClick={sortByLName}>lName</td>
</tr>
</thead>
<tbody>
{
tableData.map((item) => {
const uuid = generateUuid();
return (<tr key={uuid}>
<td>{item.name}</td>
<td>{item.lname}</td>
</tr>)
})
}
</tbody>
</table>
</>
);
}