<script setup lang="ts">
import { h, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
import {
FlexRender,
columnResizingFeature,
columnSizingFeature,
createColumnHelper,
tableFeatures,
useTable,
} from '@tanstack/vue-table'
import { makeData } from './makeData'
import type { FunctionalComponent } from 'vue'
import type { Header } from '@tanstack/vue-table'
import type { Person } from './makeData'
const features = tableFeatures({ columnResizingFeature, columnSizingFeature })
const columnHelper = createColumnHelper<typeof features, Person>()
const columns = columnHelper.columns([
columnHelper.group({
header: 'Name',
footer: (props) => props.column.id,
columns: columnHelper.columns([
columnHelper.accessor('firstName', {
cell: (info) => info.getValue(),
footer: (props) => props.column.id,
}),
columnHelper.accessor((row) => row.lastName, {
id: 'lastName',
cell: (info) => info.getValue(),
header: () => 'Last Name',
footer: (props) => props.column.id,
}),
]),
}),
columnHelper.group({
header: 'Info',
footer: (props) => props.column.id,
columns: columnHelper.columns([
columnHelper.accessor('age', {
header: () => 'Age',
footer: (props) => props.column.id,
}),
columnHelper.accessor('visits', {
header: () => 'Visits',
footer: (props) => props.column.id,
}),
columnHelper.accessor('status', {
header: 'Status',
footer: (props) => props.column.id,
}),
columnHelper.accessor('progress', {
header: 'Profile Progress',
footer: (props) => props.column.id,
}),
]),
}),
])
const data = ref(makeData(200))
const refreshData = () => {
data.value = makeData(200)
}
const stressTest = () => {
data.value = makeData(5_000)
}
const table = useTable({
features,
columns,
data,
defaultColumn: {
minSize: 60,
maxSize: 800,
},
columnResizeMode: 'onChange',
debugTable: true,
debugHeaders: true,
debugColumns: true,
})
const tableEl = useTemplateRef<HTMLTableElement>('tableEl')
/**
* Instead of re-rendering Vue on every resize tick, we subscribe to the
* table store OUTSIDE of Vue and write the column size CSS variables
* directly onto the <table> element. Header and data cells reference the
* variables, so the browser updates widths with zero Vue work per tick.
* (The core resize handler already coalesces pointer events to one state
* update per animation frame.)
*/
onMounted(() => {
const writeColumnSizeVars = () => {
const el = tableEl.value
if (!el) return
for (const header of table.getFlatHeaders()) {
el.style.setProperty(
`--header-${header.id}-size`,
String(header.getSize()),
)
el.style.setProperty(
`--col-${header.column.id}-size`,
String(header.column.getSize()),
)
}
el.style.width = `${table.getTotalSize()}px`
}
writeColumnSizeVars()
const { unsubscribe } =
table.atoms.columnSizing.subscribe(writeColumnSizeVars)
onUnmounted(unsubscribe)
})
const StateDump: FunctionalComponent = () =>
h(
'pre',
{ style: { height: '10rem', overflow: 'auto' } },
JSON.stringify(table.store.get(), null, 2),
)
// Each resizer island tracks only its own column's resizing state
const Resizer: FunctionalComponent<{
header: Header<typeof features, Person, unknown>
}> = ({ header }) =>
h('div', {
class: ['resizer', { isResizing: header.column.getIsResizing() }],
onDblclick: () => header.column.resetSize(),
onMousedown: header.getResizeHandler(),
onTouchstart: header.getResizeHandler(),
})
</script>
<template>
<div class="demo-root">
<div class="button-row">
<button class="demo-button" @click="refreshData">Regenerate Data</button>
<button class="demo-button" @click="stressTest">
Stress Test (5k rows)
</button>
</div>
<div class="spacer-md" />
<StateDump />
<div class="spacer-md" />
({{ data.length.toLocaleString() }} rows)
<div class="scroll-container">
<table ref="tableEl" style="display: grid">
<thead style="display: grid">
<tr
v-for="headerGroup in table.getHeaderGroups()"
:key="headerGroup.id"
style="display: flex; width: 100%; height: 30px"
>
<th
v-for="header in headerGroup.headers"
:key="header.id"
:colspan="header.colSpan"
:style="{
display: 'flex',
flexShrink: 0,
width: `calc(var(--header-${header.id}-size) * 1px)`,
}"
>
<FlexRender v-if="!header.isPlaceholder" :header="header" />
<Resizer :header="header" />
</th>
</tr>
</thead>
<tbody style="display: grid">
<tr
v-for="row in table.getRowModel().rows"
:key="row.id"
style="
display: flex;
width: 100%;
height: 30px;
content-visibility: auto;
contain-intrinsic-height: auto 30px;
"
>
<td
v-for="cell in row.getAllCells()"
:key="cell.id"
:style="{
display: 'flex',
flexShrink: 0,
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
}"
>
{{ cell.renderValue() }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>