Activer le scroll infini
Le composant BaseTable supporte également un mode scroll infini qui charge automatiquement les données suivantes lorsque l'utilisateur atteint le bas de la liste. Cette option est idéale pour les interfaces mobiles ou les listes longues parcourues séquentiellement.
Modifications du template
Pour supporter le scroll infini, le template du composant BaseTable est enrichi avec :
<template>
<div class="base-table">
<div
ref="scrollContainer"
class="table-container"
:class="{ 'infinite-scroll-container': infiniteScroll }"
:style="scrollContainerStyle"
@scroll="handleScroll"
>
<table class="table">
<thead ref="theadRef">
<!-- ... colonnes (identique) ... -->
</thead>
<tbody ref="tbodyRef">
<!-- ... lignes de données (identique) ... -->
<!-- Loader en bas du tableau en mode scroll infini -->
<tr v-if="infiniteScroll && loading">
<td :colspan="enhancedColumns.length" class="loading-row">
<BaseLoader />
</td>
</tr>
<!-- Message de fin de liste -->
<tr v-if="infiniteScroll && !hasMore && items.length > 0">
<td :colspan="enhancedColumns.length" class="end-message">
{{ endMessage }}
</td>
</tr>
</tbody>
</table>
<!-- Loader overlay uniquement en mode pagination -->
<div v-if="!infiniteScroll && loading" class="loading-overlay">
<BaseLoader />
</div>
</div>
<!-- Pagination classique OU footer scroll infini -->
<TablePagination
v-if="!infiniteScroll"
:current-page="currentPage"
:total-items="totalItems"
:entity-name="entityName"
:items-per-page="itemsPerPage"
@page-change="goToPage"
/>
<div v-else class="infinite-footer">
<span class="total-info">
{{ items.length }} / {{ totalItems }} {{ entityName }}
</span>
</div>
</div>
</template>
Points clés :
- Le conteneur de scroll écoute l'événement
@scrollpour détecter quand charger plus de données. - Le header du tableau devient
stickypour rester visible pendant le défilement. - Un message personnalisable (
endMessage) s'affiche quand toutes les données sont chargées. - Le footer affiche la progression (
42 / 150 clients).
Nouvelles props pour le scroll infini
const props = defineProps({
// ... props existantes ...
infiniteScroll: {
type: Boolean,
default: false,
},
endMessage: {
type: String,
default: "Vous avez atteint la fin de la liste",
},
});
| Prop | Type | Défaut | Description |
|---|---|---|---|
infiniteScroll | Boolean | false | Active le mode scroll infini |
endMessage | String | "Vous avez atteint la fin de la liste" | Message affiché à la fin |
Nouvelles variables réactives
const scrollContainer = ref<HTMLElement | null>(null);
const theadRef = ref<HTMLElement | null>(null);
const tbodyRef = ref<HTMLElement | null>(null);
const calculatedMaxHeight = ref<number | null>(null);
const scrollContainerStyle = computed(() => {
if (!props.infiniteScroll || !calculatedMaxHeight.value) return {};
return {
maxHeight: `${calculatedMaxHeight.value}px`,
};
});
const calculateScrollHeight = () => {
if (!props.infiniteScroll) return;
if (!tbodyRef.value || !theadRef.value) return;
const tbodyHeight = tbodyRef.value.getBoundingClientRect().height;
const theadHeight = theadRef.value.getBoundingClientRect().height;
// Hauteur max = thead + tbody pour déclencher le scroll
const maxHeight = theadHeight + tbodyHeight;
calculatedMaxHeight.value = Math.max(maxHeight, 100); // Minimum 100px
};
const hasMore = computed(() => {
return items.value.length < totalItems.value;
});
Fonction de chargement adaptée
La fonction loadData est modifiée pour gérer l'accumulation des données en mode scroll infini :
const loadData = async (reset: boolean = false) => {
if (loading.value) return;
if (props.infiniteScroll && !reset && !hasMore.value) return;
loading.value = true;
try {
// Reset si demandé (changement de filtre/tri)
if (props.infiniteScroll && reset) {
currentPage.value = 1;
items.value = [];
}
const activeFilters = Object.entries(filters.value).reduce(
(acc, [key, value]) => {
if (value !== "" && value !== null && value !== undefined) {
acc[key] = value;
}
return acc;
},
{} as Record<string, unknown>
);
const params: TableQueryParams & Record<string, unknown> = {
page: currentPage.value,
itemsPerPage: props.itemsPerPage,
...activeFilters,
...(sortOrders.value.length > 0 && { orders: sortOrders.value }),
};
const response = await props.fetchData(params);
if (props.infiniteScroll) {
// Mode scroll infini : accumule les données
if (reset) {
items.value = response.items;
} else {
items.value = [...items.value, ...response.items];
}
currentPage.value++;
} else {
// Mode pagination : remplace les données
items.value = response.items;
}
totalItems.value = response.totalItems;
if (response.search?.mapping) {
enhancedColumns.value =
enhanceColumnsFromApi(
items.value,
response.search.mapping,
props.columns
) ?? props.columns;
}
// Calculer la hauteur du scroll après le premier chargement
if (props.infiniteScroll && !calculatedMaxHeight.value) {
// Attendre le prochain tick pour que le DOM soit mis à jour
setTimeout(() => calculateScrollHeight(), 0);
}
} catch (error) {
console.error("Erreur lors du chargement des données:", error);
} finally {
loading.value = false;
}
};
En mode scroll infini, le paramètre reset permet de réinitialiser la liste. Il doit être passé à true lors d'un changement de filtre ou de tri pour repartir de zéro.
Détection du scroll pour charger plus
const handleScroll = () => {
if (!props.infiniteScroll) return;
if (!scrollContainer.value || loading.value || !hasMore.value) return;
const { scrollTop, scrollHeight, clientHeight } = scrollContainer.value;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
// Charge plus quand on est à moins de 50px du bas
if (distanceFromBottom < 50) {
loadData();
}
};
Adaptation du tri et des filtres
Le tri et les filtres doivent réinitialiser la liste en mode scroll infini :
const toggleSort = (field: string) => {
// ... logique de tri existante ...
currentPage.value = 1;
loadData(props.infiniteScroll); // reset = true en mode scroll infini
};
const onFilterChange = () => {
clearTimeout(filterTimeout);
filterTimeout = window.setTimeout(() => {
currentPage.value = 1;
loadData(props.infiniteScroll); // reset = true en mode scroll infini
}, 500);
};
onMounted(() => {
loadData(props.infiniteScroll); // reset = true en mode scroll infini
});
Styles CSS pour le scroll infini
.infinite-scroll-container .table thead {
position: sticky;
top: 0;
z-index: 5;
}
.infinite-scroll-container .table thead th {
border-bottom: 0;
}
.infinite-scroll-container {
overflow-y: auto;
}
.loading-row {
text-align: center !important;
padding: 20px;
}
.end-message {
text-align: center !important;
padding: 20px;
color: #999;
font-size: 14px;
}
.infinite-footer {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 12px 16px;
border-top: 1px solid #e0e0e0;
background: white;
}
.total-info {
color: #666;
font-size: 14px;
}
Exemple d'utilisation : Customer (scroll infini)
Voici comment utiliser le même composant BaseTable en mode scroll infini :
<template>
<BaseTable
:columns="columns"
:fetch-data="fetchCustomers"
entity-name="clients"
:items-per-page="5"
:infinite-scroll="true"
end-message="Vous avez vu tous les clients"
/>
</template>
<script setup lang="ts">
import BaseTable from "@/core/components/table/BaseTable.vue";
import customerService from "@/modules/connected/customer/services/customerService";
import { TableColumn } from "@/core/models/table/tableColumn";
import { TableQueryParams } from "@/core/models/table/tableQueryParams";
defineOptions({
name: "CustomersInfiniteView",
});
const columns: TableColumn[] = [
{ field: "id", label: "ID" },
{ field: "fullName", label: "Nom complet" },
{ field: "age", label: "Âge" },
{ field: "city", label: "Ville" },
];
const fetchCustomers = async (params: TableQueryParams) => {
const response = await customerService.getCustomers(params);
return {
items: response.member,
totalItems: response.totalItems,
search: response.search,
};
};
</script>
Différences avec le mode pagination :
| Aspect | Pagination | Scroll infini |
|---|---|---|
| Props supplémentaires | Aucune | :infinite-scroll="true" |
| Affichage du loader | Overlay sur tout le tableau | Ligne en bas du tableau |
| Navigation | Boutons de page | Défilement automatique |
| Footer | "Total: 42 clients" | "42 / 150 clients" |
Le service et la fonction fetchData sont identiques entre les deux modes. Seule la prop infinite-scroll change le comportement du composant.