Skip to content

Tableau - DsfrTable

🌟 Introduction

Le composant DsfrTable est un élément puissant et polyvalent pour afficher des données sous forme de tableaux dans vos applications Vue. Utilisant une combinaison de slots, de props, et d'événements personnalisés, ce composant offre une flexibilité remarquable. Plongeons dans les détails !

🏅 La documentation sur le tableau sur le DSFR

La story sur le tableau sur le storybook de VueDsfr

🛠️ Props

NomTypeDéfautObligatoireDescription
titlestringLes en-têtes de votre tableau.
headersArray<string>[]Les en-têtes de votre tableau.
rowsArray<DsfrTableRowProps | string[] | DsfrTableCellProps[]>[]Les données de chaque rangée dans le tableau.
rowKeystring | FunctionundefinedUne clé unique pour chaque rangée, utilisée pour optimiser la mise à jour du DOM.
currentPagenumber1La page actuelle dans la pagination du tableau.
resultsDisplayednumber10Le nombre de résultats affichés par page dans la pagination.

📡 Événements

NomDescription
update:currentPageÉmis lors du changement de la page actuelle.

🧩 Slots

  • header: Ce slot permet de personnaliser les en-têtes du tableau. Par défaut, il utilise DsfrTableHeaders avec les props headers.
  • Slot par défaut: Utilisé pour le corps du tableau. Par défaut, il affiche les rangées de données via DsfrTableRow.

📝 Exemples

Exemple Basique

vue
<script lang="ts" setup>
import DsfrTable from '../DsfrTable.vue'
</script>

<template>
  <DsfrTable
    title="Exemple de tableau simple"
    :headers="['Nom', 'Age', 'Ville']"
    :rows="[
      { rowData: ['Alice', '30', 'Paris'] },
      { rowData: ['Bob', '24', 'Lyon'] },
    ]"
  />
</template>

Exemple utilisant des composants dans les cellules

vue
<script lang="ts" setup>
import { getCurrentInstance } from 'vue'

import DsfrTag from '../../DsfrTag/DsfrTag.vue'
import DsfrTable from '../DsfrTable.vue'
import type { DsfrTableCellProps, DsfrTableRowProps } from '../DsfrTable.types'

getCurrentInstance()?.appContext.app.component('DsfrTag', DsfrTag)

const title = 'Utilisateurs'
const headers = ['Nom', 'Prénom', 'Email', 'Statut']
const rows: (string | DsfrTableRowProps | DsfrTableCellProps | { component: string, [k: string]: unknown })[][] = [
  [
    'SÖZE',
    'Keyser',
    'keyser.soze@mastermind.com',
    {
      component: 'DsfrTag',
      label: 'Info',
      class: 'info',
    },
  ],
  [
    'HUNT',
    'Ethan',
    'ethan.hunt@impossible.com',
    {
      component: 'DsfrTag',
      label: 'Erreur',
      class: 'error',
    },
  ],
  [
    'HOLMES',
    'Sherlock',
    'sherlock.holmes@whodunit.com',
    {
      component: 'DsfrTag',
      label: 'Succès',
      class: 'success',
    },
  ],
  [
    'JONES',
    'Indiana',
    'indiana.jones@marshall-college.com',
    {
      component: 'DsfrTag',
      label: 'Info',
      class: 'info',
    },
  ],
  [
    'WAYNE',
    'Bruce',
    'bruce.wayne@batmail.com',
    {
      component: 'DsfrTag',
      label: 'Erreur',
      class: 'error',
    },
  ],
]
</script>

<template>
  <DsfrTable
    :title="title"
    :headers="headers"
    :rows="rows"
    :no-caption="noCaption"
    :pagination="pagination"
    :current-page="currentPage"
    :results-displayed="resultsDisplayed"
  />
</template>

<style scoped>
:deep(.info) {
  color: var(--info-425-625);
  background-color: var(--info-950-100);
}
:deep(.error) {
  color: var(--error-425-625);
  background-color: var(--error-950-100);
}
:deep(.success) {
  color: var(--success-425-625);
  background-color: var(--success-950-100);
}
</style>

⚙️ Code source du composant

vue
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'

import DsfrTableHeaders from './DsfrTableHeaders.vue'
import DsfrTableRow, { type DsfrTableRowProps } from './DsfrTableRow.vue'
import type { DsfrTableProps } from './DsfrTable.types'

export type { DsfrTableProps }

const props = withDefaults(defineProps<DsfrTableProps>(), {
  headers: () => [],
  rows: () => [],
  rowKey: undefined,
  currentPage: 1,
  resultsDisplayed: 10,
})

// Permet aux utilisateurs d'utiliser une fonction afin de charger des résultats au changement de page
const emit = defineEmits<{ (event: 'update:currentPage'): void }>()

const getRowData = (row: DsfrTableProps['rows']) => {
  return Array.isArray(row) ? row : (row as unknown as DsfrTableRowProps).rowData
}

