Tableau de données
Des lignes avec recherche, tri, sélection et pagination. Il fait les quatre lui-même sur les lignes qu'on lui donne, ou les confie à un serveur en se contentant de rapporter ce qui est demandé.
Utilisation
<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>Exemples
Tri
Une colonne marquée sortable reçoit un en-tête cliquable, qui alterne croissant, décroissant, puis l'ordre dans lequel les lignes ont été données. v-model:sort lit et pose cet état.
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>Recherche
searchable pose un champ dans la barre d'outils, qui cherche dans les colonnes déclarées sans tenir compte de la casse ni des accents. v-model:search pilote le terme depuis ailleurs dans la 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>Toute valeur de v-model:per-page supérieure à zéro active la pagination. showRange ajoute le compte des lignes à côté de la navigation.
<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>Lignes par page
perPageOptions ajoute au pied un menu pour choisir le nombre de lignes par page. v-model:per-page rapporte ce qui a été choisi.
<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>Sélection
selectable ajoute une case à chaque ligne et une à l'en-tête pour la page visible. rowKey est alors obligatoire, et v-model:selected contient les identités données par ce champ.
<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>Le slot #title remplace la prop title et occupe la gauche de la barre d'outils, le champ de recherche gardant la droite.
<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>Cellules personnalisées
Un slot nommé d'après la clé d'une colonne remplace le contenu de ses cellules, et reçoit la ligne, la valeur brute et la colonne. La recherche et le tri lisent toujours la valeur sous-jacente.
<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>En-têtes personnalisés
Un slot nommé head- suivi de la clé de la colonne remplace un en-tête. Sur une colonne triable, il est rendu à l'intérieur du bouton de tri : tenez-vous-en donc au texte et à la décoration.
<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>Variantes
variant définit la décoration : flat n'en porte aucune, outlined ajoute un fond surélevé, une bordure, des coins arrondis et une gouttière autour de la barre d'outils, de la légende et du pied.
<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 resserre chaque cellule, et avec elles le champ de recherche, le menu de taille de page et la pagination.
<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>Lignes zébrées
striped teinte une ligne sur deux.
<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 garde les en-têtes de colonnes en place pendant le défilement des lignes. Il demande une zone de défilement bornée, par la prop height ou par un parent qui a sa propre hauteur.
<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>Pleine hauteur
height borne tout le composant, barre d'outils et pied compris, un nombre étant lu en pixels. Sans elle, le tableau prend la hauteur de son parent dès que celui-ci en a une.
<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>Conteneurs étroits
responsive décide de ce que fait un conteneur trop étroit pour les colonnes : défiler latéralement, ou transformer chaque ligne en carte avec ses en-têtes de colonnes répétés à l'intérieur. En cartes, la ligne d'en-têtes est hors de vue : ses boutons de tri et sa case « tout sélectionner » quittent l'ordre de tabulation avec elle.
<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>Côté serveur
serverSide délègue la recherche, le tri et la pagination : les lignes sont affichées telles qu'elles arrivent et update:params rapporte chaque changement. Passez total pour la pagination et la plage, et searchDebounce pour retarder la recherche.
<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>Chargement et vide
loading affiche un indicateur et loadingText à la place des lignes, et passe avant le vide. emptyText est ce que dit le tableau quand il n'y a rien à montrer. Les slots #loading et #empty remplacent l'un ou l'autre état, le second recevant la recherche (search) qui a vidé le tableau.
<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>Un tableau complet
Tout à la fois : un titre, une recherche, une sélection, quatre colonnes triables, des cellules personnalisées, et un pied portant le compte de la sélection, la taille de page, la plage et la 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
Événements
Slots
Types
Les types que les tables ci-dessus nomment, écrits comme la librairie les déclare. Ceux qui portent export s'importent depuis vectis-ui pour typer votre propre code ; les autres décrivent la forme de ce qu'un slot fournit.
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
Variables CSS