Installation
How to install Trove and its dependencies.
Installation
pnpm dlx shadcn@latest add http://trove.pragyan.fyi/r/trove.json
Under the Hood
Trove uses @tanstack/react-table
for its core table logic
and is styled with shadcn/ui
components.
Install Tanstak table
pnpm add @tanstack/react-table
Copy and paste the following code into your project.
"use client";
import React, { useState, createContext, useContext, ReactNode } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { ChevronDown, ChevronRight } from "lucide-react";
import { flexRender, Table as TanstackTable, Row } from "@tanstack/react-table";
import { cn } from "@/lib/utils";
type TroveContextType = {
expanded: Set<string>;
toggle: (id: string) => void;
expandAll: () => void;
collapseAll: () => void;
animationDuration?: number;
clickableRows: boolean;
};
const TroveContext = createContext<TroveContextType | null>(null);
const useTrove = () => {
const context = useContext(TroveContext);
if (!context) throw new Error("Trove components must be used within <Trove>");
return context;
};
type TroveProps<TData> = {
table: TanstackTable<TData>;
children: ReactNode;
getRowId: (row: TData) => string;
renderExpanded: (row: Row<TData>) => ReactNode;
defaultExpanded?: string[];
animationDuration?: number;
className?: string;
onExpandedChange?: (expanded: Set<string>) => void;
clickableRows?: boolean;
};
export function Trove<TData>({
table,
children,
getRowId,
renderExpanded,
defaultExpanded = [],
animationDuration = 200,
className,
onExpandedChange,
clickableRows = false,
}: TroveProps<TData>) {
const [expandedRows, setExpandedRows] = useState<Set<string>>(
new Set(defaultExpanded)
);
const toggle = (id: string) => {
const next = new Set(expandedRows);
next.has(id) ? next.delete(id) : next.add(id);
setExpandedRows(next);
onExpandedChange?.(next);
};
const expandAll = () => {
const allIds = new Set(
table.getRowModel().rows.map((row) => getRowId(row.original))
);
setExpandedRows(allIds);
onExpandedChange?.(allIds);
};
const collapseAll = () => {
setExpandedRows(new Set());
onExpandedChange?.(new Set());
};
const contextValue: TroveContextType = {
expanded: expandedRows,
toggle,
expandAll,
collapseAll,
animationDuration,
clickableRows,
};
return (
<TroveContext.Provider value={contextValue}>
<div className={cn("space-y-4", className)}>
{children}
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
<TableHead className="w-12" />
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table
.getRowModel()
.rows.map((row) => (
<TroveRow
key={row.id}
row={row}
getRowId={getRowId}
renderExpanded={renderExpanded}
/>
))
) : (
<TableRow>
<TableCell
colSpan={
table.getHeaderGroups()[0]?.headers.length + 1 || 1
}
className="h-24 text-center text-muted-foreground"
>
No results found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</TroveContext.Provider>
);
}
export function TroveHeader({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <div className={cn("space-y-1", className)}>{children}</div>;
}
export function TroveTitle({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <h1 className={cn("text-3xl font-bold", className)}>{children}</h1>;
}
export function TroveDescription({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <p className={cn("text-muted-foreground", className)}>{children}</p>;
}
export function TroveActions({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<div className={cn("flex items-center gap-2", className)}>{children}</div>
);
}
export function TroveExpandAllButton({
children = "Expand All",
className,
...props
}: React.ComponentProps<typeof Button> & {
children?: ReactNode;
}) {
const { expandAll } = useTrove();
return (
<Button
variant="outline"
size="sm"
onClick={expandAll}
className={className}
{...props}
>
{children}
</Button>
);
}
export function TroveCollapseAllButton({
children = "Collapse All",
className,
...props
}: React.ComponentProps<typeof Button> & {
children?: ReactNode;
}) {
const { collapseAll } = useTrove();
return (
<Button
variant="outline"
size="sm"
onClick={collapseAll}
className={className}
{...props}
>
{children}
</Button>
);
}
export function TroveContent({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<div className={cn("p-4 pl-12 bg-muted/50", className)}>{children}</div>
);
}
function TroveRow<TData>({
row,
getRowId,
renderExpanded,
}: {
row: Row<TData>;
getRowId: (row: TData) => string;
renderExpanded: (row: Row<TData>) => ReactNode;
}) {
const { expanded, toggle, animationDuration, clickableRows } = useTrove();
const rowId = getRowId(row.original);
const isOpen = expanded.has(rowId);
const handleToggle = () => {
toggle(rowId);
};
const handleRowClick = (event: React.MouseEvent) => {
if (!clickableRows) return;
if ((event.target as Element).closest("button")) return;
handleToggle();
};
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
handleToggle();
}
};
return (
<>
<TableRow
className={cn(
"group",
clickableRows && "cursor-pointer hover:bg-muted/50"
)}
onClick={handleRowClick}
>
<TableCell className="w-12">
<Button
variant="ghost"
size="sm"
onClick={handleToggle}
onKeyDown={handleKeyDown}
className="p-0 h-8 w-8 hover:bg-muted transition-colors"
aria-expanded={isOpen}
aria-label={isOpen ? "Collapse row" : "Expand row"}
>
{isOpen ? (
<ChevronDown className="h-4 w-4 transition-transform duration-200" />
) : (
<ChevronRight className="h-4 w-4 transition-transform duration-200" />
)}
</Button>
</TableCell>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
<TableRow>
<TableCell
colSpan={row.getVisibleCells().length + 1}
className="p-0 border-0"
>
<div
style={{ transitionDuration: `${animationDuration}ms` }}
className={cn(
"overflow-hidden transition-all ease-in-out",
isOpen ? "max-h-[1000px] opacity-100" : "max-h-0 opacity-0"
)}
>
{renderExpanded(row)}
</div>
</TableCell>
</TableRow>
</>
);
}
Anatomy
Here is a basic example of how to use the Trove
component.
import {
Trove,
TroveContent,
TroveHeader,
TroveTitle,
TroveActions,
TroveExpandAllButton,
TroveCollapseAllButton,
} from "@/components/ui/trove";
function MyTable() {
return (
<Trove
table={table}
getRowId={(row) => row.id}
renderExpanded={(row) => (
<TroveContent>
<p>Expanded content for {row.original.name}</p>
</TroveContent>
)}
clickableRows
>
<TroveHeader>
<TroveTitle>My Data Table</TroveTitle>
<TroveDescription>Click to expand the rows</TroveDescription>
<TroveActions>
<TroveExpandAllButton />
<TroveCollapseAllButton />
</TroveActions>
</TroveHeader>
</Trove>
);
}
Types
Prop | Type | Default |
---|---|---|
table? | TanstackTable<TData> | required |
children? | ReactNode | required |
getRowId? | (row: TData) => string | required |
renderExpanded? | (row: Row<TData>) => ReactNode | required |
defaultExpanded? | string[] | [] |
animationDuration? | number | 200 |
className? | string | undefined |
onExpandedChange? | (expanded: Set<string>) => void | undefined |
clickableRows? | boolean | false |