vue-datatable-core
v0.1.1
Published
Headless, fully-typed data table logic for Vue 3 — sorting, filtering, pagination, row selection, column resizing/visibility/ordering/pinning, and virtualization. Bring your own markup and styles.
Maintainers
Readme
vue-datatable-core
Headless, fully-typed data table logic for Vue 3. You bring the markup and styles — it brings sorting, filtering, pagination, row selection, column resizing / visibility / ordering / pinning, and virtualization.
- Headless. Zero markup, zero CSS. Render your own
<table>, divs, cards — whatever. The composable only manages state and derivation. - Vue-native. Built directly on Vue's reactivity (
ref/computed), not a wrapped framework-agnostic core. Real refs, realv-model, noflexRenderindirection. - Zero dependencies. Only
vueas a peer. ~6 kB gzipped for the core, ~1.6 kB for the virtualizer. - Typed end-to-end.
useDataTable<User>()infers cell value types from your accessors. - Controlled or uncontrolled. Every state slice works out of the box, or can be driven externally — which is also how server-side mode works.
- SSR / Nuxt safe. No DOM access at module scope.
Install
npm install vue-datatable-coreRequires Vue ^3.3.
Quick start
import { ref } from 'vue'
import { useDataTable, createColumnHelper } from 'vue-datatable-core'
interface User { id: number; name: string; email: string; age: number }
const data = ref<User[]>(/* … */)
const col = createColumnHelper<User>()
const table = useDataTable<User>({
data,
columns: [
col.accessor('name', { header: 'Name', enableSort: true }),
col.accessor('age', { header: 'Age', enableSort: true, filterFn: 'numberRange' }),
col.accessor('email', { header: 'Email', enableResize: true }),
],
initialState: { pagination: { pageSize: 20 } },
})useDataTable returns a reactive object, so there's no .value in templates:
<table>
<thead>
<tr>
<th
v-for="h in table.headers"
:key="h.id"
:style="{ width: h.size + 'px' }"
:aria-sort="h.ariaSort"
@click="h.toggleSort($event.shiftKey)"
>
{{ h.label }}
<span v-if="h.isSorted">{{ h.isSorted === 'asc' ? '▲' : '▼' }}</span>
<span v-if="h.canResize" class="resizer" @mousedown="(e) => h.getResizeHandler()(e)" />
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in table.rows" :key="row.id">
<td v-for="cell in row.cells" :key="cell.id">{{ cell.value }}</td>
</tr>
</tbody>
</table>
<button :disabled="!table.canPrevPage" @click="table.prevPage()">Prev</button>
<span>Page {{ table.pageIndex + 1 }} / {{ table.pageCount }}</span>
<button :disabled="!table.canNextPage" @click="table.nextPage()">Next</button>Columns
Define columns with a key, a dot-path string, or a function accessor. Use createColumnHelper<T>() for the best inference, or plain ColumnDef<T>[].
col.accessor('email', { header: 'Email' }) // key
col.accessor('address.city' as any, { id: 'city' }) // dot-path
col.compute((u) => `${u.first} ${u.last}`, { // function
id: 'fullName', header: 'Name',
})
col.display({ id: 'actions', header: '' }) // no accessor (buttons, etc.)Each column supports: enableSort, sortFn, sortDescFirst, enableFilter, filterFn, enableGlobalFilter, size / minSize / maxSize, enableResize, enableHiding, enablePinning, and free-form meta.
Features
Everything is a method on the table plus getters on each header / row.
// Sorting (3-state; pass `true` for multi-sort / shift-click)
table.toggleSort('age') // header.isSorted, header.sortIndex, header.ariaSort
table.setSorting([{ id: 'age', desc: true }])
// Filtering
table.setGlobalFilter('ada') // searches all global-filterable columns
table.setColumnFilter('age', [30, 50])
// Pagination
table.setPageSize(25); table.nextPage(); table.lastPage()
// Selection (needs a stable getRowId for persistence across sorting/paging)
row.toggleSelected() // row.selected, row.canSelect
table.toggleAllPageRowsSelected() // table.selectedRows, table.isAllRowsSelected
// Column visibility / ordering / pinning
header.toggleVisibility() // table.allHeaders for a column menu
table.setColumnOrder(['age', 'name'])
header.pin('left') // header.pinned, header.pinOffset (px for sticky)
// Resizing — attach the handler to a grip element
<span class="resizer" @mousedown="(e) => header.getResizeHandler()(e)" />Stable row ids
Selection (and any per-row state) keys off a row id. Provide getRowId so it survives sorting/filtering/paging:
useDataTable({ data, columns, getRowId: (u) => String(u.id) })Virtualization
The virtualizer is a separate, pairable composable (so it tree-shakes away if unused, and works on any list):
import { useVirtualizer } from 'vue-datatable-core/virtual'
const scrollEl = ref<HTMLElement | null>(null)
const v = useVirtualizer({
count: () => table.rows.length,
getScrollElement: () => scrollEl.value,
estimateSize: () => 40, // px per row; measured rows override this
overscan: 8,
})<div ref="scrollEl" style="overflow:auto; height:400px">
<div :style="{ height: v.totalSize + 'px', position: 'relative' }">
<div
v-for="item in v.virtualItems"
:key="item.key"
:data-index="item.index"
:ref="v.measureElement"
:style="{ position:'absolute', top:0, transform:`translateY(${item.start}px)` }"
>
{{ table.rows[item.index].getValue('name') }}
</div>
</div>
</div>Supports fixed and dynamically-measured heights, vertical or horizontal, plus scrollToIndex(i, { align }).
Server-side / manual mode
Set manualSorting, manualFiltering, and/or manualPagination to skip the built-in stage and feed already-processed data. The table still tracks state — watch it and refetch:
const table = useDataTable({
data: serverRows, // already sorted/filtered/paged by your API
columns,
manualSorting: true,
manualFiltering: true,
manualPagination: true,
pageCount: () => totalPages, // you know the total
})
watch(() => table.getState(), (s) => refetch(s), { deep: true })You can also fully control any single slice via state + the matching on…Change callback (great for syncing to the URL):
const sorting = ref<SortingState>([])
useDataTable({ data, columns, state: { sorting }, onSortingChange: (s) => (sorting.value = s) })Roadmap
- ✅ Sorting · filtering · pagination · selection · resizing · visibility · ordering · pinning · virtualization
- ⏭ Grouping & aggregation, expandable sub-rows
- ⏭ Faceted filter helpers (unique values, min/max)
- ⏭ Nuxt module + devtools
License
MIT
