R: Improve data-table internals
This commit is contained in:
parent
85faab2078
commit
ea97a7a075
|
@ -1,20 +1,13 @@
|
|||
'use client';
|
||||
'use no memo';
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
type ColumnSort,
|
||||
createColumnHelper,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
type PaginationState,
|
||||
type RowData,
|
||||
type RowSelectionState,
|
||||
type SortingState,
|
||||
type TableOptions,
|
||||
type Updater,
|
||||
useReactTable,
|
||||
type VisibilityState
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
|
@ -25,6 +18,7 @@ import { PaginationTools } from './pagination-tools';
|
|||
import { TableBody } from './table-body';
|
||||
import { TableFooter } from './table-footer';
|
||||
import { TableHeader } from './table-header';
|
||||
import { useDataTable } from './use-data-table';
|
||||
|
||||
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
|
||||
|
||||
|
@ -125,62 +119,17 @@ export function DataTable<TData extends RowData>({
|
|||
onRowDoubleClicked,
|
||||
noDataComponent,
|
||||
|
||||
enableRowSelection,
|
||||
rowSelection,
|
||||
|
||||
enableHiding,
|
||||
columnVisibility,
|
||||
|
||||
enableSorting,
|
||||
initialSorting,
|
||||
|
||||
enablePagination,
|
||||
paginationPerPage = 10,
|
||||
paginationOptions = [10, 20, 30, 40, 50],
|
||||
onChangePaginationOption,
|
||||
|
||||
...restProps
|
||||
}: DataTableProps<TData>) {
|
||||
const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []);
|
||||
const [lastSelected, setLastSelected] = useState<string | null>(null);
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: paginationPerPage
|
||||
});
|
||||
const table = useDataTable({ ...restProps });
|
||||
console.log(table);
|
||||
|
||||
const handleChangePagination = useCallback(
|
||||
(updater: Updater<PaginationState>) => {
|
||||
setPagination(prev => {
|
||||
const resolvedValue = typeof updater === 'function' ? updater(prev) : updater;
|
||||
if (onChangePaginationOption && prev.pageSize !== resolvedValue.pageSize) {
|
||||
onChangePaginationOption(resolvedValue.pageSize);
|
||||
}
|
||||
return resolvedValue;
|
||||
});
|
||||
},
|
||||
[onChangePaginationOption]
|
||||
);
|
||||
|
||||
const tableImpl = useReactTable({
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,
|
||||
getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
|
||||
|
||||
state: {
|
||||
pagination: pagination,
|
||||
sorting: sorting,
|
||||
rowSelection: rowSelection ?? {},
|
||||
columnVisibility: columnVisibility ?? {}
|
||||
},
|
||||
enableHiding: enableHiding,
|
||||
onPaginationChange: enablePagination ? handleChangePagination : undefined,
|
||||
onSortingChange: enableSorting ? setSorting : undefined,
|
||||
enableMultiRowSelection: enableRowSelection,
|
||||
...restProps
|
||||
});
|
||||
|
||||
const isEmpty = tableImpl.getRowModel().rows.length === 0;
|
||||
const isPaginationEnabled = typeof table.getCanNextPage === 'function';
|
||||
const isEmpty = table.getRowModel().rows.length === 0;
|
||||
|
||||
const fixedSize = useMemo(() => {
|
||||
if (!rows) {
|
||||
|
@ -194,47 +143,40 @@ export function DataTable<TData extends RowData>({
|
|||
}, [rows, dense, noHeader, contentHeight]);
|
||||
|
||||
const columnSizeVars = useMemo(() => {
|
||||
const headers = tableImpl.getFlatHeaders();
|
||||
const headers = table.getFlatHeaders();
|
||||
const colSizes: Record<string, number> = {};
|
||||
for (const header of headers) {
|
||||
colSizes[`--header-${header.id}-size`] = header.getSize();
|
||||
colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
|
||||
}
|
||||
return colSizes;
|
||||
}, [tableImpl]);
|
||||
}, [table]);
|
||||
|
||||
return (
|
||||
<div tabIndex={-1} id={id} className={className} style={{ minHeight: fixedSize, maxHeight: fixedSize, ...style }}>
|
||||
<table className='w-full' style={{ ...columnSizeVars }}>
|
||||
{!noHeader ? (
|
||||
<TableHeader
|
||||
table={tableImpl}
|
||||
enableRowSelection={enableRowSelection}
|
||||
enableSorting={enableSorting}
|
||||
headPosition={headPosition}
|
||||
resetLastSelected={() => setLastSelected(null)}
|
||||
/>
|
||||
<TableHeader table={table} headPosition={headPosition} resetLastSelected={() => setLastSelected(null)} />
|
||||
) : null}
|
||||
|
||||
<TableBody
|
||||
table={tableImpl}
|
||||
table={table}
|
||||
dense={dense}
|
||||
noHeader={noHeader}
|
||||
conditionalRowStyles={conditionalRowStyles}
|
||||
enableRowSelection={enableRowSelection}
|
||||
lastSelected={lastSelected}
|
||||
onChangeLastSelected={setLastSelected}
|
||||
onRowClicked={onRowClicked}
|
||||
onRowDoubleClicked={onRowDoubleClicked}
|
||||
/>
|
||||
|
||||
{!noFooter ? <TableFooter table={tableImpl} /> : null}
|
||||
{!noFooter ? <TableFooter table={table} /> : null}
|
||||
</table>
|
||||
|
||||
{enablePagination && !isEmpty ? (
|
||||
{isPaginationEnabled && !isEmpty ? (
|
||||
<PaginationTools
|
||||
id={id ? `${id}__pagination` : undefined}
|
||||
table={tableImpl}
|
||||
table={table}
|
||||
paginationOptions={paginationOptions}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
'use client';
|
||||
'use no memo';
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { type Table } from '@tanstack/react-table';
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
'use no memo';
|
||||
|
||||
import { type Column } from '@tanstack/react-table';
|
||||
|
||||
import { IconSortAsc, IconSortDesc } from '../icons';
|
||||
|
||||
interface SortingIconProps<TData> {
|
||||
column: Column<TData>;
|
||||
interface SortingIconProps {
|
||||
sortDirection?: 'asc' | 'desc' | false;
|
||||
}
|
||||
|
||||
export function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
|
||||
return (
|
||||
<>
|
||||
{{
|
||||
desc: <IconSortDesc size='1rem' />,
|
||||
asc: <IconSortAsc size='1rem' />
|
||||
}[column.getIsSorted() as string] ?? <IconSortDesc size='1rem' className='opacity-0 group-hover:opacity-25' />}
|
||||
</>
|
||||
);
|
||||
export function SortingIcon({ sortDirection }: SortingIconProps) {
|
||||
if (sortDirection === 'asc') {
|
||||
return <IconSortAsc size='1rem' />;
|
||||
}
|
||||
if (sortDirection === 'desc') {
|
||||
return <IconSortDesc size='1rem' />;
|
||||
}
|
||||
return <IconSortDesc size='1rem' className='opacity-0 group-hover:opacity-25' />;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use no memo';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { type Cell, flexRender, type Row, type Table } from '@tanstack/react-table';
|
||||
import clsx from 'clsx';
|
||||
|
||||
|
@ -10,7 +11,6 @@ interface TableBodyProps<TData> {
|
|||
table: Table<TData>;
|
||||
dense?: boolean;
|
||||
noHeader?: boolean;
|
||||
enableRowSelection?: boolean;
|
||||
conditionalRowStyles?: IConditionalStyle<TData>[];
|
||||
|
||||
lastSelected: string | null;
|
||||
|
@ -24,16 +24,16 @@ export function TableBody<TData>({
|
|||
table,
|
||||
dense,
|
||||
noHeader,
|
||||
enableRowSelection,
|
||||
conditionalRowStyles,
|
||||
lastSelected,
|
||||
onChangeLastSelected,
|
||||
onRowClicked,
|
||||
onRowDoubleClicked
|
||||
}: TableBodyProps<TData>) {
|
||||
function handleRowClicked(target: Row<TData>, event: React.MouseEvent<Element>) {
|
||||
const handleRowClicked = useCallback(
|
||||
(target: Row<TData>, event: React.MouseEvent<Element>) => {
|
||||
onRowClicked?.(target.original, event);
|
||||
if (enableRowSelection && target.getCanSelect()) {
|
||||
if (target.getCanSelect()) {
|
||||
if (event.shiftKey && !!lastSelected && lastSelected !== target.id) {
|
||||
const { rows, rowsById } = table.getRowModel();
|
||||
const lastIndex = rowsById[lastSelected].index;
|
||||
|
@ -53,15 +53,20 @@ export function TableBody<TData>({
|
|||
target.toggleSelected(!target.getIsSelected());
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[table, lastSelected, onChangeLastSelected, onRowClicked]
|
||||
);
|
||||
|
||||
function getRowStyles(row: Row<TData>) {
|
||||
const getRowStyles = useCallback(
|
||||
(row: Row<TData>) => {
|
||||
return {
|
||||
...conditionalRowStyles!
|
||||
.filter(item => item.when(row.original))
|
||||
.reduce((prev, item) => ({ ...prev, ...item.style }), {})
|
||||
};
|
||||
}
|
||||
},
|
||||
[conditionalRowStyles]
|
||||
);
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
|
@ -72,11 +77,15 @@ export function TableBody<TData>({
|
|||
'cc-scroll-row',
|
||||
'clr-hover cc-animate-color',
|
||||
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
|
||||
row.getIsSelected() ? 'clr-selected' : 'odd:bg-prim-200 even:bg-prim-100'
|
||||
table.options.enableRowSelection && row.getIsSelected()
|
||||
? 'clr-selected'
|
||||
: 'odd:bg-prim-200 even:bg-prim-100'
|
||||
)}
|
||||
style={{ ...(conditionalRowStyles ? getRowStyles(row) : []) }}
|
||||
onClick={event => handleRowClicked(row, event)}
|
||||
onDoubleClick={event => onRowDoubleClicked?.(row.original, event)}
|
||||
>
|
||||
{enableRowSelection ? (
|
||||
{table.options.enableRowSelection ? (
|
||||
<td key={`select-${row.id}`} className='pl-2 border-y'>
|
||||
<SelectRow row={row} onChangeLastSelected={onChangeLastSelected} />
|
||||
</td>
|
||||
|
@ -91,8 +100,6 @@ export function TableBody<TData>({
|
|||
paddingTop: dense ? '0.25rem' : '0.5rem',
|
||||
width: noHeader && index === 0 ? `calc(var(--col-${cell.column.id}-size) * 1px)` : 'auto'
|
||||
}}
|
||||
onClick={event => handleRowClicked(row, event)}
|
||||
onDoubleClick={event => onRowDoubleClicked?.(row.original, event)}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
|
|
|
@ -8,18 +8,10 @@ import { SortingIcon } from './sorting-icon';
|
|||
interface TableHeaderProps<TData> {
|
||||
table: Table<TData>;
|
||||
headPosition?: string;
|
||||
enableRowSelection?: boolean;
|
||||
enableSorting?: boolean;
|
||||
resetLastSelected: () => void;
|
||||
}
|
||||
|
||||
export function TableHeader<TData>({
|
||||
table,
|
||||
headPosition,
|
||||
enableRowSelection,
|
||||
enableSorting,
|
||||
resetLastSelected
|
||||
}: TableHeaderProps<TData>) {
|
||||
export function TableHeader<TData>({ table, headPosition, resetLastSelected }: TableHeaderProps<TData>) {
|
||||
return (
|
||||
<thead
|
||||
className='bg-prim-100 cc-shadow-border'
|
||||
|
@ -30,7 +22,7 @@ export function TableHeader<TData>({
|
|||
>
|
||||
{table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{enableRowSelection ? (
|
||||
{table.options.enableRowSelection ? (
|
||||
<th className='pl-2' scope='col'>
|
||||
<SelectAll table={table} resetLastSelected={resetLastSelected} />
|
||||
</th>
|
||||
|
@ -43,14 +35,16 @@ export function TableHeader<TData>({
|
|||
className='cc-table-header group'
|
||||
style={{
|
||||
width: `calc(var(--header-${header?.id}-size) * 1px)`,
|
||||
cursor: enableSorting && header.column.getCanSort() ? 'pointer' : 'auto'
|
||||
cursor: table.options.enableSorting && header.column.getCanSort() ? 'pointer' : 'auto'
|
||||
}}
|
||||
onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined}
|
||||
onClick={table.options.enableSorting ? header.column.getToggleSortingHandler() : undefined}
|
||||
>
|
||||
{!header.isPlaceholder ? (
|
||||
<span className='inline-flex gap-1'>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{enableSorting && header.column.getCanSort() ? <SortingIcon column={header.column} /> : null}
|
||||
{table.options.enableSorting && header.column.getCanSort() ? (
|
||||
<SortingIcon sortDirection={header.column.getIsSorted()} />
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
</th>
|
||||
|
|
125
rsconcept/frontend/src/components/data-table/use-data-table.ts
Normal file
125
rsconcept/frontend/src/components/data-table/use-data-table.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import {
|
||||
type ColumnSort,
|
||||
createColumnHelper,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
type PaginationState,
|
||||
type RowData,
|
||||
type RowSelectionState,
|
||||
type SortingState,
|
||||
type TableOptions,
|
||||
type Updater,
|
||||
useReactTable,
|
||||
type VisibilityState
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
|
||||
|
||||
/** Style to conditionally apply to rows. */
|
||||
export interface IConditionalStyle<TData> {
|
||||
/** Callback to determine if the style should be applied. */
|
||||
when: (rowData: TData) => boolean;
|
||||
|
||||
/** Style to apply. */
|
||||
style: React.CSSProperties;
|
||||
}
|
||||
|
||||
export interface UseDataTableProps<TData extends RowData>
|
||||
extends Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
|
||||
/** Enable row selection. */
|
||||
enableRowSelection?: boolean;
|
||||
|
||||
/** Current row selection. */
|
||||
rowSelection?: RowSelectionState;
|
||||
|
||||
/** Enable hiding of columns. */
|
||||
enableHiding?: boolean;
|
||||
|
||||
/** Current column visibility. */
|
||||
columnVisibility?: VisibilityState;
|
||||
|
||||
/** Enable pagination. */
|
||||
enablePagination?: boolean;
|
||||
|
||||
/** Number of rows per page. */
|
||||
paginationPerPage?: number;
|
||||
|
||||
/** Callback to be called when the pagination option is changed. */
|
||||
onChangePaginationOption?: (newValue: number) => void;
|
||||
|
||||
/** Enable sorting. */
|
||||
enableSorting?: boolean;
|
||||
|
||||
/** Initial sorting. */
|
||||
initialSorting?: ColumnSort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dta representation as a table.
|
||||
*
|
||||
* @param headPosition - Top position of sticky header (0 if no other sticky elements are present).
|
||||
* No sticky header if omitted
|
||||
*/
|
||||
export function useDataTable<TData extends RowData>({
|
||||
enableRowSelection,
|
||||
rowSelection,
|
||||
|
||||
enableHiding,
|
||||
columnVisibility,
|
||||
|
||||
enableSorting,
|
||||
initialSorting,
|
||||
|
||||
enablePagination,
|
||||
paginationPerPage = 10,
|
||||
onChangePaginationOption,
|
||||
|
||||
...restProps
|
||||
}: UseDataTableProps<TData>) {
|
||||
const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: paginationPerPage
|
||||
});
|
||||
|
||||
const handleChangePagination = useCallback(
|
||||
(updater: Updater<PaginationState>) => {
|
||||
setPagination(prev => {
|
||||
const resolvedValue = typeof updater === 'function' ? updater(prev) : updater;
|
||||
if (onChangePaginationOption && prev.pageSize !== resolvedValue.pageSize) {
|
||||
onChangePaginationOption(resolvedValue.pageSize);
|
||||
}
|
||||
return resolvedValue;
|
||||
});
|
||||
},
|
||||
[onChangePaginationOption]
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
state: {
|
||||
pagination: pagination,
|
||||
sorting: sorting,
|
||||
rowSelection: rowSelection,
|
||||
columnVisibility: columnVisibility
|
||||
},
|
||||
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
|
||||
enableSorting: enableSorting,
|
||||
getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,
|
||||
onSortingChange: enableSorting ? setSorting : undefined,
|
||||
|
||||
getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
|
||||
onPaginationChange: enablePagination ? handleChangePagination : undefined,
|
||||
|
||||
enableHiding: enableHiding,
|
||||
enableMultiRowSelection: enableRowSelection,
|
||||
enableRowSelection: enableRowSelection,
|
||||
...restProps
|
||||
});
|
||||
return table;
|
||||
}
|
Loading…
Reference in New Issue
Block a user