Appearance
Data table
Shows many records in rows and columns, with sorting, selection, expandable rows and paging.
vue
<script setup lang="ts">
import { type DataTableColumn, FsDataTable } from '@fivespark/ui'
interface Order { order: string, customer: string, amount: number }
const orders: Order[] = [
{ order: '1042', customer: 'Bakker BV', amount: 1240 },
{ order: '1043', customer: 'De Vries Installatietechniek', amount: 868.5 },
{ order: '1044', customer: 'Jansen Bouw B.V.', amount: 3150 },
{ order: '1045', customer: 'Appel & Co', amount: 455 },
]
const euro = new Intl.NumberFormat('nl', { style: 'currency', currency: 'EUR' })
const columns: DataTableColumn<Order>[] = [
{ key: 'order', header: 'Order', sortable: true, width: '6rem' },
{ key: 'customer', header: 'Klant', sortable: true },
{ key: 'amount', header: 'Bedrag', sortable: true, align: 'end', width: '8rem', format: v => euro.format(v as number) },
]
</script>
<template>
<div class="w-full">
<FsDataTable caption="Orders" :columns :data="orders" :row-id="o => o.order" />
</div>
</template>Usage
vue
<script setup lang="ts">
import { type DataTableColumn, FsDataTable } from '@fivespark/ui'
interface Order { order: string, customer: string }
const columns: DataTableColumn<Order>[] = [
{ key: 'order', header: 'Order' },
{ key: 'customer', header: 'Klant', sortable: true },
]
</script>
<template>
<FsDataTable caption="Orders" :columns :data="orders" :row-id="o => o.order" />
</template>caption names the table and is shown as its title. rowId gives every row a stable id, which selection and expansion use. Put units in the headers and use align="end" for amounts. For a plain, static table, use Table.
Examples
Sorting
Mark columns sortable. A press sorts ascending, the next descending. Bind v-model:sorting to read or set the sort.
vue
<script setup lang="ts">
import { type DataTableColumn, FsDataTable } from '@fivespark/ui'
import { ref } from 'vue'
interface Order { order: string, customer: string, amount: number }
const orders: Order[] = [
{ order: '1042', customer: 'Bakker BV', amount: 1240 },
{ order: '1043', customer: 'De Vries Installatietechniek', amount: 868.5 },
{ order: '1044', customer: 'Jansen Bouw B.V.', amount: 3150 },
]
const euro = new Intl.NumberFormat('nl', { style: 'currency', currency: 'EUR' })
const columns: DataTableColumn<Order>[] = [
{ key: 'customer', header: 'Klant', sortable: true },
{ key: 'amount', header: 'Bedrag', sortable: true, align: 'end', width: '8rem', format: v => euro.format(v as number) },
]
const sorting = ref<{ column: string, direction: 'ascending' | 'descending' } | null>({ column: 'amount', direction: 'descending' })
</script>
<template>
<div class="w-full">
<FsDataTable v-model:sorting="sorting" caption="Orders op bedrag" :columns :data="orders" :row-id="o => o.order" />
</div>
</template>Selection and bulk actions
selectable adds a box to each row. The bulk slot holds the buttons that act on the selection; disable them while it is empty.
vue
<script setup lang="ts">
import { type DataTableColumn, FsButton, FsDataTable } from '@fivespark/ui'
import { ref } from 'vue'
interface Order { order: string, customer: string }
const orders: Order[] = [
{ order: '1042', customer: 'Bakker BV' },
{ order: '1043', customer: 'De Vries Installatietechniek' },
{ order: '1044', customer: 'Jansen Bouw B.V.' },
]
const columns: DataTableColumn<Order>[] = [
{ key: 'order', header: 'Order', width: '6rem' },
{ key: 'customer', header: 'Klant' },
]
const selection = ref<string[]>([])
</script>
<template>
<div class="w-full">
<FsDataTable v-model:selection="selection" caption="Orders" :columns :data="orders" :row-id="o => o.order" selectable>
<template #bulk="{ selection: selected }">
<FsButton label="Exporteren" size="dense" :disabled="!selected.length" />
</template>
</FsDataTable>
</div>
</template>Custom cells
A cell-<key> slot replaces a column's text with a link, a badge or a menu. A link to a record goes in a cell, never on the row.
vue
<script setup lang="ts">
import { type DataTableColumn, FsBadge, FsDataTable, FsLink } from '@fivespark/ui'
interface Order { order: string, customer: string, status: 'Betaald' | 'Open' }
const orders: Order[] = [
{ order: '1042', customer: 'Bakker BV', status: 'Betaald' },
{ order: '1043', customer: 'De Vries Installatietechniek', status: 'Open' },
{ order: '1044', customer: 'Jansen Bouw B.V.', status: 'Open' },
]
const columns: DataTableColumn<Order>[] = [
{ key: 'order', header: 'Order', width: '6rem' },
{ key: 'customer', header: 'Klant' },
{ key: 'status', header: 'Status', width: '7rem' },
]
</script>
<template>
<div class="w-full">
<FsDataTable caption="Orders" :columns :data="orders" :row-id="o => o.order">
<template #cell-order="{ row }">
<FsLink :to="`/orders/${row.order}`">{{ row.order }}</FsLink>
</template>
<template #cell-status="{ row }">
<FsBadge :tone="row.status === 'Betaald' ? 'success' : 'neutral'" :label="row.status" />
</template>
</FsDataTable>
</div>
</template>Expandable rows
The expanded slot holds the detail of an open row, and adds the expand column.
vue
<script setup lang="ts">
import { type DataTableColumn, FsDataTable } from '@fivespark/ui'
interface Order { order: string, customer: string, address: string }
const orders: Order[] = [
{ order: '1042', customer: 'Bakker BV', address: 'Hoofdstraat 1, Utrecht' },
{ order: '1043', customer: 'De Vries Installatietechniek', address: 'Kade 4, Rotterdam' },
]
const columns: DataTableColumn<Order>[] = [
{ key: 'order', header: 'Order', width: '6rem' },
{ key: 'customer', header: 'Klant' },
]
</script>
<template>
<div class="w-full">
<FsDataTable caption="Orders" :columns :data="orders" :row-id="o => o.order">
<template #expanded="{ row }">
<p class="type-body text-text">Afleveradres: {{ row.address }}</p>
</template>
</FsDataTable>
</div>
</template>Paging
Give page-size and the table pages its rows, with the choices from page-sizes (25, 50 and 100). With a server, give total and pass only the current page as data: the app then sorts and pages.
vue
<script setup lang="ts">
import { type DataTableColumn, FsDataTable } from '@fivespark/ui'
interface Order { order: string, customer: string }
const orders: Order[] = Array.from({ length: 60 }, (_, i) => ({ order: String(1000 + i), customer: `Klant ${i + 1}` }))
const columns: DataTableColumn<Order>[] = [
{ key: 'order', header: 'Order', width: '6rem' },
{ key: 'customer', header: 'Klant' },
]
</script>
<template>
<div class="w-full">
<FsDataTable caption="Orders" :columns :data="orders" :row-id="o => o.order" :page-size="25" />
</div>
</template>Empty
Say why there are no rows, and for a filter, how to clear it.
vue
<script setup lang="ts">
import { type DataTableColumn, FsDataTable, FsEmptyState } from '@fivespark/ui'
interface Order { order: string, customer: string }
const columns: DataTableColumn<Order>[] = [
{ key: 'order', header: 'Order', width: '6rem' },
{ key: 'customer', header: 'Klant' },
]
</script>
<template>
<div class="w-full">
<FsDataTable caption="Orders" :columns :data="[]" :row-id="o => o.order">
<template #empty>
<FsEmptyState size="dense" title="Geen orders gevonden" description="Er zijn geen orders voor deze periode." />
</template>
</FsDataTable>
</div>
</template>Loading
vue
<script setup lang="ts">
import { type DataTableColumn, FsDataTable } from '@fivespark/ui'
interface Order { order: string, customer: string }
const columns: DataTableColumn<Order>[] = [
{ key: 'order', header: 'Order', width: '6rem' },
{ key: 'customer', header: 'Klant' },
]
</script>
<template>
<div class="w-full">
<FsDataTable loading caption="Orders" :columns :data="[]" :row-id="o => o.order" />
</div>
</template>Keyboard
| Key | Does |
|---|---|
| Enter, Space | Presses the focused sort button or expand button |
| Space | Checks or unchecks the focused row box |
| Tab | Moves through the controls of the table, in reading order |
API Reference
FsDataTable
| Prop | Type | Default | Description |
|---|---|---|---|
caption | string | required | The table's name, shown as its title above it. |
hideCaption | boolean | false | Keeps the title for assistive technology only, for a table under a heading that already names it. |
size | "default" | "dense" | "default" | `dense` beside dense controls. |
columns | DataTableColumn<T>[] | required | The columns, in order. |
data | T[] | required | The rows: all of them, or with `total` the current page's. |
rowId | (row: T) => string | required | A row's stable id; selection and open rows are kept by it. |
total | number | The number of rows across all pages. Given, the app sorts and pages, and `data` is the page to show. | |
selectable | boolean | false | A box on each row to select it, and the bar for actions on the selection. |
pageSizes | number[] | [25, 50, 100] | The choices for rows per page. Paging is on when `pageSize` or `total` is given. |
loading | boolean | false | Skeleton rows in place of the data, until it arrives. |
sorting | { column: string; direction: "ascending" | "descending"; } | null | null | The sorted column; ascending first, never back to unsorted. |
selection | string[] | [] | The selected rows' ids. |
expanded | string[] | [] | The open rows' ids. |
page | number | 1 | The page shown, from 1. |
pageSize | number | Rows per page. |
| Event | Payload | Description |
|---|---|---|
update:sorting | [value: { column: string; direction: "ascending" | "descending"; } | null] | Sent with the new `sorting`, for `v-model:sorting`. |
update:selection | [value: string[]] | Sent with the new `selection`, for `v-model:selection`. |
update:expanded | [value: string[]] | Sent with the new `expanded`, for `v-model:expanded`. |
update:page | [value: number] | Sent with the new `page`, for `v-model:page`. |
update:pageSize | [value: number | undefined] | Sent with the new `pageSize`, for `v-model:pageSize`. |
| Slot | Props | Description |
|---|---|---|
expanded | { row: T; } | The detail of an open row; given, each row has a button to open it. |
bulk | { selection: string[]; } | The bulk bar's buttons. Block them while `selection` is empty. |
empty | any | In place of the rows when there are none: a dense `FsEmptyState` saying why, and what next. |