QUI React Table

Editable Data

Name
Info
First Name
Last Name
Age
Visits
Status
Profile Progress
Show
10
import {useCallback, useEffect, useMemo, useRef, useState} from "react"
import {QButton, QPagination, QProgressCircle, QTextInput} from "@qui/react"
import {
CellContext,
Column,
ColumnDef,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
QTable,
QTbody,
QTd,
QTh,
QThead,
QTr,
RowData,
Table,
useReactTable,
useTablePagination,
} from "@qui/react-table"
import {Person, usePersonData} from "~utils/data"
// Extend the row meta to provide a TypeScript type for our custom function.
declare module "@qui/react-table" {
interface TableMeta<_TData extends RowData> {
updateData: (rowIndex: number, columnId: string, value: unknown) => void
}
}
function Cell({
column: {id},
getValue,
row: {index},
table,
}: CellContext<Person, unknown>) {
const initialValue = getValue()
// We need to keep and update the state of the cell normally
const [value, setValue] = useState(initialValue)
// When the input is blurred, we'll call our table meta's updateData function
const onBlur = () => {
table.options.meta?.updateData(index, id, value)
}
// If the initialValue is changed external, sync it up with our state
useEffect(() => {
setValue(initialValue)
}, [initialValue])
return (
<QTextInput
clearable={false}
fullWidth
onBlur={onBlur}
onChange={(e, value) => setValue(value)}
size="s"
value={value as string}
/>
)
}
// Give our default column cell renderer editing superpowers!
const defaultColumn: Partial<ColumnDef<Person>> = {
cell: Cell,
}
function useSkipper() {
const shouldSkipRef = useRef(true)
const shouldSkip = shouldSkipRef.current
// Wrap a function with this to skip a pagination reset temporarily
const skip = useCallback(() => {
shouldSkipRef.current = false
}, [])
useEffect(() => {
shouldSkipRef.current = true
})
return [shouldSkip, skip] as const
}
export default function EditableData() {
const columns = useMemo<ColumnDef<Person>[]>(
() => [
{
columns: [
{
accessorKey: "firstName",
header: "First Name",
},
{
accessorFn: (row) => row.lastName,
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",
},
],
[],
)
const {data: personData = [], isFetching, refetch} = usePersonData(1000)
const [mutableData, setMutableData] = useState<Person[]>(personData)
useEffect(() => {
setMutableData(personData)
}, [personData])
const refreshData = () => refetch()
const [autoResetPageIndex, skipAutoResetPageIndex] = useSkipper()
const table = useReactTable({
autoResetPageIndex,
columns,
data: mutableData,
defaultColumn,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
// Provide our updateData function to our table meta
meta: {
updateData: (rowIndex: number, columnId: string, value: unknown) => {
// Skip page index reset until after next rerender
skipAutoResetPageIndex()
setMutableData((old) =>
old.map((row, index) => {
if (index === rowIndex) {
return {
...old[rowIndex]!,
[columnId]: value,
}
}
return row
}),
)
},
},
})
const paginationProps = useTablePagination(table)
return (
<div className="overflow-x-auto p-2">
<div className="mb-4 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">
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{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}>
{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
{...paginationProps}
renderPageMeta={(context) =>
`${context.currentPage} of ${context.totalPages}`
}
rowsPerPageLabel="Show"
rowsPerPageOptions={[10, 20, 50]}
/>
</div>
</div>
)
}
function Filter({
column,
table,
}: {
column: Column<any, any>
table: Table<any>
}) {
const firstValue = table
.getPreFilteredRowModel()
.flatRows[0]?.getValue(column.id)
const columnFilterValue = column.getFilterValue()
return typeof firstValue === "number" ? (
<div className="flex space-x-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}
/>
)
}