React Table - setting up a collapsible row of data - reactjs

I am currently trying to implement React-Table, with a data structure which matches this typescript definition.
export type VendorContent = {
id: number;
name: string;
user_name: string;
dob: string;
};
export type VendorData = {
vendor: string;
rows: VendorContent[];
};
<DataTable defaultData={vendorData} />
Structurally, the design I have looks like this:
Within the DataTable itself, I have something like this:
const columns = [
columnHelper.display({
id: 'actions',
cell: (props) => <p>test</p>,
}),
columnHelper.accessor('name', {
header: () => 'Name',
cell: (info) => info.renderValue(),
}),
columnHelper.accessor('user_name', {
header: () => 'User name',
cell: (info) => info.renderValue(),
}),
columnHelper.accessor('dob', {
header: () => 'DOB',
cell: (info) => info.renderValue(),
}),
];
const DataTable = (props: DataTableProps) => {
const { defaultData } = props;
const [data, setData] = React.useState(() =>
defaultData.flatMap((item) => item.rows)
);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
Now, here's the kicker. Vendor1, and Vendor2 are collapsible rows, and need to be somehow passed into the table, but the defaultData.flatMap((item) => item.rows) which sets up each row, is obviously removing this information / structure. Ergo, I've nothing to hook into to try and render that in the table.
Things I've tried:
const [data, setData] = React.useState(() =>
defaultData
);
Once I try and pass the full Data object in, the column definition complains. (Data passed is no longer an array).
getSubRows within the React Table hook seems to require a full definition of all the columns (all I want is the vendor name there).
Header groups seem to be rendered before the headings, but what I actually want is almost a 'row group' that is expandable / collapsible?
How would I achieve a design similar to the below, with a data structure as illustrated, such that there are row 'headings' which designate the vendor?
I've setup a codesandbox here that sort of illustrates the problem: https://codesandbox.io/s/sad-morning-g5is0e?file=/src/App.js

First steps
Starting from this docs and this example from docs we can create a colapsable row like this (click on the vendor to expand/collapse next rows).
Steps to do it:
Import useExpanded and add it as the second argument of useTable (after the object containing { columns, data })
Replace defaultData.flatMap((item) => item.rows) with myData.map((row) => ({ ...row, subRows: row.rows })) (or if you can just rename rows to subRows and you can just send defaultData (without any mapping or altering of the data).
Add at the beginning of const columns = React.useMemo(() => [ the following snippet:
{
id: "vendor",
Header: ({ getToggleAllRowsExpandedProps }) => (
<span {...getToggleAllRowsExpandedProps()}>VENDOR</span> // this can be changed
),
Cell: ({ row }) =>
row.original.vendor ? <span {...row.getToggleRowExpandedProps({})}>row.vendor</span> : null, // render null if the row is not expandable
},
4. Add the DOB to the columns
Formatting the rows
With some reverse engineering from this question (using colspan) we can render only one value per row (reverse because we want the main row to use all 4 cells).
This will also make the first header part very small and lead to something like this for example.
How we got here from First steps:
We rendered the first cell if the original row has any value for vendor key and
We expanded the cell (in this case) for a span of 4 rows
Main difference in a snippet:
{
row.original.vendor ? (
<td {...row.cells[0].getCellProps()} colSpan={4}>
{row.cells[0].render("Cell")}
</td>
) : (
row.cells.map((cell) => {
return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
})
);
}
Unfortunately I don't think there is another (easier / more straight forward) way to do it (I mean I don't think this is bad, but I think it can be confusing especially if you try to figure it out searching trough so many pages of docs and there is no guide in this direction as far as I know).
Also please note I tried to highlight and explain the process. There might be some small extra adjustments needed in the code.

Related

react ant design table with json - updating data without key problem

I would like to display some json data (coming from my backend and handled in a hook) in a similar way to this : https://ant.design/components/table/#components-table-demo-tree-data
I have a column with a checkbox and a column with an input (both must be editable by the user) and the final json state must be updated with the new datas.
Here you can find the structure of my json : https://pastebin.com/wA0GCs1K
Here you have a screen of the final result :
The code I used to fetch the data :
const [dataServiceInterface, setDataServiceInterface] = useState(null);
useEffect(() => {
CreateApiService.fetchDataServiceInterface(questionFiles, responseFiles).then((result) => {
if (result != null) {
setDataServiceInterface(result);
}
});
}, []);
Here you have the code I used to update the attributes (column constant for the second part) :
const onInputChange = (key, record, index) => (e) => {
e.preventDefault();
console.log(key);
console.log(index);
console.log(record);
console.log(e.target.value);
dataServiceInterface.itemsQ[index][key] = e.target.value;
};
// some other code here
{
title: "Json Name",
dataIndex: "name",
key: "name",
render: (text, record, index) => (
<Input
//defaultValue={text}
value={text}
onChange={onInputChange("name", record, index)}
/>
),
},
Problem is (I think) : As I dont have a key defined in my json datas (parents and children), when I try to update the children it dosent work. I can't change the structure of the json because it's a business constraint. I was thinking of copying my json data in another state, then add the keys ... and still didn't try this solution and don't know if it works. I will update this if it's the case.
Meanwhile, if someone had the same issue and has any idea/hint/suggestion, would appreciate very much. Thx.
I think the problem here is that the component is not rendering the data once you have it. This can be solved by using useState hook, you should lookup for the docs.
I would love to get a little bit more of the component that you are building. But the approach to this would be something like this:
const Component = () {
const [data, useData] = useState([]);
const onInputChange = (key, record, index) => (e) => {
e.preventDefault();
const data = // You get fetch the data here
useData(data);
};
return <>
{
data && data.itemsQ.map((item) => {
// here you display the information contained in each item
})
}
</>
}

Is it safe to delete from an array based state via array index?

I just started with React and I am wondering if it is safe to delete via an array index or if this could end up with some kind of race conditions / timing issues. As concrete example, let's consider a table with 3 rows and in each row there's a remove button:
So my state contains:
first row (index 0)
second row (index 1)
third row (index 2)
Now the user deletes the second row, leaving us with this array content:
first row (index 0)
third row (now index 1 instead of 2)
Next, the user clicks on the third row before the component re-renders, so the remove button for the third row still references index 2 instead of already index 1. So splice(index, 2) is executed causing an error, because index 2 doesn't exist anymore.
Is that theoretically possible in React, if a user is lightning fast clicking the remove buttons?
Sample code / Codesandbox
Here's some concrete code example, which is also available on my Github repository and especially on Codesandbox
App.tsx (relevant code excerpt)
<MyTable
initialRows={[
{ id: 101, content: 'first row' },
{ id: 102, content: 'second row' },
{ id: 103, content: 'third row' }
]}
/>
MyTable.tsx (relevant code excerpt)
import { FC, useState } from 'react';
interface Row {
id: number;
content: string;
}
interface Props {
initialRows: Row[];
}
export const MyTable: FC<Props> = function ({ initialRows }) {
const [rows, setRows] = useState(initialRows);
return (
<table>
<tbody>
{rows.map((row, index) => (
<tr key={`id${row.id}`}>
<td>{row.content}</td>
<td>
<button
type="button"
onClick={() => {
const newRows = [...rows];
newRows.splice(index, 1);
setRows(newRows);
}}
>
remove
</button>
</td>
</tr>
))}
</tbody>
</table>
);
};
Your onClick handler should look something like this:
onClick={() => setRows(rows.filter(elem => elem.id != row.id ))}
This is the best way to do it if you have an array of objects. In a real world application there should be an API call to change the data in the database first and if that is success, only then update the rows state using the filter method.
No, this is not safe. A simple example to show why this is the case is as follows:
const Component = () => {
const [arrayState, setArrayState] = useState(['item 1', 'item 2', 'item 3'])
const deleteItem = (index) => {
const newArray = [...arrayState];
newArray.splice(index, 1);
setArrayState(newRows);
}
const deleteItemAfterSimulatedDelay = (index) => {
// assume this simulates a slow network request
setTimeout(() => {
deleteItem(index)
}, 300)
}
useEffect(() => {
deleteItemAfterSimulatedDelay();
deleteItem(index);
}, [someOtherDependency])
}
Assume that deleteItem is the result of a quick network request, whereas deleteItemAfterSimulatedDelay is the result of a slow network request - this is obviously unrealistic but the results of network delays etc. could cause you to delete the incorrect item.
A better solution would be to filter the existing array per Amir's response, as even 'deleting by index' after finding that index is still subject to race conditions.
const deleteByKey = (key) => {
const itemIndex = array.find(item => {
item.key === key
})
// this will guarantee that we're deleting an item with the correct credentials but is still subject to race.
const deleteItem(itemIndex)
}

React Data Grid, how can I remove a row?

I using the example provided by React, and I want to have a button on every row and when it is clicked a row to be deleted.
https://codesandbox.io/s/5vy2q8owj4?from-embed
I am new to reactjs, it is possible to do it?
What I thought to do is to add another row with a button and inside the component to have a function like this, I don't know how to call this function from the outside:
{ key: "", name: "", formatter: () => <button onClick={() =>
this.deleteRows(title)}>Delete</button>}
deleteRows = (id) => {
let rows = this.state.rows.slice()
rows = rows.filter(row => row.id !== id)
this.setState({ rows })
}
Thanks
It's possible. You may use getCellActions to achieve this. Here's a working example: https://codesandbox.io/s/5091lpolzk

Delete particular item in React Table?

Header: "Delete",
id:'delete',
accessor: str => "delete",
Cell: (row)=> (
<span onClick={(row) => this.onRemoveHandler(row,props)} style={{cursor:'pointer',color:'blue',textDecoration:'underline'}}>
Delete
</span>
)
React Table
This is related to the header delete span link.The code snippets shows the render the delete label with hyperlink.
Here once a user click on delete link how can I get the id of that particular row.
ID has been already assgined to all the row from the json data.
So,How to pass the cellInfo or rowInfo inside the onClick function .
If you check out the docs (specificaly under 'Renderers'), the row object the cell receives is in the following format:
{
// Row-level props
row: Object, // the materialized row of data
original: , // the original row of data
index: '', // the index of the row in the original array
viewIndex: '', // the index of the row relative to the current view
level: '', // the nesting level of this row
nestingPath: '', // the nesting path of this row
aggregated: '', // true if this row's values were aggregated
groupedByPivot: '', // true if this row was produced by a pivot
subRows: '', // any sub rows defined by the `subRowKey` prop
// Cells-level props
isExpanded: '', // true if this row is expanded
value: '', // the materialized value of this cell
resized: '', // the resize information for this cell's column
show: '', // true if the column is visible
width: '', // the resolved width of this cell
maxWidth: '', // the resolved maxWidth of this cell
tdProps: '', // the resolved tdProps from `getTdProps` for this cell
columnProps: '', // the resolved column props from 'getProps' for this cell's column
classes: '', // the resolved array of classes for this cell
styles: '' // the resolved styles for this cell
}
Depending on what your input data looks like, you can use this information to delete from the dataset. If you plan on dynamically editing your data, you should store it in the state, so that the table component can update according to your edits. Assuming that in your state, you save your dataset as data, and use that to populate the table, you can alter the state in your onclick function:
Header: "Delete",
id:'delete',
accessor: str => "delete",
Cell: (row)=> (
<span onClick={() => {
let data = this.state.data;
console.log(this.state.data[row.index]);
data.splice(row.index, 1)
this.setState({data})
}}>
Delete
</span>
)
so a rough approximation of your app would like this:
this.state = {
data: <your data set>
}
<ReactTable
data={this.state.data}
columns={[
<other columns you have>,
{
Header: "Delete",
id:'delete',
accessor: str => "delete",
Cell: (row)=> (
<span style={{cursor:'pointer',color:'blue',textDecoration:'underline'}}
onClick={() => {
let data = this.state.data;
console.log(this.state.data[row.index]);
data.splice(row.index, 1)
this.setState({data})
}}>
Delete
</span>
)}
]}
/>
And of course, you don't need to log that row to the console, that doesn't need to be there. This is also just the quickest and easiest way to handle it, you could instead use the row object to get any specific element you want (id, name, etc.) and use that to remove from the dataset
An important note though: There is a big difference between viewIndex and index, index is what you want to use for your specific case
If you are like me and are using React-Table v7 and you are also using a hooks based approach in your components you will want to do it this way.
const [data, setData] = useState([]);
const columns = React.useMemo(
() => [
{
Header: 'Header1',
accessor: 'Header1Accessor',
},
{
Header: 'Header2',
accessor: 'Header2Accessor',
},
{
Header: 'Delete',
id: 'delete',
accessor: (str) => 'delete',
Cell: (tableProps) => (
<span style={{cursor:'pointer',color:'blue',textDecoration:'underline'}}
onClick={() => {
// ES6 Syntax use the rvalue if your data is an array.
const dataCopy = [...data];
// It should not matter what you name tableProps. It made the most sense to me.
dataCopy.splice(tableProps.row.index, 1);
setData(dataCopy);
}}>
Delete
</span>
),
},
],
[data],
);
// Name of your table component
<ReactTable
data={data}
columns={columns}
/>
The important part is when you are defining your columns make sure that the data in your parent component state is part of the dependency array in React.useMemo.

Antd: Is it possible to move the table row expander inside one of the cells?

I have an antd table where the data inside one of the columns can get pretty large. I am showing this data in full when the row is expanded but because the cell with a lot of data is on the right side of the screen and the expander icon is on the left side of the screen it is not very intuitive. What I would like to do is move the expander icon inside the actual cell so that the user knows they can click the + to see the rest of the data.
Thanks in advance.
Yes, you can and you have to dig a little deeper their docucmentation...
According to rc-table docs you can use expandIconColumnIndex for the index column you want to add the +, also you have to add expandIconAsCell={false} to make it render as part of the cell.
See Demo
This is how you can make any column expendable.
First add expandedRowKeys in your component state
state = {
expandedRowKeys: [],
};
Then you need to add these two functions onExpand and updateExpandedRowKeys
<Table
id="table-container"
rowKey={record => record.rowKey}
className={styles['quote-summary-table']}
pagination={false}
onExpand={this.onExpand}
expandedRowKeys={this.state.expandedRowKeys}
columns={columns({
updateExpandedRowKeys: this.updateExpandedRowKeys,
})
}
dataSource={this.data}
oldTable={false}
/>
This is how you need to define the function so
that in expandedRowKeys we will always have
updates values of expanded rowKeys
onExpand = (expanded, record) => {
this.updateExpandedRowKeys({ record });
};
updateExpandedRowKeys = ({ record }) => {
const rowKey = record.rowKey;
const isExpanded = this.state.expandedRowKeys.find(key => key === rowKey);
let expandedRowKeys = [];
if (isExpanded) {
expandedRowKeys = expandedRowKeys.reduce((acc, key) => {
if (key !== rowKey) acc.push(key);
return acc;
}, []);
} else {
expandedRowKeys.push(rowKey);
}
this.setState({
expandedRowKeys,
});
}
And finally, you need to call the function updateExpandedRowKeys
for whichever column you want to have the expand-collapse functionality available.
Even it can be implemented for multiple columns.
export const columns = ({
updateExpandedRowKeys,
}) => {
let columnArr = [
{
title: 'Product',
key: 'productDes',
dataIndex: 'productDes',
className: 'productDes',
render: (text, record) => (
<span onClick={rowKey => updateExpandedRowKeys({ record })}>
{text}
</span>
),
}, {
title: 'Product Cat',
key: 'productCat',
dataIndex: 'productCat',
className: 'product-Cat',
}]
}

Resources