Skip to content

Commit

Permalink
PORTALS-2795 - Support virtualizing TanStack tables
Browse files Browse the repository at this point in the history
- Add StyledVirtualTanStackTable component, a wrapper around StyledTanStackTable with support for @tanstack/react-virtual
- Add @tanstack/react-virtual
- Augment StyledTanStackTable, TableBody to support slots, trying to match the MUI design pattern. The use of slots reduces duplicated code that we would need in StyledVirtualTanStackTable
  • Loading branch information
nickgros committed Jan 6, 2025
1 parent 7cd2fe7 commit a4bf611
Show file tree
Hide file tree
Showing 9 changed files with 615 additions and 116 deletions.
3 changes: 2 additions & 1 deletion packages/synapse-react-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"@tanstack/query-core": "5.22.2",
"@tanstack/react-query": "5.22.2",
"@tanstack/react-query-devtools": "5.24.0",
"@tanstack/react-table": "^8.20.5",
"@tanstack/react-table": "^8.20.6",
"@tanstack/react-virtual": "^3.11.1",
"@upsetjs/react": "^1.11.0",
"animate.css": "^4.1.1",
"bootstrap": "^4.6.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,100 @@
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'

type StyledTanStackTableProps<T = unknown> = {
table: Table<T>
type StyledTanStackTableSlots<
TData = unknown,
TRowData = Row<TData>,
> = TableBodyProps<TData, TRowData>['slots'] & {
Table?: React.ElementType<
React.DetailedHTMLProps<
React.HTMLAttributes<HTMLTableElement>,
HTMLTableElement
>
>
Thead?: React.ElementType<
React.DetailedHTMLProps<
React.HTMLAttributes<HTMLTableSectionElement>,
HTMLTableSectionElement
>
>
Th?: React.ElementType<
React.DetailedHTMLProps<
React.HTMLAttributes<HTMLTableCellElement>,
HTMLTableCellElement
>
>
}

type SlotProps<TData = unknown, TRowData = Row<TData>> = TableBodyProps<
TData,
TRowData
>['slotProps'] & {
Table?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLTableElement>,
HTMLTableElement
>
Thead?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLTableSectionElement>,
HTMLTableSectionElement
>
Th?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLTableCellElement>,
HTMLTableCellElement
>
}

export type StyledTanStackTableProps<TData = unknown, TRowType = Row<TData>> = {
table: Table<TData>
styledTableContainerProps?: StyledTableContainerProps
fullWidth?: boolean
}
slots?: StyledTanStackTableSlots<TData, TRowType>
slotProps?: SlotProps<TData, TRowType>
} & 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
Expand Down Expand Up @@ -51,19 +126,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
Expand All @@ -81,14 +165,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,119 @@
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 './TableBody'

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)

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>(
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',
height: `${rowVirtualizer.getTotalSize()}px`, //tells scrollbar how big the table is
position: 'relative', //needed for absolute positioning of rows
},
},
})}
/>
</RowVirtualizerContext.Provider>
)
}
Loading

0 comments on commit a4bf611

Please sign in to comment.