QUI React Table

Row Pinning

Pin
First Name
Last Name
Age
Visits
Status
Profile Progress
Show
10
{
"rowPinning": {
"bottom": [],
"top": []
},
"rowSelection": {}
}
import {useMemo, useState} from "react"
import {ArrowDown, ArrowUp, ChevronDown, ChevronRight, X} from "lucide-react"
import {CodeHighlight} from "@qui/mdx-docs"
import {
QButton,
QCheckbox,
QIconButton,
QPagination,
QProgressCircle,
QTextInput,
} from "@qui/react"
import {
CellContext,
Column,
ColumnDef,
ExpandedState,
flexRender,
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
QTable,
QTbody,
QTd,
QTh,
QThead,
QTr,
Row,
RowPinningState,
Table,
useReactTable,
useTablePagination,
} from "@qui/react-table"
import {Person, usePersonData} from "~utils/data"
export default function RowPinning() {
// table states
const [rowPinning, setRowPinning] = useState<RowPinningState>({
bottom: [],
top: [],
})
const [expanded, setExpanded] = useState<ExpandedState>({})
// demo states
const [keepPinnedRows, setKeepPinnedRows] = useState(false)
const [includeLeafRows, setIncludeLeafRows] = useState(true)
const [includeParentRows, setIncludeParentRows] = useState(true)
const columns: ColumnDef<Person>[] = useMemo<ColumnDef<Person>[]>(
() => [
{
cell: ({row}) =>
row.getIsPinned() ? (
<QIconButton
dense
icon={X}
onClick={() => row.pin(false, includeLeafRows, includeParentRows)}
/>
) : (
<div className="flex gap-1">
<QIconButton
dense
icon={ArrowUp}
onClick={() =>
row.pin("top", includeLeafRows, includeParentRows)
}
/>
<QIconButton
dense
icon={ArrowDown}
onClick={() =>
row.pin("bottom", includeLeafRows, includeParentRows)
}
/>
</div>
),
header: () => "Pin",
id: "pin",
},
{
accessorKey: "firstName",
cell: ({getValue, row}: CellContext<Person, any>) => {
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={(event, checked) => {
row.toggleSelected(checked)
}}
/>
{row.getCanExpand() ? (
<QIconButton
icon={row.getIsExpanded() ? ChevronDown : ChevronRight}
onClick={row.getToggleExpandedHandler()}
size="s"
/>
) : null}
<span>{getValue()}</span>
</>
</div>
)
},
footer: (props) => props.column.id,
header: ({table}) => {
return (
<div className="flex items-center gap-2">
<QCheckbox
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
<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",
},
{
accessorKey: "age",
header: "Age",
size: 50,
},
{
accessorKey: "visits",
header: "Visits",
size: 50,
},
{
accessorKey: "status",
header: "Status",
},
{
accessorKey: "progress",
header: "Profile Progress",
size: 80,
},
],
[includeLeafRows, includeParentRows],
)
const {data = [], isFetching, refetch} = usePersonData(1000, 2, 2)
const refreshData = () => refetch()
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSubRows: (row) => row.subRows,
initialState: {pagination: {pageIndex: 0, pageSize: 10}},
keepPinnedRows,
onExpandedChange: setExpanded,
onRowPinningChange: setRowPinning,
state: {
expanded,
rowPinning,
},
})
const paginationProps = useTablePagination(table)
return (
<div className="flex flex-col overflow-x-auto p-2">
<div className="align-center vertical flex flex-col gap-2">
<QCheckbox
checked={includeParentRows}
label="Include Parent Rows When Pinning Child"
onChange={(e, checked) => setIncludeParentRows(checked)}
/>
<QCheckbox
checked={includeLeafRows}
label="Include Leaf Rows When Pinning Parent"
onChange={(e, checked) => setIncludeLeafRows(checked)}
/>
<QCheckbox
checked={keepPinnedRows}
label="Persist Pinned Rows across Pagination and Filtering"
onChange={(e, checked) => setKeepPinnedRows(checked)}
/>
<div className="mt-1 flex items-center gap-2">
<QButton onClick={refreshData} variant="outline">
Refresh Data
</QButton>
{isFetching ? <QProgressCircle size="xs" /> : null}
</div>
</div>
<div className="mt-3 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 min-h-[28px] items-center justify-center">
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</div>
{header.column.getCanFilter() ? (
<Filter column={header.column} table={table} />
) : null}
</div>
)}
</QTh>
)
})}
</QTr>
))}
</QThead>
<QTbody>
{table.getTopRows().map((row) => (
<PinnedRow key={row.id} row={row} table={table} />
))}
{table.getCenterRows().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>
)
})}
{table.getBottomRows().map((row) => (
<PinnedRow key={row.id} row={row} table={table} />
))}
</QTbody>
</QTable>
</div>
<div className="mt-4">
<QPagination
{...paginationProps}
renderPageMeta={(context) =>
`${context.currentPage} of ${context.totalPages}`
}
rowsPerPageLabel="Show"
rowsPerPageOptions={[10, 20, 50]}
/>
</div>
<CodeHighlight
className="mt-4 w-fit"
code={JSON.stringify(
{
rowPinning: table.getState().rowPinning,
rowSelection: table.getState().rowSelection,
},
null,
2,
)}
disableCopy
language="json"
/>
</div>
)
}
function PinnedRow({row, table}: {row: Row<Person>; table: Table<Person>}) {
return (
<QTr
className="sticky"
isSelected
style={{
bottom:
row.getIsPinned() === "bottom"
? `${
(table.getBottomRows().length - 1 - row.getPinnedIndex()) * 26
}px`
: undefined,
top:
row.getIsPinned() === "top"
? `${row.getPinnedIndex() * 26 + 48}px`
: undefined,
}}
>
{row.getVisibleCells().map((cell) => {
return (
<QTd key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</QTd>
)
})}
</QTr>
)
}
function Filter({
column,
table,
}: {
column: Column<Person>
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}
/>
)
}