Skip to content

Commit 89ff17f

Browse files
committed
feat: ✨ ajoute le composant DsfrDataTable
1 parent bfda45d commit 89ff17f

File tree

8 files changed

+447
-17
lines changed

8 files changed

+447
-17
lines changed

.vitepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ const composants = [
147147
text: 'DsfrConsent',
148148
link: '/composants/DsfrConsent.md',
149149
},
150+
{
151+
text: 'DsfrDataTable',
152+
link: '/composants/DsfrDataTable.md',
153+
},
150154
{
151155
text: 'DsfrErrorPage',
152156
link: '/composants/DsfrErrorPage.md',
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Tableau - `DsfrDataTable`
2+
3+
## 🌟 Introduction
4+
5+
Le composant `DsfrDataTable` 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 !
6+
7+
🏅 La documentation sur le tableau sur le [DSFR](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/tableau/)
8+
9+
<VIcon name="vi-file-type-storybook" /> La story sur le tableau sur le storybook de [VueDsfr](https://storybook.vue-ds.fr/?path=/docs/composants-dsfrdatatable--docs)
10+
11+
## Props 🛠️
12+
13+
| Nom | Type | Défaut | Obligatoire | Description |
14+
|-------------------|------------------------------------------------|-----------|-------------|-----------------------------------------------------------------------------------------------------|
15+
| `title` | `string` | || Les en-têtes de votre tableau. |
16+
| `headers` | `Array<string>` | `[]` | | Les en-têtes de votre tableau. |
17+
| `rows` | `Array<DsfrDataTableRowProps \| string[] \| DsfrDataTableCellProps[]>` | `[]` | | Les données de chaque rangée dans le tableau. |
18+
| `rowKey` | `string \| Function` | `undefined`| | Une clé unique pour chaque rangée, utilisée pour optimiser la mise à jour du DOM. |
19+
| `currentPage` | `number` | `1` | | La page actuelle dans la pagination du tableau. |
20+
| `resultsDisplayed`| `number` | `10` | | Le nombre de résultats affichés par page dans la pagination. |
21+
22+
## Events 📡
23+
24+
| Nom | Description |
25+
|----------------------|-------------------------------------------------|
26+
| `update:currentPage` | Émis lors du changement de la page actuelle. |
27+
28+
## 🧩 Slots
29+
30+
- **`header`**: Ce slot permet de personnaliser les en-têtes du tableau. Par défaut, il utilise [`DsfrDataTableHeaders`](./DsfrDataTableHeader.md) avec les props `headers`.
31+
- **Slot par défaut**: Utilisé pour le corps du tableau. Par défaut, il affiche les rangées de données via `DsfrDataTableRow`.
32+
33+
## Exemples 📝
34+
35+
### Exemple Basique
36+
37+
::: code-group
38+
39+
<Story data-title="Démo basique" min-h="260px">
40+
<div class="fr-container">
41+
<DsfrDataTableDemoSimple />
42+
</div>
43+
</Story>
44+
45+
<<< ./docs-demo/DsfrDataTableDemoSimple.vue
46+
47+
:::
48+
49+
### Exemple Complexe
50+
51+
::: code-group
52+
53+
<Story data-title="Démo complexe" min-h="300px">
54+
<div class="fr-container">
55+
<DsfrDataTableDemoComplexe />
56+
</div>
57+
</Story>
58+
59+
<<< ./docs-demo/DsfrDataTableDemoComplexe.vue
60+
61+
:::
62+
63+
### Exemple Plus Complexe
64+
65+
::: code-group
66+
67+
<Story data-title="Démo complexe" min-h="400px">
68+
<div class="fr-container">
69+
<DsfrDataTableDemoPlusComplexe />
70+
</div>
71+
</Story>
72+
73+
<<< ./docs-demo/DsfrDataTableDemoPlusComplexe.vue
74+
75+
:::
76+
77+
## ⚙️ Code source du composant
78+
79+
::: code-group
80+
81+
<<< DsfrDataTable.vue
82+
<<< DsfrDataTable.types.ts
83+
84+
:::
85+
86+
C'est tout, amis développeurs ! Avec DsfrDataTable, donnez vie à vos données comme jamais auparavant ! 🎉
87+
88+
<script setup lang="ts">
89+
import DsfrDataTableDemoSimple from './docs-demo/DsfrDataTableDemoSimple.vue'
90+
import DsfrDataTableDemoComplexe from './docs-demo/DsfrDataTableDemoComplexe.vue'
91+
import DsfrDataTableDemoPlusComplexe from './docs-demo/DsfrDataTableDemoPlusComplexe.vue'
92+
</script>
Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1+
import type { Page } from '../DsfrPagination/DsfrPagination.types'
2+
3+
export type DsfrDataTableRow = (string | number | boolean | bigint | symbol)[]
4+
| Record<string | symbol | number, unknown>
5+
16
export type DsfrDataTableProps = {
7+
id?: string
28
title: string
3-
headersRow: string[]
4-
contentRows: string[]
5-
headersColumn?: string[]
6-
selectableRows?: string[]
9+
rowKey?: string | number
10+
headersRow: (string | { key: string, label: string, headerAttrs?: Record<string, unknown> })[]
11+
rows: DsfrDataTableRow[]
712
topActionsRow?: string[]
813
bottomActionsRow?: string[]
14+
selectableRows?: boolean
915
verticalBorders?: boolean
1016
bottomCaption?: boolean
1117
noCaption?: boolean
18+
pages?: Page[]
1219
pagination?: boolean
20+
paginationOptions?: number[]
1321
currentPage?: number
14-
resultsPerPage?: number
22+
rowsPerPage?: number
1523
}
Lines changed: 177 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,191 @@
11
<script lang="ts" setup>
2+
import { computed } from 'vue'
3+
4+
import { getRandomId } from '@/utils/random-utils'
5+
import DsfrPagination, { type Page } from '../DsfrPagination/DsfrPagination.vue'
26
import type { DsfrDataTableProps } from './DsfrDataTable.types'
37
48
const props = withDefaults(defineProps<DsfrDataTableProps>(), {
5-
headersRow: () => [],
6-
contentRows: () => [],
7-
headersColumn: () => [],
8-
selectableRows: () => [],
9+
id: () => getRandomId('table'),
910
topActionsRow: () => [],
1011
bottomActionsRow: () => [],
11-
currentPage: 1,
12-
resultsPerPage: 10,
12+
currentPage: 0,
13+
rowsPerPage: 10,
14+
paginationOptions: () => [
15+
5,
16+
10,
17+
20,
18+
],
19+
})
20+
21+
const emit = defineEmits<{
22+
'update:current-page': [page: number]
23+
}>()
24+
25+
const selection = defineModel<string[]>('selection')
26+
const rowsPerPage = defineModel<number>('rowsPerPage', { default: 10 })
27+
const currentPage = defineModel<number>('currentPage', { default: 1 })
28+
const pageCount = computed(() => Math.ceil(props.rows.length / rowsPerPage.value))
29+
const pages = computed<Page[]>(() => props.pages ?? Array.from({ length: pageCount.value }).map((x, i) => ({ label: `${i + 1}`, title: `Page ${i + 1}`, href: `#${i + 1}` })))
30+
31+
const lowestLimit = computed(() => currentPage.value * rowsPerPage.value)
32+
const highestLimit = computed(() => (currentPage.value + 1) * rowsPerPage.value)
33+
34+
const finalRows = computed(() => {
35+
const rowKeys = props.headersRow.map((header) => {
36+
if (typeof header !== 'object') {
37+
return header
38+
}
39+
return header.key
40+
})
41+
42+
const rows = props.rows.map((row) => {
43+
if (Array.isArray(row)) {
44+
return row
45+
}
46+
return rowKeys.map(key => typeof row !== 'object' ? row : row[key] ?? row)
47+
})
48+
49+
if (props.pagination) {
50+
return rows.slice(lowestLimit.value, highestLimit.value)
51+
}
52+
53+
return rows
1354
})
1455
</script>
1556

1657
<template>
58+
<div
59+
class="fr-table"
60+
>
61+
<div class="fr-table__wrapper">
62+
<div class="fr-table__container">
63+
<div class="fr-table__content">
64+
<table :id="id">
65+
<caption>
66+
{{ title }}
67+
</caption>
68+
<thead>
69+
<tr>
70+
<th
71+
v-if="selectableRows"
72+
class="fr-cell--fixed"
73+
role="columnheader"
74+
>
75+
<span class="fr-sr-only">Sélectionner</span>
76+
</th>
77+
<th
78+
v-for="header of headersRow"
79+
:key="typeof header === 'object' ? header.key : header"
80+
scope="col"
81+
v-bind="typeof header === 'object' && header.headerAttrs"
82+
>
83+
<slot
84+
name="header"
85+
v-bind="typeof header === 'object' ? header : { key: header, label: header }"
86+
>
87+
{{ typeof header === 'object' ? header.label : header }}
88+
</slot>
89+
</th>
90+
</tr>
91+
</thead>
92+
<tbody>
93+
<tr
94+
v-for="(row, idx) of finalRows"
95+
:key="`row-${idx}`"
96+
:data-row-key="idx + 1"
97+
>
98+
<th
99+
v-if="selectableRows"
100+
class="fr-cell--fixed"
101+
role="columnheader"
102+
>
103+
<div class="fr-checkbox-group fr-checkbox-group--sm">
104+
<!-- @vue-expect-error TS2538 -->
105+
<input
106+
id="table-select-checkbox-7748--0"
107+
v-model="selection"
108+
:value="row[rowKey] ?? `row-${idx}`"
109+
type="checkbox"
110+
>
111+
<label
112+
class="fr-label"
113+
for="table-select-checkbox-7748--0"
114+
>
115+
Sélectionner la ligne {{ idx + 1 }}
116+
</label>
117+
</div>
118+
</th>
119+
120+
<!-- @vue-expect-error TS2538 -->
121+
<td
122+
v-for="(cell, cellIdx) of row"
123+
:key="typeof cell === 'object' ? cell[rowKey] : cell"
124+
>
125+
<slot
126+
name="cell"
127+
v-bind="{
128+
colKey: typeof headersRow[cellIdx] === 'object'
129+
? headersRow[cellIdx].key
130+
: headersRow[cellIdx],
131+
cell,
132+
}"
133+
>
134+
<!-- @vue-expect-error TS2538 -->
135+
{{ typeof cell === 'object' ? cell[rowKey] : cell }}
136+
</slot>
137+
</td>
138+
</tr>
139+
</tbody>
140+
</table>
141+
</div>
142+
</div>
143+
</div>
144+
<slot name="pagination">
145+
<template
146+
v-if="pagination"
147+
>
148+
<div class="flex">
149+
<div class="fr-select-group flex">
150+
<label class="fr-label">Résultats par page : </label>
151+
<select
152+
v-model="rowsPerPage"
153+
class="fr-select"
154+
@change="emit('update:current-page', 0)"
155+
>
156+
<option
157+
value=""
158+
:selected="!paginationOptions.includes(rowsPerPage)"
159+
disabled="true"
160+
hidden="hidden"
161+
>
162+
Sélectionner une option
163+
</option>
164+
<option
165+
v-for="(option, idx) in paginationOptions"
166+
:key="idx"
167+
:value="option"
168+
:selected="+option === rowsPerPage"
169+
>
170+
{{ option }}
171+
</option>
172+
</select>
173+
</div>
174+
<div class="flex ml-1">
175+
<span class="self-center">Page {{ currentPage + 1 }} sur {{ pageCount }}</span>
176+
</div>
177+
<DsfrPagination
178+
v-model:current-page="currentPage"
179+
:pages="pages"
180+
/>
181+
</div>
182+
</template>
183+
</slot>
184+
</div>
17185
</template>
18186

19187
<style scoped>
20-
188+
.flex {
189+
display: flex;
190+
}
21191
</style>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<script lang="ts" setup>
2+
import { ref } from 'vue'
3+
4+
import DsfrDataTable from '../DsfrDataTable.vue'
5+
import type { DsfrDataTableProps } from '../DsfrDataTable.types'
6+
7+
const headers: DsfrDataTableProps['headersRow'] = [
8+
{
9+
key: 'id',
10+
label: 'ID',
11+
},
12+
{
13+
key: 'name',
14+
label: 'Name',
15+
},
16+
{
17+
key: 'email',
18+
label: 'Email',
19+
},
20+
]
21+
22+
const rows = [
23+
[1, 'John Doe', 'john.doe@gmail.com'],
24+
[2, 'Jane Doe', 'jane.doe@gmail.com'],
25+
[3, 'James Bond', 'james.bond@mi6.gov.uk'],
26+
]
27+
28+
const click = (event: MouseEvent, key: string) => {
29+
console.warn(event, key)
30+
}
31+
32+
const selection = ref<string[]>([])
33+
</script>
34+
35+
<template>
36+
<div class="fr-container fr-my-2v">
37+
<DsfrDataTable
38+
v-model:selection="selection"
39+
title="Titre du tableau (caption)"
40+
:headers-row="headers"
41+
:rows="rows"
42+
selectable-rows
43+
:row-key="0"
44+
>
45+
<template #header="{ key, label }">
46+
<div @click="click($event, key)">
47+
<em>{{ label }}</em>
48+
</div>
49+
</template>
50+
51+
<template #cell="{ colKey, cell }">
52+
<template v-if="colKey === 'email'">
53+
<a :href="`mailto:${cell as string}`">{{ cell }}</a>
54+
</template>
55+
<template v-else>
56+
{{ cell }} <em>({{ colKey }})</em>
57+
</template>
58+
</template>
59+
</DsfrDataTable>
60+
IDs sélectionnées : {{ selection }}
61+
</div>
62+
</template>

0 commit comments

Comments
 (0)