I'm trying to adapt the following example from tanstack documentation: https://tanstack.com/table/latest/docs/framework/react/examples/editable-data
I created a table with a single editable column named Current Value. It works, but when I try to navigate the table with tab key, I see the cells abruptly changing their values. I've tried comparing the id of the input element in onBlur against the row index with no success. Can someone suggest what I'm missing?
Here's my code so far:
import {
RowData,
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import React, { useState, useEffect, useMemo } from "react";
import TestModeSelect from "./TestModeSelect";
import { Tooltip } from "flowbite-react";
declare module "@tanstack/react-table" {
interface TableMeta<TData extends RowData> {
updateData: (rowIndex: number, columnId: string, value: unknown) => void;
}
}
const ConfigPanel: React.FC = () => {
const [data, setData] = useState<MkDeviceCell[]>(() => []);
const [testModeOptions, setTestModeOptions] = useState<MkDeviceTestMode[]>(
[]
);
const [quickModeOptions, setQuickModeOptions] = useState<MkDeviceQuickMode[]>(
[]
);
const readConfig = () => {
getDeviceConfig()
.then((result) => {
setData(result.cells);
setTestModeOptions(result.test_modes);
setQuickModeOptions(result.quick_modes);
})
.catch((err) => {
error(`Error occurred while trying to read device config: ${err}`);
});
};
const writeConfig = () => {
setDeviceConfig(data).then((success) => {
if (success) {
readConfig();
}
});
};
const factoryReset = async () => {
let result = await ask(
"This action will factory reset the device and cannot be reverted. Are you sure?",
{
title: "Tiny CC Tool",
type: "warning",
}
);
if (result) {
if (await invoke("factory_reset", {})) {
await readConfig();
}
}
};
const handleCellValueChange = (
rowId: number,
_columnId: string,
value: string
) => {
setData((old) => {
return old.map((row) => {
if (row.address === rowId) {
row.current_value = parseInt(value);
}
return row;
});
});
};
const columns = useMemo<ColumnDef<MkDeviceCell>[]>(
() => [
{
header: "Address",
footer: (props) => props.column.id,
accessorFn: (row) => row.address,
id: "address",
},
{
header: "Name",
footer: (props) => props.column.id,
accessorFn: (row) => [row.name, row.description],
id: "name",
cell: ({ getValue }) => {
const [name, description] = getValue() as [string, string];
return (
<>
<Tooltip
content={toolTipData(description)}
placement="right"
style="light"
>
{`${name}`}
</Tooltip>
</>
);
},
},
{
header: "Current Value",
footer: (props) => props.column.id,
accessorFn: (row) => [
row.current_value,
row.min_value,
row.max_value,
row.allowed_values,
row.address,
],
id: "current_value",
cell: ({ getValue, row: { index }, column: { id }, table }) => {
const [initialValue, minValue, maxValue, allowedValues, address] =
getValue() as [number, number, number, number[], number];
const [value, setValue] = useState(initialValue.toString());
const [activeValue, setActiveValue] = useState("");
// When the input is blurred, we'll call our table meta's updateData function
const onBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const elemId = `ROW_${address}`;
if (elemId !== e.target.id) {
return;
}
const val = parseInt(value || "");
if (allowedValues.includes(val)) {
table.options.meta?.updateData(index, id, val);
} else if (val >= minValue && val <= maxValue) {
table.options.meta?.updateData(index, id, val);
} else {
// Handle out-of-range values
console.error(
`Value ${val} is outside the allowed range (${minValue} - ${maxValue})`
);
// You can add additional error handling or feedback to the user here
}
};
const onFocus = (e: React.FocusEvent<HTMLInputElement>) => {
setActiveValue(e.target.value);
};
const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log("On change");
setValue(e.target.value);
};
// If the initialValue is changed external, sync it up with our state
useEffect(() => {
setValue(initialValue.toString());
}, [initialValue]);
return (
<>
<input
id={`ROW_${address}`}
key={`ROW_${address}`}
value={value}
onChange={handleOnChange}
onBlur={onBlur}
onFocus={onFocus}
/>
</>
);
},
},
],
[]
);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
meta: {
updateData: (rowIndex, columnId, value) => {
handleCellValueChange(rowIndex, columnId, value as string);
},
},
debugTable: true,
});
function toolTipData(description: string) {
const array = description.split("\n");
return (
<div className="text-left">
{array.map((element, index) => (
<div key={index}>
{element}
<br />
</div>
))}
</div>
);
}
function showTable(data: Array<object>) {
if (data.length > 0) {
return (
<table className="w-full text-center table border border-collapse ">
<thead className="sticky top-0 bg-gray-50 text-center table-header-group border border-collapse ">
{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 className="text-center ">
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className="border border-separate ">
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
);
} else {
return <div className="bg-gray-100 h-full"></div>;
}
}
return (
<div className="h-full flex flex-col border rounded-lg">
<div className="overflow-y-scroll h-full">{showTable(data)}</div>
<div className="p-2 bg-gray-50 border rounded-t-none rounded-lg sticky bottom-0 flex flex-row justify-between md:flex-wrap">
<div>
<button
onClick={() => readConfig()}
className=" bg-blue-700 text-white text-xs p-2 border border-blue-950 rounded-l-lg hover:bg-blue-900 md:text-xs lg:text-lg"
>
Read Config
</button>
<button
onClick={() => writeConfig()}
className=" bg-blue-700 text-white border border-l-0 border-blue-950 text-xs p-2 hover:bg-blue-900 md:text-xs lg:text-lg"
>
Save Config
</button>
<button
onClick={() => factoryReset()}
className=" bg-blue-700 text-white text-xs border border-l-0 border-blue-950 rounded-r-lg p-2 hover:bg-blue-900 md:text-xs lg:text-lg"
>
Factory Reset
</button>
</div>
<div className="float right md:mt-2">
<TestModeSelect
testModeOptions={testModeOptions}
quickOptions={quickModeOptions}
/>
</div>
</div>
</div>
);
};
export default ConfigPanel;