Data table
Rows with searching, sorting, selection and pagination. It does all four itself over the rows it is given, or hands them to a server and simply reports what is being asked for.
Usage
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project' },
{ key: 'owner', label: 'Owner' },
{ key: 'commits', label: 'Commits' },
]
const rows = [
{ name: 'Vectis UI', owner: 'Ada Lovelace', commits: 320 },
{ name: 'Atlas', owner: 'Grace Hopper', commits: 87 },
{ name: 'Meridian', owner: 'Alan Turing', commits: 44 },
]
</script>
<template>
<VDataTable :columns="columns" :rows="rows" row-key="name" title="Projects" />
</template>Examples
Sorting
A column marked sortable gets a clickable heading, cycling ascending, descending, then the order the rows were given. v-model:sort reads and sets that state.
Sorted by commits, descending
<script setup lang="ts">
import { ref } from 'vue'
import { VDataTable, type DataTableSort } from 'vectis-ui'
/* `sortable` turns a heading into a button and the table does the sorting itself, on the
values as they are given. The ascending icon points down, the spreadsheet convention. */
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status', sortable: true },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const rows = [
{ name: 'Vectis', owner: 'Xavier', status: 'Active', commits: 320 },
{ name: 'Atlas', owner: 'Nadia', status: 'Active', commits: 87 },
{ name: 'Brume', owner: 'Louis', status: 'Archived', commits: 1204 },
{ name: 'Granit', owner: 'Emma', status: 'Active', commits: 45 },
]
/* The sort is a model, so the table can open on a column already sorted, and what the
reader clicks can be read back. */
const sort = ref<DataTableSort | null>({ key: 'commits', direction: 'desc' })
const DIRECTIONS = { asc: 'ascending', desc: 'descending' }
</script>
<template>
<div class="demo">
<VDataTable
v-model:sort="sort"
:columns="columns"
:rows="rows"
row-key="name"
title="Projects"
caption="Organisation projects"
/>
<p class="state">
Sorted by {{ sort ? `${sort.key}, ${DIRECTIONS[sort.direction]}` : 'nothing' }}
</p>
</div>
</template>
<style scoped>
.demo {
display: grid;
gap: var(--vectis-space-3);
}
.state {
margin: 0;
color: var(--vectis-color-text-muted);
font-size: var(--vectis-text-body-sm-size);
}
</style>Search
searchable puts a field in the toolbar, which searches the declared columns while ignoring case and accents. v-model:search drives the term from elsewhere on the page.
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status' },
{ key: 'commits', label: 'Commits', align: 'end' as const },
]
/* `Éclair` keeps its accent: the search ignores diacritics, so eclair finds it. */
const rows = [
{ name: 'Vectis', owner: 'Xavier', status: 'Active', commits: 320 },
{ name: 'Atlas', owner: 'Nadia', status: 'Active', commits: 87 },
{ name: 'Brume', owner: 'Louis', status: 'Archived', commits: 1204 },
{ name: 'Granit', owner: 'Emma', status: 'Active', commits: 45 },
{ name: 'Éclair', owner: 'Xavier', status: 'Active', commits: 296 },
{ name: 'Falaise', owner: 'Nadia', status: 'Archived', commits: 133 },
{ name: 'Givre', owner: 'Louis', status: 'Active', commits: 58 },
{ name: 'Houle', owner: 'Emma', status: 'Active', commits: 411 },
]
</script>
<template>
<VDataTable
:columns="columns"
:rows="rows"
row-key="name"
title="Projects"
searchable
search-placeholder="Search projects"
search-label="Search the projects"
caption="Organisation projects"
/>
</template>Any v-model:per-page above zero turns the pagination on. showRange adds the row count beside the nav.
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status' },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const NAMES = [
'Vectis',
'Atlas',
'Brume',
'Granit',
'Éclair',
'Falaise',
'Givre',
'Houle',
'Islet',
'Jade',
'Karst',
'Lande',
'Mistral',
'Nacre',
'Ombre',
'Pollen',
'Quartz',
'Rivage',
'Sillage',
'Tuile',
'Vigie',
'Zenith',
]
const OWNERS = ['Xavier', 'Nadia', 'Louis', 'Emma']
const rows = NAMES.map((name, index) => ({
name,
owner: OWNERS[index % OWNERS.length],
status: index % 3 === 0 ? 'Archived' : 'Active',
commits: ((index + 3) * 37) % 500,
}))
</script>
<template>
<!-- A page size is all it takes: passing one down, bound or not, turns the pagination on. -->
<VDataTable
:columns="columns"
:rows="rows"
row-key="name"
title="Projects"
searchable
:per-page="6"
show-range
caption="Organisation projects"
/>
</template>Rows per page
perPageOptions adds a menu to the footer for choosing how many rows a page holds. v-model:per-page reports what was chosen.
<script setup lang="ts">
import { ref } from 'vue'
import { VDataTable } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status' },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const NAMES = [
'Vectis',
'Atlas',
'Brume',
'Granit',
'Éclair',
'Falaise',
'Givre',
'Houle',
'Islet',
'Jade',
'Karst',
'Lande',
'Mistral',
'Nacre',
'Ombre',
'Pollen',
'Quartz',
'Rivage',
'Sillage',
'Tuile',
'Vigie',
'Zenith',
]
const OWNERS = ['Xavier', 'Nadia', 'Louis', 'Emma']
const rows = NAMES.map((name, index) => ({
name,
owner: OWNERS[index % OWNERS.length],
status: index % 3 === 0 ? 'Archived' : 'Active',
commits: ((index + 3) * 37) % 500,
}))
const perPage = ref(5)
</script>
<template>
<VDataTable
v-model:per-page="perPage"
:columns="columns"
:rows="rows"
row-key="name"
title="Projects"
:per-page-options="[5, 10, 25]"
show-range
caption="Organisation projects"
/>
</template>Selection
selectable adds a checkbox to every row and one to the heading for the visible page. rowKey is required with it, and v-model:selected holds the identities that field gives.
<script setup lang="ts">
import { ref } from 'vue'
import { VDataTable, type DataTableRowId } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status' },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const rows = [
{ name: 'Vectis', owner: 'Xavier', status: 'Active', commits: 320 },
{ name: 'Atlas', owner: 'Nadia', status: 'Active', commits: 87 },
{ name: 'Brume', owner: 'Louis', status: 'Archived', commits: 1204 },
{ name: 'Granit', owner: 'Emma', status: 'Active', commits: 45 },
{ name: 'Éclair', owner: 'Xavier', status: 'Active', commits: 296 },
]
type Project = (typeof rows)[number]
/* What comes back are the identities `rowKey` names, never the row objects. */
const selected = ref<DataTableRowId[]>(['Atlas'])
/* "Select row" says nothing about which one, so the row itself names its checkbox. */
const selectRowLabel = (row: Project) => `Select ${row.name}`
</script>
<template>
<div class="demo">
<VDataTable
v-model:selected="selected"
:columns="columns"
:rows="rows"
row-key="name"
title="Projects"
selectable
:select-row-label="selectRowLabel"
select-all-label="Select every project on this page"
caption="Organisation projects"
/>
<p class="state">Selected: {{ selected.length ? selected.join(', ') : 'nothing' }}</p>
</div>
</template>
<style scoped>
.demo {
display: grid;
gap: var(--vectis-space-3);
}
.state {
margin: 0;
color: var(--vectis-color-text-muted);
font-size: var(--vectis-text-body-sm-size);
}
</style>The #title slot replaces the title prop and takes the left of the toolbar, the search field keeping the right.
<script setup lang="ts">
import { computed, ref } from 'vue'
import { VButton, VDataTable, VMenu, VMenuItem } from 'vectis-ui'
import { expand_more as expandMore } from 'vectis-ui/icons'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status' },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const rows = [
{ name: 'Vectis', owner: 'Xavier', status: 'Active', commits: 320 },
{ name: 'Atlas', owner: 'Nadia', status: 'Active', commits: 87 },
{ name: 'Brume', owner: 'Louis', status: 'Archived', commits: 1204 },
{ name: 'Granit', owner: 'Emma', status: 'Active', commits: 45 },
{ name: 'Éclair', owner: 'Xavier', status: 'Archived', commits: 296 },
]
/* The filtering is yours: the table shows the rows it is given, and the search field it
provides narrows them further. */
const status = ref('All')
const filtered = computed(() =>
status.value === 'All' ? rows : rows.filter((row) => row.status === status.value),
)
</script>
<template>
<VDataTable
:columns="columns"
:rows="filtered"
row-key="name"
searchable
caption="Organisation projects"
>
<template #title>
<div class="toolbar">
<span>Projects</span>
<VMenu match-trigger>
<template #trigger="{ triggerProps }">
<VButton
v-bind="triggerProps"
variant="outline"
tone="neutral"
size="sm"
:icon-end="expandMore"
>
Status: {{ status }}
</VButton>
</template>
<VMenuItem
v-for="option in ['All', 'Active', 'Archived']"
:key="option"
:label="option"
@select="status = option"
/>
</VMenu>
<VButton size="sm">New project</VButton>
</div>
</template>
</VDataTable>
</template>
<style scoped>
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--vectis-space-3);
}
</style>Custom cells
A slot named after a column key replaces what that column's cells show, and receives the row, the raw value and the column. Searching and sorting still read the underlying value.
<script setup lang="ts">
import { VAvatar, VChip, VDataTable } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status' },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const rows = [
{ name: 'Vectis', owner: 'Xavier Darmet', status: 'Active', commits: 320 },
{ name: 'Atlas', owner: 'Nadia Rousseau', status: 'Active', commits: 87 },
{ name: 'Brume', owner: 'Louis Fabre', status: 'Archived', commits: 1204 },
{ name: 'Granit', owner: 'Emma Lefort', status: 'Active', commits: 45 },
]
const numbers = new Intl.NumberFormat('en-GB')
</script>
<template>
<VDataTable
:columns="columns"
:rows="rows"
row-key="name"
title="Projects"
caption="Organisation projects"
>
<!-- A cell slot is named after its column key, and receives the whole row. -->
<template #cell-owner="{ row }">
<span class="owner">
<VAvatar :name="row.owner" size="xs" />
{{ row.owner }}
</span>
</template>
<template #cell-status="{ row }">
<VChip :tone="row.status === 'Active' ? 'success' : 'neutral'" size="xs">
{{ row.status }}
</VChip>
</template>
<!-- Sorting reads the underlying value, so a formatted number still sorts as a number. -->
<template #cell-commits="{ row }">
<span class="figure">{{ numbers.format(row.commits) }}</span>
</template>
</VDataTable>
</template>
<style scoped>
.owner {
display: inline-flex;
align-items: center;
gap: var(--vectis-space-2);
}
.figure {
font-variant-numeric: tabular-nums;
}
</style>Custom headings
A slot named head- plus the column key replaces a heading. On a sortable column it renders inside the sort button, so keep it to text and decoration.
<script setup lang="ts">
import { VDataTable, VIcon } from 'vectis-ui'
import { code, schedule } from 'vectis-ui/icons'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'updated', label: 'Last update' },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const rows = [
{ name: 'Vectis', owner: 'Xavier', updated: '2 days ago', commits: 320 },
{ name: 'Atlas', owner: 'Nadia', updated: '3 weeks ago', commits: 87 },
{ name: 'Brume', owner: 'Louis', updated: 'Yesterday', commits: 1204 },
{ name: 'Granit', owner: 'Emma', updated: 'An hour ago', commits: 45 },
]
</script>
<template>
<VDataTable
:columns="columns"
:rows="rows"
row-key="name"
title="Projects"
caption="Organisation projects"
>
<template #head-updated="{ column }">
<span class="heading"><VIcon :name="schedule" :size="16" />{{ column.label }}</span>
</template>
<!-- On a sortable column the slot renders inside the sort button, so keep it to text
and decoration: a control there would be a control inside a control. -->
<template #head-commits="{ column }">
<span class="heading"><VIcon :name="code" :size="16" />{{ column.label }}</span>
</template>
</VDataTable>
</template>
<style scoped>
.heading {
display: inline-flex;
align-items: center;
gap: var(--vectis-space-1);
}
</style>Variants
variant sets the decoration: flat carries none, outlined adds a raised background, a border, rounded corners and a gutter around the toolbar, the caption and the footer.
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const rows = [
{ name: 'Vectis', owner: 'Xavier', commits: 320 },
{ name: 'Atlas', owner: 'Nadia', commits: 87 },
{ name: 'Brume', owner: 'Louis', commits: 1204 },
]
</script>
<template>
<div class="stack">
<!-- Flat carries no decoration at all and sits on whatever surface it is placed on. -->
<VDataTable
:columns="columns"
:rows="rows"
row-key="name"
title="Flat"
searchable
caption="Organisation projects, flat"
/>
<!-- Outlined makes it a card, and the frame gives the toolbar and the footer a gutter. -->
<VDataTable
variant="outlined"
:columns="columns"
:rows="rows"
row-key="name"
title="Outlined"
searchable
caption="Organisation projects, outlined"
/>
</div>
</template>
<style scoped>
.stack {
display: grid;
gap: var(--vectis-space-6);
}
</style>Compact
compact tightens every cell, and the search field, the page size menu and the pagination with them.
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status' },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const rows = [
{ name: 'Vectis', owner: 'Xavier', status: 'Active', commits: 320 },
{ name: 'Atlas', owner: 'Nadia', status: 'Active', commits: 87 },
{ name: 'Brume', owner: 'Louis', status: 'Archived', commits: 1204 },
{ name: 'Granit', owner: 'Emma', status: 'Active', commits: 45 },
{ name: 'Éclair', owner: 'Xavier', status: 'Active', commits: 296 },
{ name: 'Falaise', owner: 'Nadia', status: 'Archived', commits: 133 },
]
</script>
<template>
<!-- The cells tighten, and so does everything the table renders with them: the search
field, the page size menu and the pagination all take the shorter step. -->
<VDataTable
compact
variant="outlined"
:columns="columns"
:rows="rows"
row-key="name"
title="Projects"
searchable
:per-page="4"
show-range
caption="Organisation projects"
/>
</template>Striped rows
striped tints every other row.
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status' },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const rows = [
{ name: 'Vectis', owner: 'Xavier', status: 'Active', commits: 320 },
{ name: 'Atlas', owner: 'Nadia', status: 'Active', commits: 87 },
{ name: 'Brume', owner: 'Louis', status: 'Archived', commits: 1204 },
{ name: 'Granit', owner: 'Emma', status: 'Active', commits: 45 },
{ name: 'Éclair', owner: 'Xavier', status: 'Active', commits: 296 },
{ name: 'Falaise', owner: 'Nadia', status: 'Archived', commits: 133 },
]
</script>
<template>
<VDataTable
striped
:columns="columns"
:rows="rows"
row-key="name"
title="Projects"
caption="Organisation projects"
/>
</template>stickyHeader keeps the column headings in place while the rows scroll. It needs a bounded scrolling area, either the height prop or a parent with a height of its own.
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status' },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const NAMES = [
'Vectis',
'Atlas',
'Brume',
'Granit',
'Éclair',
'Falaise',
'Givre',
'Houle',
'Islet',
'Jade',
'Karst',
'Lande',
'Mistral',
'Nacre',
'Ombre',
'Pollen',
'Quartz',
'Rivage',
'Sillage',
'Tuile',
'Vigie',
'Zenith',
]
const OWNERS = ['Xavier', 'Nadia', 'Louis', 'Emma']
const rows = NAMES.map((name, index) => ({
name,
owner: OWNERS[index % OWNERS.length],
status: index % 3 === 0 ? 'Archived' : 'Active',
commits: ((index + 3) * 37) % 500,
}))
</script>
<template>
<!-- The headings need something to stay put against, so the scrolling area has to be
bounded: here by `height`, otherwise by a parent with a height of its own. -->
<VDataTable
sticky-header
variant="outlined"
:height="320"
:columns="columns"
:rows="rows"
row-key="name"
title="Projects"
caption="Organisation projects"
/>
</template>Full height
height bounds the whole component, toolbar and footer included, a number being read as pixels. Left out, the table takes its parent's height whenever the parent has one.
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status' },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const NAMES = [
'Vectis',
'Atlas',
'Brume',
'Granit',
'Éclair',
'Falaise',
'Givre',
'Houle',
'Islet',
'Jade',
'Karst',
'Lande',
]
const OWNERS = ['Xavier', 'Nadia', 'Louis', 'Emma']
const rows = NAMES.map((name, index) => ({
name,
owner: OWNERS[index % OWNERS.length],
status: index % 3 === 0 ? 'Archived' : 'Active',
commits: ((index + 3) * 37) % 500,
}))
</script>
<template>
<!-- The panel has a height, so the table takes it: the toolbar and the footer keep their
places and only the rows scroll, whatever the page holds. Change the page size and
the footer stays where it is. -->
<div class="panel">
<VDataTable
variant="outlined"
sticky-header
:columns="columns"
:rows="rows"
row-key="name"
title="Projects"
searchable
:per-page="5"
:per-page-options="[5, 10]"
show-range
caption="Organisation projects"
/>
</div>
</template>
<style scoped>
.panel {
block-size: 26rem;
}
</style>Narrow containers
responsive decides what a container too narrow for the columns does: scroll sideways, or turn each row into a card with its column headings repeated inside it. As cards, the heading row is out of sight, so its sort buttons and its "select all" box leave the tab order with it.
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status' },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const rows = [
{ name: 'Vectis', owner: 'Xavier', status: 'Active', commits: 320 },
{ name: 'Atlas', owner: 'Nadia', status: 'Active', commits: 87 },
{ name: 'Brume', owner: 'Louis', status: 'Archived', commits: 1204 },
]
</script>
<template>
<!-- The threshold is the COMPONENT's own width, not the window's, so the box below is
resizable: drag its corner past 640px and the cards become a table again. -->
<div class="box">
<VDataTable
responsive="stack"
variant="outlined"
:columns="columns"
:rows="rows"
row-key="name"
title="Projects"
selectable
caption="Organisation projects"
/>
</div>
</template>
<style scoped>
.box {
overflow: auto;
inline-size: 24rem;
min-inline-size: 15rem;
max-inline-size: 100%;
padding: var(--vectis-space-1);
resize: horizontal;
}
</style>Server side
serverSide hands the searching, the sorting and the paging over: the rows are shown as they arrive and update:params reports every change. Pass total for the pagination and the range, and searchDebounce to delay the search.
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { VDataTable, type DataTableParams } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner' },
{ key: 'status', label: 'Status' },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const NAMES = [
'Vectis',
'Atlas',
'Brume',
'Granit',
'Éclair',
'Falaise',
'Givre',
'Houle',
'Islet',
'Jade',
'Karst',
'Lande',
'Mistral',
'Nacre',
'Ombre',
'Pollen',
'Quartz',
'Rivage',
'Sillage',
'Tuile',
'Vigie',
'Zenith',
]
const OWNERS = ['Xavier', 'Nadia', 'Louis', 'Emma']
/* What a database would hold. Nothing here is ever handed to the table whole. */
const DATA = NAMES.map((name, index) => ({
name,
owner: OWNERS[index % OWNERS.length],
status: index % 3 === 0 ? 'Archived' : 'Active',
commits: ((index + 3) * 37) % 500,
}))
type Project = (typeof DATA)[number]
const rows = ref<Project[]>([])
const total = ref(0)
const loading = ref(true)
/* A stand-in for the request. In server mode the table filters, sorts and slices nothing,
so everything below is the answering end's work. */
function load(params: DataTableParams) {
loading.value = true
window.setTimeout(() => {
const term = params.search.trim().toLowerCase()
const matching = term
? DATA.filter((row) => `${row.name} ${row.owner} ${row.status}`.toLowerCase().includes(term))
: DATA
const key = params.sortKey as keyof Project | null
const sorted = key
? [...matching].sort(
(a, b) =>
String(a[key]).localeCompare(String(b[key]), 'en', { numeric: true }) *
(params.sortDirection === 'desc' ? -1 : 1),
)
: matching
const size = params.perPage ?? sorted.length
const start = (params.page - 1) * size
rows.value = sorted.slice(start, start + size)
total.value = sorted.length
loading.value = false
}, 600)
}
/* Nothing is emitted when the table appears, so the first page is asked for here. */
onMounted(() => load({ page: 1, perPage: 5, sortKey: null, sortDirection: null, search: '' }))
</script>
<template>
<VDataTable
server-side
variant="outlined"
:columns="columns"
:rows="rows"
:total="total"
:loading="loading"
row-key="name"
title="Projects"
searchable
:per-page="5"
:per-page-options="[5, 10]"
show-range
caption="Organisation projects, a page at a time"
@update:params="load"
/>
</template>Loading and empty
loading shows a spinner and loadingText in place of the rows, and is answered before emptiness. emptyText is what the table says when there is nothing to show. The #loading and #empty slots replace either state, the second receiving the search that emptied the table.
<script setup lang="ts">
import { VDataTable } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project' },
{ key: 'owner', label: 'Owner' },
{ key: 'commits', label: 'Commits', align: 'end' as const },
]
</script>
<template>
<div class="stack">
<!-- Loading is answered before emptiness, so a table waiting for its rows never
claims there are none. -->
<VDataTable
loading
variant="outlined"
:columns="columns"
:rows="[]"
title="Loading"
caption="Projects being loaded"
/>
<VDataTable
variant="outlined"
:columns="columns"
:rows="[]"
empty-text="No project yet"
title="Nothing to show"
caption="Projects, none of them"
/>
</div>
</template>
<style scoped>
.stack {
display: grid;
gap: var(--vectis-space-6);
}
</style>A complete table
Everything at once: a title, a search, a selection, four sortable columns, cells of its own, and a footer carrying the selection count, the page size, the range and the pagination.
<script setup lang="ts">
import { ref } from 'vue'
import { VAvatar, VChip, VDataTable, type DataTableRowId } from 'vectis-ui'
const columns = [
{ key: 'name', label: 'Project', sortable: true },
{ key: 'owner', label: 'Owner', sortable: true },
{ key: 'status', label: 'Status', sortable: true },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' as const },
]
const NAMES = [
'Vectis',
'Atlas',
'Brume',
'Granit',
'Éclair',
'Falaise',
'Givre',
'Houle',
'Islet',
'Jade',
'Karst',
'Lande',
'Mistral',
'Nacre',
'Ombre',
'Pollen',
'Quartz',
'Rivage',
'Sillage',
'Tuile',
'Vigie',
'Zenith',
]
const OWNERS = ['Xavier Darmet', 'Nadia Rousseau', 'Louis Fabre', 'Emma Lefort']
const rows = NAMES.map((name, index) => ({
name,
owner: OWNERS[index % OWNERS.length],
status: index % 3 === 0 ? 'Archived' : 'Active',
commits: ((index + 3) * 37) % 500,
}))
type Project = (typeof rows)[number]
const selected = ref<DataTableRowId[]>([])
const numbers = new Intl.NumberFormat('en-GB')
const selectRowLabel = (row: Project) => `Select ${row.name}`
const selectionText = (count: number) => `${count} project${count > 1 ? 's' : ''} selected`
</script>
<template>
<VDataTable
v-model:selected="selected"
variant="outlined"
:columns="columns"
:rows="rows"
row-key="name"
title="Projects"
searchable
search-placeholder="Search projects"
selectable
:select-row-label="selectRowLabel"
:selection-text="selectionText"
sticky-header
:height="420"
:per-page="8"
:per-page-options="[8, 16, 24]"
show-range
caption="Every project in the organisation"
>
<template #cell-owner="{ row }">
<span class="owner">
<VAvatar :name="row.owner" size="xs" />
{{ row.owner }}
</span>
</template>
<template #cell-status="{ row }">
<VChip :tone="row.status === 'Active' ? 'success' : 'neutral'" size="xs">
{{ row.status }}
</VChip>
</template>
<template #cell-commits="{ row }">
<span class="figure">{{ numbers.format(row.commits) }}</span>
</template>
</VDataTable>
</template>
<style scoped>
.owner {
display: inline-flex;
align-items: center;
gap: var(--vectis-space-2);
}
.figure {
font-variant-numeric: tabular-nums;
}
</style>API
Props
Events
Slots
Types
The types the tables above name, written as the library declares them. The ones carrying export can be imported from vectis-ui to type your own code; the others are the shape of what a slot hands out.
export interface BuiltinIcon {
name: string
paths: readonly [string] | readonly [string, string]
}export interface DataTableColumn {
key: string
label: string
sortable?: boolean
align?: DataTableColumnAlign
}export type DataTableColumnAlign = 'start' | 'center' | 'end'
export interface DataTableEmptySlotProps {
search: string
}export interface DataTableParams {
page: number
perPage: number | null
sortKey: string | null
sortDirection: DataTableSortDirection | null
search: string
}export interface DataTableRange {
start: number
end: number
total: number
}export type DataTableRowId = string | number
export interface DataTableSort {
key: string
direction: DataTableSortDirection
}export type DataTableSortDirection = 'asc' | 'desc'
export type IconRender =
| { path: string; viewBox?: string }
| { component: Component; props?: Record<string, unknown> }
| { src: string }
| { text: string; class?: string }
| { class: string }export type IconSource = string | BuiltinIcon | IconRender
CSS variables