<script setup lang="ts">
import {
FlexRender,
aggregationFeature,
aggregationFn_count,
aggregationFn_extent,
aggregationFn_mean,
aggregationFn_sum,
columnFilteringFeature,
createColumnHelper,
createFilteredRowModel,
createPaginatedRowModel,
filterFn_includesString,
metaHelper,
rowPaginationFeature,
rowSelectionFeature,
tableFeatures,
useTable,
} from '@tanstack/vue-table'
import { ref } from 'vue'
import { makeData } from './makeData'
import type { Sale } from './makeData'
import type { Table } from '@tanstack/vue-table'
type RowSource = 'all' | 'custom' | 'filtered' | 'page' | 'selected'
type AggregationTableMeta = { rowSource: RowSource }
const features = tableFeatures({
aggregationFeature,
columnFilteringFeature,
rowPaginationFeature,
rowSelectionFeature,
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
filterFns: { includesString: filterFn_includesString },
aggregationFns: {
count: aggregationFn_count,
extent: aggregationFn_extent,
mean: aggregationFn_mean,
sum: aggregationFn_sum,
},
tableMeta: metaHelper<AggregationTableMeta>(),
})
const columnHelper = createColumnHelper<typeof features, Sale>()
function getAggregationRows(table: Table<typeof features, Sale>) {
const source = table.options.meta?.rowSource
if (source === 'all') return table.getCoreRowModel().rows
if (source === 'page') return table.getRowModel().rows
if (source === 'selected') return table.getFilteredSelectedRowModel().rows
if (source === 'custom') return table.getCoreRowModel().rows.slice(0, 3)
return undefined
}
function formatValue(value: unknown): string {
if (Array.isArray(value)) return value.map(formatValue).join(' – ')
if (value && typeof value === 'object')
return Object.entries(value)
.map(([key, entry]) => `${key}: ${formatValue(entry)}`)
.join(', ')
if (typeof value === 'number')
return value.toLocaleString(undefined, { maximumFractionDigits: 2 })
return String(value ?? '—')
}
const data = ref(makeData(10_000))
const rowSource = ref<RowSource>('filtered')
const columns = columnHelper.columns([
columnHelper.display({ id: 'select' }),
columnHelper.accessor('category', {
header: 'Category',
filterFn: 'includesString',
}),
columnHelper.accessor('item', {
header: 'Item',
footer: ({ table }) => `${table.options.meta?.rowSource} total`,
}),
columnHelper.accessor('amount', {
header: 'Amount',
aggregationFn: 'sum',
cell: ({ getValue }) => getValue<number>().toLocaleString(),
footer: ({ column, table }) =>
formatValue(column.getAggregationValue(getAggregationRows(table))),
}),
columnHelper.accessor('score', {
header: 'Score',
aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }],
footer: ({ column, table }) =>
formatValue(column.getAggregationValue(getAggregationRows(table))),
}),
])
const table = useTable({
features,
data,
columns,
get meta() {
return { rowSource: rowSource.value }
},
initialState: { pagination: { pageIndex: 0, pageSize: 10 } },
debugTable: true,
debugColumns: true,
})
function setCategoryFilter(event: Event) {
table
.getColumn('category')
?.setFilterValue((event.target as HTMLInputElement).value)
}
function setRowSource(event: Event) {
rowSource.value = (event.target as HTMLSelectElement).value as RowSource
}
function setPage(event: Event) {
const value = (event.target as HTMLInputElement).value
table.setPageIndex(value ? Number(value) - 1 : 0)
}
</script>
<template>
<div class="demo-root">
<h1>Aggregation without grouping</h1>
<p>
Amount uses a scalar <code>sum</code>. Score runs count, mean, and range
together and returns a keyed object.
</p>
<div>
<button @click="data = makeData(10_000)">Regenerate Data</button
><button @click="data = makeData(200_000)">
Stress Test (200k rows)
</button>
</div>
<div class="spacer-sm" />
<div class="controls">
<label
>Category filter:
<input
:value="table.getColumn('category')?.getFilterValue() ?? ''"
@input="setCategoryFilter"
/></label>
<label
>Total rows:
<select :value="rowSource" @change="setRowSource">
<option value="filtered">Filtered rows</option>
<option value="all">All rows</option>
<option value="page">Visible page</option>
<option value="selected">Filtered selected rows</option>
<option value="custom">First three core rows</option>
</select></label
>
</div>
<div class="spacer-sm" />
<table>
<thead>
<tr v-for="group in table.getHeaderGroups()" :key="group.id">
<th v-for="header in group.headers" :key="header.id">
<input
v-if="header.column.id === 'select'"
type="checkbox"
:checked="table.getIsAllPageRowsSelected()"
@change="table.toggleAllPageRowsSelected()"
/><FlexRender v-else-if="!header.isPlaceholder" :header="header" />
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in table.getRowModel().rows" :key="row.id">
<td
v-for="cell in row.getAllCells()"
:key="cell.id"
:class="{ numeric: cell.column.id === 'amount' }"
>
<input
v-if="cell.column.id === 'select'"
type="checkbox"
:checked="row.getIsSelected()"
@change="row.toggleSelected()"
/><FlexRender v-else :cell="cell" />
</td>
</tr>
</tbody>
<tfoot>
<tr v-for="group in table.getFooterGroups()" :key="group.id">
<th
v-for="header in group.headers"
:key="header.id"
:colspan="header.colSpan"
>
<FlexRender v-if="!header.isPlaceholder" :footer="header" />
</th>
</tr>
</tfoot>
</table>
<div class="spacer-sm" />
<div class="controls">
<button
class="demo-button demo-button-sm"
@click="table.firstPage()"
:disabled="!table.getCanPreviousPage()"
>
<<</button
><button
class="demo-button demo-button-sm"
@click="table.previousPage()"
:disabled="!table.getCanPreviousPage()"
>
<</button
><button
class="demo-button demo-button-sm"
@click="table.nextPage()"
:disabled="!table.getCanNextPage()"
>
></button
><button
class="demo-button demo-button-sm"
@click="table.lastPage()"
:disabled="!table.getCanNextPage()"
>
>>
</button>
<span class="inline-controls"
><div>Page</div>
<strong
>{{
(table.atoms.pagination.get().pageIndex + 1).toLocaleString()
}}
of {{ table.getPageCount().toLocaleString() }}</strong
></span
>
<span class="inline-controls"
>| Go to page:<input
type="number"
min="1"
:max="table.getPageCount()"
:value="table.atoms.pagination.get().pageIndex + 1"
@input="setPage"
class="page-size-input"
/></span>
<select
:value="table.atoms.pagination.get().pageSize"
@change="
table.setPageSize(Number(($event.target as HTMLSelectElement).value))
"
>
<option v-for="size in [10, 20, 30, 40, 50]" :key="size" :value="size">
Show {{ size }}
</option>
</select>
</div>
<div>
Showing {{ table.getRowModel().rows.length.toLocaleString() }} of
{{ table.getRowCount().toLocaleString() }} Rows
</div>
<pre>{{ JSON.stringify(table.atoms.pagination.get(), null, 2) }}</pre>
</div>
</template>