Aller au contenu principal

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 :

src/core/components/table/BaseTable.vue - Différences pour le scroll infini
<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 @scroll pour détecter quand charger plus de données.
  • Le header du tableau devient sticky pour 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

src/core/components/table/BaseTable.vue
const props = defineProps({
// ... props existantes ...

infiniteScroll: {
type: Boolean,
default: false,
},
endMessage: {
type: String,
default: "Vous avez atteint la fin de la liste",
},
});
PropTypeDéfautDescription
infiniteScrollBooleanfalseActive le mode scroll infini
endMessageString"Vous avez atteint la fin de la liste"Message affiché à la fin

Nouvelles variables réactives

src/core/components/table/BaseTable.vue
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 :

src/core/components/table/BaseTable.vue
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;
}
};
Comportement du paramètre reset

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

src/core/components/table/BaseTable.vue
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 :

src/core/components/table/BaseTable.vue
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

src/core/components/table/BaseTable.vue
.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 :

src/modules/connected/customer/views/CustomersInfiniteView.vue
<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 :

AspectPaginationScroll infini
Props supplémentairesAucune:infinite-scroll="true"
Affichage du loaderOverlay sur tout le tableauLigne en bas du tableau
NavigationBoutons de pageDéfilement automatique
Footer"Total: 42 clients""42 / 150 clients"
Même service, même fonction fetchData

Le service et la fonction fetchData sont identiques entre les deux modes. Seule la prop infinite-scroll change le comportement du composant.