Skip to content

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 ​

KeyDoes
Enter, SpacePresses the focused sort button or expand button
SpaceChecks or unchecks the focused row box
TabMoves through the controls of the table, in reading order

API Reference ​

FsDataTable ​

PropTypeDefaultDescription
captionstringrequiredThe table's name, shown as its title above it.
hideCaptionbooleanfalseKeeps the title for assistive technology only, for a table under a heading that already names it.
size"default" | "dense""default"`dense` beside dense controls.
columnsDataTableColumn<T>[]requiredThe columns, in order.
dataT[]requiredThe rows: all of them, or with `total` the current page's.
rowId(row: T) => stringrequiredA row's stable id; selection and open rows are kept by it.
totalnumberThe number of rows across all pages. Given, the app sorts and pages, and `data` is the page to show.
selectablebooleanfalseA box on each row to select it, and the bar for actions on the selection.
pageSizesnumber[][25, 50, 100]The choices for rows per page. Paging is on when `pageSize` or `total` is given.
loadingbooleanfalseSkeleton rows in place of the data, until it arrives.
sorting{ column: string; direction: "ascending" | "descending"; } | nullnullThe sorted column; ascending first, never back to unsorted.
selectionstring[][]The selected rows' ids.
expandedstring[][]The open rows' ids.
pagenumber1The page shown, from 1.
pageSizenumberRows per page.
EventPayloadDescription
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`.
SlotPropsDescription
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.
emptyanyIn place of the rows when there are none: a dense `FsEmptyState` saying why, and what next.