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 normallyconst [value, setValue] = useState(initialValue)// When the input is blurred, we'll call our table meta's updateData functionconst onBlur = () => {table.options.meta?.updateData(index, id, value)}// If the initialValue is changed external, sync it up with our stateuseEffect(() => {setValue(initialValue)}, [initialValue])return (<QTextInputclearable={false}fullWidthonBlur={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 temporarilyconst 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 metameta: {updateData: (rowIndex: number, columnId: string, value: unknown) => {// Skip page index reset until after next rerenderskipAutoResetPageIndex()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"><QTextInputclassName="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] ?? ""}/><QTextInputclassName="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>) : (<QTextInputclassName="w-36"onChange={(e, value) => column.setFilterValue(value)}placeholder="Search..."size="s"value={(columnFilterValue ?? "") as string}/>)}