Reputation: 211
I'm currently trying Tanstack Table, which is a great library!
What I'm trying to do is build up a table that looks like this:
My data come from an API with this type:
type Accounting = {
category: string;
total: number; // Sum of all expenses
expenses: {
name: string;
value: number;
}[];
};
Here is what I've got so far:
const columns = React.useMemo(
() => [
columnHelper.accessor("category", {
header: "Category",
id: "category",
cell: (info) => {
return (
<>
{info.row.getCanExpand() && (
<button
{...{
onClick: info.row.getToggleExpandedHandler(),
style: { cursor: "pointer" },
className: "mr-2",
}}
>
{info.row.getIsExpanded() ? (
<Chevron direction="down" />
) : (
<Chevron direction="right" />
)}
</button>
)}
{info.getValue()}
</>
);
},
}),
columnHelper.accessor("expenses", {
header: undefined,
id: "expense-name",
cell: (info) => {
// FIXME : this returns a table of objects, I want one row per expense
return info.getValue();
},
}),
columnHelper.accessor("expenses", {
aggregationFn: "sum",
id: "expense-value",
header: "Expense",
cell: (info) => {
// FIXME : this returns a table of objects, I want one row per expense
return info.getValue();
},
}),
],
[]
);
const grouping = React.useMemo(() => ["category"], []);
const table = useReactTable({
data,
columns,
state: {
grouping,
expanded,
},
getExpandedRowModel: getExpandedRowModel(),
getGroupedRowModel: getGroupedRowModel(),
getCoreRowModel: getCoreRowModel(),
onExpandedChange: setExpanded,
autoResetExpanded: false,
});
The problem here is that I've got a One-To-Many relationship between one category and multiple expenses for this category.
The above code does not work because tanstack table tries to render a list of objects (a list of expense).
I haven't figure out how Tanstack Table can handle this ? Should I override some sort of render method ? Is Tanstack Table the good choice for this kind of data ?
Respectfully.
Upvotes: 16
Views: 7563
Reputation: 42288
You can draw a lot from the React Table Expanding Example.
The critical part which you are missing is the getSubRows
function on the table. Rather than rendering the individual expenses within the cell
of the parent category, we want to create a new row in the table for each expense.
Our two columns are a title and a number. For a top-level Accounting
object, it is the category
and the total
. For an individual expense, it's the name
and value
. Since those are different properties, we'll need to do some mapping.
Let's start really simple and define those two columns for the top-level objects:
const columns = React.useMemo(
() => [
columnHelper.accessor("category", {
header: "Category",
id: "category"
}),
columnHelper.accessor("total", {
header: "Expense",
id: "expense-value"
})
],
[]
);
Now, let's add in the expand functionality. (You do not need the grouping functionality.)
We will store the expanded
state in the component and pass it to the table, syncing changes through the onExpandedChange
option.
We will add a new getSubRows
option to your table. This is a function that takes the parent Accounting
object and returns an array of child Accounting
objects. As explained earlier, we need to rename some properties to make the interfaces match up. Our columns
config expects an object with category
and total
. I set an empty array of expenses
here because the child rows do not have children of their own.
const [expanded, setExpanded] = React.useState<ExpandedState>({});
const table = useReactTable({
data,
columns,
state: {
expanded
},
getExpandedRowModel: getExpandedRowModel(),
getCoreRowModel: getCoreRowModel(),
onExpandedChange: setExpanded,
getSubRows: (originalRow) =>
originalRow.expenses.map((expense) => ({
category: expense.name,
total: expense.value,
expenses: []
}))
});
For your columns, the only change that I made from the previous is to display an expand/contract button on the parent rows (which you already had in your attempt).
const columns = React.useMemo(
() => [
columnHelper.accessor("category", {
header: "Category",
id: "category",
cell: (info) => {
return (
<>
{info.row.getCanExpand() && (
<button onClick={info.row.getToggleExpandedHandler()}>
{info.row.getIsExpanded() ? "-" : "+"}
</button>
)}
{info.getValue()}
</>
);
}
}),
columnHelper.accessor("total", {
header: "Expense",
id: "expense-value"
})
],
[]
);
Now we have fully-functional row expansion. It's not beautiful but I'll leave it to you to add styling.
Upvotes: 24