QUI React Table

Row Expansion

Name
Info
First Name
Last Name
Age
Visits
Status
Profile Progress
Show
10
{
"expanded": {},
"rowSelection": {}
}
import {useState} from "react"
import {ChevronDown, ChevronRight, ChevronRightIcon} from "lucide-react"
import {CodeHighlight} from "@qui/mdx-docs"
import {
QButton,
QCheckbox,
QIconButton,
QPagination,
QProgressCircle,
QTextInput,
} from "@qui/react"
import {
Column,
ColumnDef,
ExpandedState,
flexRender,
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
QTable,
QTbody,
QTd,
QTh,
QThead,
QTr,
Table,
useReactTable,
useTablePagination,
} from "@qui/react-table"
import {Person, usePersonData} from "~utils/data"
const columns: ColumnDef<Person>[] = [
{
columns: [
{
accessorKey: "firstName",
cell: ({getValue, row}) => {
const indeterminate = row.getIsSomeSelected()
const checked = row.getIsSelected()
return (
<div
className="inline-flex items-center gap-2"
style={{
// Since rows are flattened by default,
// we can use the row.depth property
// and paddingLeft to visually indicate the depth
// of the row
paddingLeft: `${row.depth * 2}rem`,
}}
>
<>
<QCheckbox
checked={checked}
indeterminate={indeterminate}
onChange={() => row.toggleSelected(!checked)}
/>
{row.getCanExpand() ? (
<div className="inline-flex items-center justify-center">
<QIconButton
className="inline-flex justify-center"
icon={
<ChevronRightIcon
size={16}
style={{
transform: row.getIsExpanded()
? "rotate(90deg)"
: "unset",
transition: "transform 161ms ease",
}}
/>
}
onClick={row.getToggleExpandedHandler()}
size="s"
/>
</div>
) : null}
<span>{getValue()}</span>
</>
</div>
)
},
header: ({table}) => {
return (
<>
<div className="flex items-center gap-2">
<QIconButton
icon={
table.getIsAllRowsExpanded() ? ChevronDown : ChevronRight
}
onClick={table.getToggleAllRowsExpandedHandler()}
size="s"
/>
<span>First Name</span>
</div>
</>
)
},
},
{
accessorFn: (row) => row.lastName,
cell: (info) => info.getValue(),
header: "Last Name",
id: "lastName",
},
],
header: "Name",
},
{
columns: [
{
accessorKey: "age",
header: "Age",
},
{
accessorKey: "visits",
header: "Visits",
},
{
accessorKey: "status",
header: "Status",
},
{
accessorKey: "progress",
header: "Profile Progress",
},
],
header: "Info",
},
]
export default function Expanding() {
const {data = [], isFetching, refetch} = usePersonData(100, 5, 3)
const refreshData = () => refetch()
const [expanded, setExpanded] = useState<ExpandedState>({})
const table = useReactTable({
columns,
data,
filterFns: {},
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSubRows: (row) => row.subRows,
onExpandedChange: setExpanded,
state: {
expanded,
},
})
const paginationProps = useTablePagination(table)
return (
<div className="flex flex-col gap-2 overflow-x-auto p-2">
<div className="mb-2 inline-flex items-center gap-2">
<QButton onClick={refreshData} variant="outline">
Refresh Data
</QButton>
{isFetching ? <QProgressCircle size="xs" /> : null}
</div>
<div className="overflow-x-auto">
<QTable>
<QThead>
{table.getHeaderGroups().map((headerGroup) => (
<QTr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<QTh key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder ? null : (
<div className="inline-flex flex-col gap-1">
<div
className="inline-flex items-center justify-center"
style={{
minHeight: header.column.getCanFilter()
? 28
: "auto",
}}
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</div>
{header.column.getCanFilter() ? (
<Filter column={header.column} table={table} />
) : null}
</div>
)}
</QTh>
)
})}
</QTr>
))}
</QThead>
<QTbody>
{table.getRowModel().rows.map((row) => {
return (
<QTr key={row.id} isSelected={row.getIsSelected()}>
{row.getVisibleCells().map((cell) => {
return (
<QTd key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</QTd>
)
})}
</QTr>
)
})}
</QTbody>
</QTable>
</div>
<div className="mt-4">
<QPagination
className="mt-4"
{...paginationProps}
renderPageMeta={(context) =>
`${context.currentPage} of ${context.totalPages}`
}
rowsPerPageLabel="Show"
rowsPerPageOptions={[10, 20, 50]}
/>
</div>
<CodeHighlight
className="max-h-[400px] overflow-y-auto"
code={JSON.stringify(
{expanded, rowSelection: table.getState().rowSelection},
null,
2,
)}
language="json"
/>
</div>
)
}
function Filter({
column,
table,
}: {
column: Column<Person, any>
table: Table<Person>
}) {
const firstValue = table
.getPreFilteredRowModel()
.flatRows[0]?.getValue(column.id)
const columnFilterValue = column.getFilterValue()
return typeof firstValue === "number" ? (
<div className="flex gap-2">
<QTextInput
className="w-24"
inputProps={{min: 0, type: "number"}}
onChange={(e, value) =>
column.setFilterValue((old: [number, number]) => [value, old?.[1]])
}
placeholder="Min"
size="s"
value={(columnFilterValue as [number, number])?.[0] ?? ""}
/>
<QTextInput
className="w-24"
inputProps={{min: 0, type: "number"}}
onChange={(e, value) =>
column.setFilterValue((old: [number, number]) => [old?.[0], value])
}
placeholder="Max"
size="s"
value={(columnFilterValue as [number, number])?.[1] ?? ""}
/>
</div>
) : (
<QTextInput
className="w-36"
onChange={(e, value) => column.setFilterValue(value)}
placeholder="Search..."
size="s"
value={(columnFilterValue ?? "") as string}
/>
)
}