-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PORTALS-2795 - Support virtualizing TanStack tables #1485
Changes from all commits
a4bf611
607469f
86aba73
d26cf69
b28f309
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,59 @@ | ||
import { flexRender, Row, Table } from '@tanstack/react-table' | ||
import { useMemo } from 'react' | ||
import { flexRender, Table } from '@tanstack/react-table' | ||
import { | ||
StyledTableContainer, | ||
StyledTableContainerProps, | ||
} from '../styled/StyledTableContainer' | ||
import { MemoizedTableBody, TableBody } from './TableBody' | ||
import { MemoizedTableBody, TableBody, TableBodyProps } from './TableBody' | ||
import { | ||
getColumnSizeCssVariable, | ||
getHeaderSizeCssVariable, | ||
} from './TanStackTableUtils' | ||
import { StyledTanStackTableSlotProps, StyledTanStackTableSlots } from './types' | ||
|
||
type StyledTanStackTableProps<T = unknown> = { | ||
table: Table<T> | ||
export type StyledTanStackTableProps<TData = unknown, TRowType = Row<TData>> = { | ||
table: Table<TData> | ||
styledTableContainerProps?: StyledTableContainerProps | ||
fullWidth?: boolean | ||
} | ||
slots?: StyledTanStackTableSlots<TData, TRowType> | ||
slotProps?: StyledTanStackTableSlotProps<TData, TRowType> | ||
Comment on lines
+18
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In an effort to reduce duplicate code, I added There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is my first exposure to the slots design pattern, and I can see the utility. I think it was a good call to copy this pattern in this implementation |
||
} & Pick<TableBodyProps<TData, TRowType>, 'rows' | 'rowTransform'> | ||
|
||
/** | ||
* Component that renders a styled table using @tanstack/react-table. Pass your table instance into the component and | ||
* it will render the table with the appropriate headers and rows. | ||
*/ | ||
export default function StyledTanStackTable< | ||
TData = unknown, | ||
TRowType = Row<TData>, | ||
>(props: StyledTanStackTableProps<TData, TRowType>) { | ||
const { | ||
table, | ||
styledTableContainerProps, | ||
fullWidth = true, | ||
slots = {}, | ||
slotProps = {}, | ||
} = props | ||
const { | ||
Thead = 'thead', | ||
Th = 'th', | ||
Table = 'table', | ||
...tableBodySlots | ||
} = slots | ||
const { | ||
Table: tableSlotProps = {}, | ||
Thead: theadSlotProps = {}, | ||
Th: thSlotProps = {}, | ||
...tableBodySlotProps | ||
} = slotProps | ||
|
||
export default function StyledTanStackTable<T = unknown>( | ||
props: StyledTanStackTableProps<T>, | ||
) { | ||
const { table, styledTableContainerProps, fullWidth = true } = props | ||
const tableBodyProps: TableBodyProps<TData, TRowType> = { | ||
table, | ||
slots: tableBodySlots, | ||
slotProps: tableBodySlotProps, | ||
rows: props.rows!, | ||
rowTransform: props.rowTransform!, | ||
} | ||
|
||
/** | ||
* Instead of calling `column.getSize()` on every render for every header | ||
|
@@ -51,19 +85,28 @@ export default function StyledTanStackTable<T = unknown>( | |
|
||
return ( | ||
<StyledTableContainer {...styledTableContainerProps}> | ||
<table style={{ ...columnSizeVars, width: tableWidth }}> | ||
<thead> | ||
<Table | ||
{...tableSlotProps} | ||
style={{ | ||
...columnSizeVars, | ||
width: tableWidth, | ||
...tableSlotProps['style'], | ||
}} | ||
> | ||
<Thead {...theadSlotProps}> | ||
{table.getHeaderGroups().map(headerGroup => { | ||
return ( | ||
<tr key={headerGroup.id}> | ||
{headerGroup.headers.map(header => ( | ||
<th | ||
<Th | ||
key={header.id} | ||
colSpan={header.colSpan} | ||
{...thSlotProps} | ||
style={{ | ||
width: `calc(var(${getHeaderSizeCssVariable( | ||
header.id, | ||
)}) * 1px)`, | ||
...thSlotProps['style'], | ||
}} | ||
> | ||
{header.isPlaceholder | ||
|
@@ -81,14 +124,14 @@ export default function StyledTanStackTable<T = unknown>( | |
onTouchStart={header.getResizeHandler()} | ||
/> | ||
)} | ||
</th> | ||
</Th> | ||
))} | ||
</tr> | ||
) | ||
})} | ||
</thead> | ||
<TableBodyElement table={table} /> | ||
</table> | ||
</Thead> | ||
<TableBodyElement<TData, TRowType> {...tableBodyProps} /> | ||
</Table> | ||
</StyledTableContainer> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { VirtualItem, Virtualizer } from '@tanstack/react-virtual' | ||
import { noop } from 'lodash-es' | ||
import React, { useContext } from 'react' | ||
import { mergeSlotProps } from '../../utils/slots/SlotUtils' | ||
import StyledTanStackTable, { | ||
StyledTanStackTableProps, | ||
} from './StyledTanStackTable' | ||
|
||
import { TrProps } from './types' | ||
|
||
type StyledVirtualTanStackTableProps<T = unknown> = Omit< | ||
StyledTanStackTableProps<T, VirtualItem>, | ||
'slots' | 'rows' | 'rowTransform' | ||
> & { | ||
rowVirtualizer: Virtualizer<any, any> | ||
onTableContainerScroll?: (target: EventTarget) => void | ||
} | ||
|
||
// Context to pass the row virtualizer to the Tr component | ||
const RowVirtualizerContext = React.createContext< | ||
Virtualizer<any, any> | undefined | ||
>(undefined) | ||
|
||
/** | ||
* A table row component modified to be used with @tanstack/react-virtual. | ||
*/ | ||
export function VirtualizedTr<TData = unknown>( | ||
props: TrProps<TData, VirtualItem>, | ||
) { | ||
const { row: virtualItem, tableRow: _tableRow, ...rest } = props | ||
const rowVirtualizer = useContext(RowVirtualizerContext) | ||
return ( | ||
<tr | ||
data-index={virtualItem.index} // needed for dynamic row height measurement | ||
ref={node => rowVirtualizer?.measureElement(node)} //measure dynamic row height | ||
{...rest} | ||
style={{ | ||
...rest.style, | ||
display: 'flex', | ||
position: 'absolute', | ||
transform: `translateY(${virtualItem.start}px)`, //this should always be a `style` as it changes on scroll | ||
width: '100%', | ||
}} | ||
/> | ||
) | ||
} | ||
|
||
/** | ||
* Stylized table component that is compatible with @tanstack/react-table and @tanstack/react-virtual to display a | ||
* virtualized table, i.e. a table that uses a virtualizer to only render the DOM nodes that contain data that is visible | ||
* within the viewport. For cases where the table rows do not need to be virtualized, use {@link StyledTanStackTable}. | ||
*/ | ||
export default function StyledVirtualTanStackTable<T = unknown>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New component that adds the styles / component overrides needed for virtualization to work |
||
props: StyledVirtualTanStackTableProps<T>, | ||
) { | ||
const { | ||
table, | ||
styledTableContainerProps, | ||
fullWidth = true, | ||
rowVirtualizer, | ||
slotProps = {}, | ||
onTableContainerScroll = noop, | ||
} = props | ||
|
||
const virtualRows = rowVirtualizer.getVirtualItems() | ||
|
||
return ( | ||
<RowVirtualizerContext.Provider value={rowVirtualizer}> | ||
<StyledTanStackTable<T, VirtualItem> | ||
table={table} | ||
rows={virtualRows} | ||
rowTransform={row => table.getRowModel().rows[row.index]} | ||
fullWidth={fullWidth} | ||
styledTableContainerProps={{ | ||
...styledTableContainerProps, | ||
style: { | ||
overflow: 'auto', //our scrollable table container | ||
position: 'relative', //needed for sticky header | ||
...styledTableContainerProps?.style, | ||
}, | ||
sx: { | ||
'thead > tr': { | ||
display: 'flex', | ||
width: '100%', | ||
}, | ||
}, | ||
onScroll: e => onTableContainerScroll(e.target), | ||
}} | ||
{...styledTableContainerProps} | ||
slots={{ | ||
Tr: VirtualizedTr, | ||
}} | ||
slotProps={mergeSlotProps(slotProps, { | ||
Table: { | ||
style: { | ||
display: 'grid', | ||
}, | ||
}, | ||
Thead: { | ||
style: { | ||
display: 'grid', | ||
position: 'sticky', | ||
top: 0, | ||
zIndex: 1, | ||
}, | ||
}, | ||
Th: { | ||
style: { | ||
display: 'flex', | ||
}, | ||
}, | ||
Tbody: { | ||
style: { | ||
display: 'grid', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Uses a |
||
height: `${rowVirtualizer.getTotalSize()}px`, //tells scrollbar how big the table is | ||
position: 'relative', //needed for absolute positioning of rows | ||
}, | ||
}, | ||
})} | ||
/> | ||
</RowVirtualizerContext.Provider> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,74 @@ | ||
import { styled } from '@mui/material' | ||
import { Row, Table } from '@tanstack/react-table' | ||
import { identity } from 'lodash-es' | ||
import { memo } from 'react' | ||
import { Cell, flexRender, Table } from '@tanstack/react-table' | ||
import { getColumnSizeCssVariable } from './TanStackTableUtils' | ||
import ExpandableTableDataCell from '../SynapseTable/ExpandableTableDataCell' | ||
import { Skeleton } from '@mui/material' | ||
|
||
function CellRenderer<T = unknown>(cell: Cell<T, unknown>) { | ||
const getWrapInExpandableTd = | ||
cell.getContext().table.options.meta?.getWrapInExpandableTd | ||
const wrapInExpandableTd = | ||
getWrapInExpandableTd && getWrapInExpandableTd(cell) | ||
const TableDataCellElement = wrapInExpandableTd | ||
? ExpandableTableDataCell | ||
: 'td' | ||
|
||
const renderPlaceholderData = | ||
cell.getContext().table.options.meta?.renderPlaceholderData | ||
import { getSlotProps } from '../../utils/slots/SlotUtils' | ||
import { TableCellRenderer as DefaultTableCellRenderer } from './TableCellRenderer' | ||
import { | ||
TableBodyPropsRowOverride, | ||
TableBodySlotProps, | ||
TableBodySlots, | ||
TrOwnerState, | ||
} from './types' | ||
|
||
return ( | ||
<TableDataCellElement | ||
key={cell.id} | ||
style={{ | ||
width: `calc(var(${getColumnSizeCssVariable(cell.column.id)}) * 1px)`, | ||
textAlign: cell.column.columnDef.meta?.textAlign, | ||
}} | ||
> | ||
{renderPlaceholderData ? ( | ||
<p> | ||
<Skeleton width={'80%'} height={'20px'} /> | ||
</p> | ||
) : ( | ||
flexRender(cell.column.columnDef.cell, cell.getContext()) | ||
)} | ||
</TableDataCellElement> | ||
) | ||
} | ||
// Simple wrapper around 'tr' to prevent forwarding invalid HTML attributes | ||
const TableRow = styled('tr', { | ||
shouldForwardProp: prop => prop !== 'row' && prop !== 'tableRow', | ||
})({}) | ||
|
||
type TableBodyProps<T = unknown> = { | ||
table: Table<T> | ||
} | ||
export type TableBodyProps<TData = unknown, TRowType = Row<TData>> = { | ||
/** The table instance */ | ||
table: Table<TData> | ||
slots?: TableBodySlots<TData, TRowType> | ||
slotProps?: TableBodySlotProps<TData, TRowType> | ||
Comment on lines
+22
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add slots |
||
} & TableBodyPropsRowOverride<TData, TRowType> | ||
|
||
/** | ||
* A table body component for use with @tanstack/react-table. This component renders the rows of the table. | ||
* @param props | ||
* @constructor | ||
*/ | ||
export function TableBody<TData = unknown, TRowType = Row<TData>>( | ||
props: TableBodyProps<TData, TRowType>, | ||
) { | ||
const { table, slots = {}, slotProps = {} } = props | ||
|
||
// By default, use TanStack Table Rows and the identity function as a transform. | ||
// This can be overridden e.g. to accomplish row virtualization. | ||
const { | ||
rows = table.getRowModel().rows as TRowType[], | ||
rowTransform = identity, | ||
} = props | ||
Comment on lines
+36
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For instance, when we create a virtual table, |
||
|
||
const { | ||
Tbody = 'tbody', | ||
Tr = TableRow, | ||
TableCellRenderer = DefaultTableCellRenderer<TData>, | ||
} = slots | ||
const { Tbody: tbodySlotProps = {}, Tr: _trSlotProps = {} } = slotProps | ||
|
||
export function TableBody<T = unknown>(props: TableBodyProps<T>) { | ||
const { table } = props | ||
return ( | ||
<tbody> | ||
{table.getRowModel().rows.map(row => ( | ||
<tr key={row.id}>{row.getVisibleCells().map(CellRenderer)}</tr> | ||
))} | ||
</tbody> | ||
<Tbody {...tbodySlotProps}> | ||
{rows.map((row, index) => { | ||
const tableRow: Row<TData> | undefined = rowTransform(row) | ||
|
||
const trOwnerState: TrOwnerState<TData, TRowType> = { row, tableRow } | ||
const trSlotProps = getSlotProps(_trSlotProps, trOwnerState) | ||
|
||
return ( | ||
<Tr | ||
key={tableRow?.id ?? index} | ||
{...trSlotProps} | ||
row={row} | ||
tableRow={tableRow} | ||
> | ||
{tableRow?.getVisibleCells().map((props, index) => ( | ||
<TableCellRenderer key={index} {...props} /> | ||
))} | ||
</Tr> | ||
) | ||
})} | ||
</Tbody> | ||
) | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tanstack/react-virtual is very small, and was easy to figure out how to get it to work with @tanstack/react-table compared to react-window, since examples exist in the react-table docs.
We're also removing another virtualization library embedded in react-base-table, so while I don't want to have multiple dependencies that do the same thing, I think this is progress.