Skip to content

Commit 4420c6a

Browse files
authored
feat(datatable): add customizable slots for sorting, checkboxes, and pagination
2 parents 9f5d218 + 1114896 commit 4420c6a

File tree

11 files changed

+423
-151
lines changed

11 files changed

+423
-151
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ dist
2424
.env
2525
.netlify
2626

27+
# Github
28+
.github/copilot-*
29+
2730
# Env
2831
.env
2932

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,53 @@ const dataTableInfos = {
7575
</CreatDatable>
7676
```
7777

78+
## Slots
79+
80+
### Sorting Icon Slot
81+
82+
You can customize the sorting icon using the `#sorting-icon` slot:
83+
84+
```html
85+
<CreatDatable id="creat-datatable" :infos="dataTableInfos">
86+
<template #sorting-icon="{ direction, headerId }">
87+
<span v-if="direction === 'asc'">↑</span>
88+
<span v-else-if="direction === 'desc'">↓</span>
89+
<span v-else>○</span>
90+
</template>
91+
</CreatDatable>
92+
```
93+
94+
### Checkbox Slots
95+
96+
You can customize the checkboxes in the header and cells using the `#checkbox-header` and `#checkbox-cell` slots:
97+
98+
```html
99+
<CreatDatable id="creat-datatable" :infos="dataTableInfos" :checkbox-config="{}">
100+
<template #checkbox-header="{ checked, toggleCheckbox }">
101+
<input type="checkbox" :checked="checked" @click="toggleCheckbox" />
102+
</template>
103+
<template #checkbox-cell="{ row, checked, toggleCheckbox }">
104+
<input type="checkbox" :checked="checked" @click="toggleCheckbox" />
105+
</template>
106+
</CreatDatable>
107+
```
108+
109+
### Pagination Slot
110+
111+
You can customize the pagination using the `#pagination` slot:
112+
113+
```html
114+
<CreatDatable id="creat-datatable" :infos="dataTableInfos" :pagination-config="{ itemsPerPage: 5 }">
115+
<template #pagination="{ currentPage, maxPage, changePage }">
116+
<div>
117+
<button @click="changePage(currentPage - 1)" :disabled="currentPage <= 1">Previous</button>
118+
<span>Page {{ currentPage }} of {{ maxPage }}</span>
119+
<button @click="changePage(currentPage + 1)" :disabled="currentPage >= maxPage">Next</button>
120+
</div>
121+
</template>
122+
</CreatDatable>
123+
```
124+
78125
## Style
79126

80127
To change th and td style

src/runtime/components/DataTable.vue

Lines changed: 100 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,63 @@
22
<div>
33
<table class="table" :class="tableClass">
44
<TableHeader
5-
:id="props.id"
6-
v-model:sort="sortModel"
7-
v-model:filters="filtersModel"
8-
v-model:checkbox="checkboxModel"
9-
:headers="props.infos.headers"
10-
:filters-class="props.filtersConfig?.class"
11-
:checkbox-config="props.checkboxConfig"
5+
:id="id"
6+
:headers="headers"
7+
:filters-class="filtersConfig?.class"
8+
:checkbox-config="checkboxConfig"
129
:table-data="tableData"
10+
:sort="sortValue"
11+
:filters="filtersValue"
12+
:checkbox="checkboxValue"
13+
@update:sort="setSort"
14+
@update:filters="setFilters"
15+
@update:checkbox="setCheckbox"
1316
>
1417
<template
15-
v-for="header in props.infos.headers"
18+
v-for="header in headersWithHeaderSlot"
1619
:key="`${id}-header-${header.id}`"
20+
#[headerSlotName(header.id)]="slotProps"
1721
>
18-
<slot :name="`header-${header.id}`" :data="header" />
22+
<slot :name="headerSlotName(header.id)" v-bind="slotProps" />
1923
</template>
2024
</TableHeader>
21-
<tbody v-if="tableData && tableData.length > 0">
25+
<tbody v-if="tableData.length > 0">
2226
<tr
2327
v-for="(data, index) in tableData"
2428
:key="`${id}-tr-${index}`"
2529
class="creat-datatable-row"
2630
>
27-
<td v-if="props.checkboxConfig">
28-
<input
29-
type="checkbox"
30-
:class="props.checkboxConfig.class"
31-
:checked="checkboxModel.includes(data)"
32-
@click="updateCheckbox(data)"
33-
/>
31+
<td v-if="checkboxConfig">
32+
<slot name="checkbox-cell" :row="data" :checked="checkboxValue.includes(data)" :toggle-checkbox="() => toggleCheckbox(data)">
33+
<input
34+
type="checkbox"
35+
:class="checkboxConfig.class"
36+
:checked="checkboxValue.includes(data)"
37+
@click="toggleCheckbox(data)"
38+
/>
39+
</slot>
3440
</td>
3541
<td
36-
v-for="header in props.infos.headers"
42+
v-for="header in headers"
3743
:key="`${id}-td-${header.id}`"
38-
:class="
39-
props.infos.content?.find((content) => content.id === header.id)
40-
?.tdClass
41-
"
44+
:class="contentClassMap.get(header.id)"
4245
>
4346
<slot v-if="slots[header.id]" :name="header.id" :data="data" />
4447
<span v-else>{{ data[header.id] }}</span>
4548
</td>
4649
</tr>
4750
</tbody>
48-
<TableEmpty v-else :headers-nb="props.infos.headers.length">
51+
<TableEmpty v-else :headers-nb="headers.length">
4952
<template #empty-state>
5053
<slot name="empty-state" />
5154
</template>
5255
</TableEmpty>
5356
</table>
5457
<TablePagination
55-
v-if="props.paginationConfig"
58+
v-if="paginationConfig"
5659
:current-page="paginationCurrentPage"
57-
:max-page="paginationMaxPage"
58-
:pagination-config="props.paginationConfig"
60+
:max-page="maxPage"
61+
:pagination-config="paginationConfig"
5962
@change-page="changePage"
6063
/>
6164
</div>
@@ -73,7 +76,9 @@ import {
7376
import TablePagination from "./TablePagination.vue";
7477
import TableEmpty from "./TableEmpty.vue";
7578
import TableHeader from "./TableHeader.vue";
76-
import { computed, ref, watch, useSlots } from "vue";
79+
import { computed, ref, watch, useSlots, toRef, toRefs } from "vue";
80+
import { useTableState } from "../composables/useTableState";
81+
import { useTableFiltering } from "../composables/useTableFiltering";
7782
7883
const slots = useSlots();
7984
@@ -88,55 +93,66 @@ const props = defineProps<{
8893
filtersConfig?: FiltersConfig;
8994
paginationConfig?: PaginationConfig;
9095
onPageChange?: (page: number) => void;
91-
checkboxConfig?: CheckboxConfig;
96+
checkboxConfig?: CheckboxConfig<T>;
9297
tableClass?: string;
9398
}>();
9499
95-
const emit = defineEmits(["update:sort", "update:filters", "update:checkbox"]);
100+
const emit = defineEmits<{
101+
"update:sort": [[string, SortDirection] | undefined];
102+
"update:filters": [{ [key: string]: string }];
103+
"update:checkbox": [T[]];
104+
}>();
96105
97-
// Sorting
98-
const sortModel = computed({
99-
get: () => props.sort,
100-
set: (value) => emit("update:sort", value),
101-
});
106+
const { id, infos, type, filtersConfig, paginationConfig, checkboxConfig } =
107+
toRefs(props);
102108
103-
// Filtering
104-
const filtersModel = computed({
105-
get: () => props.filters ?? {},
106-
set: (value) => emit("update:filters", value),
107-
});
108-
109-
const filteredData = computed(() => {
110-
if (props.type === "remote") {
111-
return props.infos.data;
112-
}
109+
const headers = computed(() => infos.value.headers);
110+
const rows = computed(() => infos.value.data ?? []);
111+
const content = computed(() => infos.value.content ?? []);
112+
const isRemote = computed(() => type.value === "remote");
113113
114-
return props.infos.data.filter((data: T) => {
115-
return props.infos.headers.every((header) => {
116-
if (!filtersModel.value[header.id]) {
117-
return true;
118-
}
114+
const headersWithHeaderSlot = computed(() => {
115+
const slotNames = new Set(Object.keys(slots));
116+
return headers.value.filter((header) =>
117+
slotNames.has(`header-${header.id}`)
118+
);
119+
});
119120
120-
const value = data[header.id];
121+
const { sortValue, filtersValue, checkboxValue, setSort, setFilters, setCheckbox } =
122+
useTableState({
123+
sort: toRef(props, 'sort'),
124+
filters: toRef(props, 'filters'),
125+
checkbox: toRef(props, 'checkbox'),
126+
onSortUpdate: (v) => emit("update:sort", v),
127+
onFiltersUpdate: (v) => emit("update:filters", v),
128+
onCheckboxUpdate: (v) => emit("update:checkbox", v),
129+
});
121130
122-
if (value == null || value.toString == null) {
123-
return false;
124-
}
131+
const { filteredData } = useTableFiltering(
132+
() => rows.value,
133+
() => headers.value,
134+
() => filtersValue.value,
135+
type.value
136+
);
125137
126-
return normalizeString(value.toString()).includes(
127-
normalizeString(filtersModel.value[header.id])
128-
);
129-
});
138+
const contentClassMap = computed(() => {
139+
const map = new Map<string, string | undefined>();
140+
content.value.forEach((c) => {
141+
if (c.tdClass) {
142+
map.set(c.id, c.tdClass);
143+
}
130144
});
145+
return map;
131146
});
132147
133148
// Pagination
134-
const ITEMS_PER_PAGE = props.paginationConfig?.itemsPerPage ?? 5;
149+
const INITIAL_PAGE = 1;
150+
const itemsPerPage = computed(() => paginationConfig.value?.itemsPerPage ?? 5);
135151
136-
const paginationCurrentPage = ref(1);
152+
const paginationCurrentPage = ref(INITIAL_PAGE);
137153
138154
watch(
139-
() => props.paginationConfig?.currentPage,
155+
() => paginationConfig.value?.currentPage,
140156
(newCurrentPage) => {
141157
if (newCurrentPage) {
142158
paginationCurrentPage.value = newCurrentPage;
@@ -145,73 +161,52 @@ watch(
145161
);
146162
147163
const maxPage = computed(() => {
148-
if (props.paginationConfig?.nbItems) {
149-
return Math.ceil(props.paginationConfig.nbItems / ITEMS_PER_PAGE);
150-
} else {
151-
return Math.ceil(filteredData.value.length / ITEMS_PER_PAGE);
152-
}
164+
const total = paginationConfig.value?.nbItems ?? filteredData.value.length;
165+
return Math.ceil(total / itemsPerPage.value) || 1;
153166
});
154167
155-
const paginationMaxPage = ref<number>(maxPage.value);
156-
157-
watch(
158-
() => props.paginationConfig?.nbItems,
159-
(newNbItems) => {
160-
if (newNbItems) {
161-
paginationMaxPage.value = Math.ceil(newNbItems / ITEMS_PER_PAGE);
162-
}
168+
watch([filteredData, () => paginationConfig.value?.nbItems], () => {
169+
if (paginationCurrentPage.value > maxPage.value) {
170+
paginationCurrentPage.value = INITIAL_PAGE;
163171
}
164-
);
165-
166-
watch(filteredData, () => {
167-
paginationMaxPage.value = maxPage.value;
168172
});
169173
170174
function changePage(page: number) {
171-
if (props.type === "remote") {
175+
if (isRemote.value) {
172176
props.onPageChange?.(page);
173177
} else {
174178
paginationCurrentPage.value = page;
175179
}
176180
}
177181
178-
// Checkbox
179-
const checkboxModel = computed({
180-
get: () => props.checkbox ?? [],
181-
set: (value) => emit("update:checkbox", value),
182-
});
182+
function toggleCheckbox(row: T) {
183+
const idKey = checkboxConfig.value?.idKey as keyof T | undefined;
184+
const isSelected = idKey
185+
? checkboxValue.value.some((r) => r[idKey] === row[idKey])
186+
: checkboxValue.value.includes(row);
183187
184-
function updateCheckbox(data: T) {
185-
if (!checkboxModel.value.includes(data)) {
186-
checkboxModel.value.push(data);
188+
if (!isSelected) {
189+
setCheckbox([...checkboxValue.value, row]);
187190
} else {
188-
const index = checkboxModel.value.indexOf(data);
189-
if (index !== -1) {
190-
checkboxModel.value.splice(index, 1);
191-
}
191+
setCheckbox(
192+
idKey
193+
? checkboxValue.value.filter((r) => r[idKey] !== row[idKey])
194+
: checkboxValue.value.filter((r) => r !== row)
195+
);
192196
}
193197
}
194198
195-
// Table data
196199
const tableData = computed(() => {
197-
let data = filteredData.value;
200+
let pageRows = filteredData.value;
198201
199-
if (props.type !== "remote" && props.paginationConfig) {
200-
const start = (paginationCurrentPage.value - 1) * ITEMS_PER_PAGE;
201-
const end = start + ITEMS_PER_PAGE;
202-
203-
data = data.slice(start, end);
202+
if (!isRemote.value && paginationConfig.value) {
203+
const start = (paginationCurrentPage.value - 1) * itemsPerPage.value;
204+
pageRows = pageRows.slice(start, start + itemsPerPage.value);
204205
}
205206
206-
return data;
207+
return pageRows;
207208
});
208209
209-
function normalizeString(string: string) {
210-
return string
211-
.normalize("NFD")
212-
.replace(/[\u0300-\u036f]/g, "")
213-
.toLowerCase();
214-
}
210+
const headerSlotName = (headerId: string) => `header-${headerId}`;
215211
</script>
216212

217-
<style scoped></style>

src/runtime/components/TableEmpty.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</tbody>
1010
</template>
1111

12-
<script setup lang="ts" generic="T">
12+
<script setup lang="ts">
1313
import { useSlots } from "vue";
1414
1515
const props = defineProps<{

0 commit comments

Comments
 (0)