const currentPage = ref(props.currentPage)
const optionSelected = ref(props.resultsDisplayed)
const pageCount = ref(
  props.rows.length > optionSelected.value
    ? Math.ceil(props.rows.length / optionSelected.value)
    : 1,
)
const paginationOptions = [5, 10, 25, 50, 100]
const returnLowestLimit = () => currentPage.value * optionSelected.value - optionSelected.value
const returnHighestLimit = () => currentPage.value * optionSelected.value

watch(
  () => optionSelected.value,
  (newVal) => {
    pageCount.value =
      props.rows.length > optionSelected.value ? Math.ceil(props.rows.length / newVal) : 1
  },
)

const truncatedResults = computed(() => {
  if (props.pagination) {
    return props.rows.slice(returnLowestLimit(), returnHighestLimit())
  }

  return props.rows
})

const goFirstPage = () => {
  currentPage.value = 1
  emit('update:currentPage')
}
const goPreviousPage = () => {
  if (currentPage.value > 1) {
    currentPage.value -= 1
    emit('update:currentPage')
  }
}
const goNextPage = () => {
  if (currentPage.value < pageCount.value) {
    currentPage.value += 1
    emit('update:currentPage')
  }
}
const goLastPage = () => {
  currentPage.value = pageCount.value
  emit('update:currentPage')
}
</script>

<template>
  <div
    class="fr-table"
    :class="{ 'fr-table--no-caption': noCaption }"
  >
    <table>
      <caption class="caption">
        {{ title }}
      </caption>
      <thead>
        <!-- @slot Slot "header" pour les en-têtes du tableau. Sera dans `<thead>` -->
        <slot name="header">
          <DsfrTableHeaders
            v-if="headers && headers.length"
            :headers="headers"
          />
        </slot>
      </thead>
      <tbody>
        <!-- @slot Slot par défaut pour le corps du tableau. Sera dans `<tbody>` -->
        <slot />
        <template v-if="rows && rows.length">
          <DsfrTableRow
            v-for="(row, i) of truncatedResults"
            :key="
              rowKey && getRowData(row as string[][])
                ? typeof rowKey === 'string'
                  ? getRowData(row as string[][])![headers.indexOf(rowKey)].toString()
                  : rowKey(getRowData(row as string[][]))
                : i
            "
            :row-data="getRowData(row as string[][])"
            :row-attrs="'rowAttrs' in row ? row.rowAttrs : {}"
          />
        </template>
        <tr v-if="pagination">
          <td :colspan="headers.length">
            <div class="flex justify-right">
              <div class="self-center">
                <span>Résultats par page : </span>
                <select
                  v-model="optionSelected"
                  @change="emit('update:currentPage')"
                >
                  <option
                    v-for="(option, idx) in paginationOptions"
                    :key="idx"
                    :value="option"
                  >
                    {{ option }}
                  </option>
                </select>
              </div>
              <div class="flex ml-1">
                <span class="self-center">Page {{ currentPage }} sur {{ pageCount }}</span>
              </div>
              <div class="flex ml-1">
                <button
                  class="fr-icon-arrow-left-s-first-line"
                  @click="goFirstPage()"
                />
                <button
                  class="fr-icon-arrow-left-s-line"
                  @click="goPreviousPage()"
                />
                <button
                  class="fr-icon-arrow-right-s-line"
                  @click="goNextPage()"
                />
                <button
                  class="fr-icon-arrow-right-s-last-line"
                  @click="goLastPage()"
                />
              </div>
            </div>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<style scoped>
.flex {
  display: flex;
}

.justify-right {
  justify-content: right;
}

.ml-1 {
  margin-left: 1rem;
}

.self-center {
  align-self: center;
}
</style>
ts
import type { HTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from 'vue'

import type VIcon from '../VIcon/VIcon.vue'

export type DsfrTableRowProps = {
  rowData?: (string | Record<string, any>)[]
  rowAttrs?: HTMLAttributes
}

export type DsfrTableHeaderProps = {
  header?: string
  headerAttrs?: ThHTMLAttributes & { onClick?: (e: MouseEvent) => void }
  icon?: string | InstanceType<typeof VIcon>['$props']
}

export type DsfrTableHeadersProps = (string | (DsfrTableHeaderProps & { text?: string }))[]

export type DsfrTableCellProps = {
  field: string | Record<string, unknown>
  cellAttrs?: TdHTMLAttributes
  component?: string
  text?: string
  title?: string
  class?: string
  onClick?: Promise<void>
}

export type DsfrTableProps = {
  title: string
  headers?: DsfrTableHeadersProps
  rows?: (DsfrTableRowProps | (DsfrTableCellProps | { component: string, [k: string]: unknown } | string)[])[]

  rowKey?: ((row: (string | Record<string, any>)[] | undefined) => string | number | symbol | undefined) | string
  noCaption?: boolean
  pagination?: boolean
  currentPage?: number
  resultsDisplayed?: number
}

C'est tout, amis développeurs ! Avec DsfrTable, donnez vie à vos données comme jamais auparavant ! 🎉