Module 10 – Eloquent, listes et pagination
Niveau 5.2 – Laravel Inertia (React & Vue)
Objectif
À la fin de ce module vous saurez :
- Afficher des listes issues d’Eloquent (utilisateurs, articles, etc.) avec pagination Laravel dans une page Inertia.
- Passer la collection paginée et les filtres (recherche, tri) en props ; gérer les paramètres de requête (query string) pour que « Page 2 », « Rechercher », « Trier par » déclenchent une nouvelle visite avec les bons paramètres.
- Afficher les liens de pagination (précédent / suivant ou numéros de page) avec le composant Link et preserveScroll.
- Construire un composant Pagination réutilisable en React et en Vue à partir de l’objet links renvoyé par Laravel.
Nous donnons un exemple complet : liste d’utilisateurs avec recherche, tri et pagination (Laravel + React et Vue).
Structure de la réponse paginée Laravel
Quand vous faites User::paginate(15) (ou ->paginate($request->get('per_page', 15))), Laravel renvoie un objet LengthAwarePaginator qui est sérialisé en JSON pour Inertia. Côté client, vous recevez un objet avec notamment :
- data : tableau des éléments de la page courante (ex. 15 utilisateurs).
- current_page : numéro de la page (1, 2, 3, …).
- last_page : nombre total de pages.
- per_page : nombre d’éléments par page.
- total : nombre total d’éléments.
- first_page_url, last_page_url, prev_page_url, next_page_url : URLs prêtes à l’emploi.
- links : tableau d’objets (label, url, active) pour chaque « lien » (Précédent, 1, 2, 3, Suivant).
Vous utilisez users.data pour boucler sur les éléments et users.links (ou prev_page_url / next_page_url) pour les liens de pagination.
Côté Laravel – contrôleur avec pagination, recherche et tri
app/Http/Controllers/UserController.php :
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Inertia\Inertia;
class UserController extends Controller
{
public function index(Request $request)
{
$users = User::query()
->when($request->search, function ($query, $search) {
$query->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
})
->when($request->sort, function ($query, $sort) use ($request) {
$order = $request->get('order', 'asc');
$query->orderBy($sort, $order);
}, function ($query) {
$query->orderBy('name', 'asc');
})
->paginate($request->get('per_page', 10))
->withQueryString();
return Inertia::render('Users/Index', [
'users' => $users,
'filters' => $request->only(['search', 'sort', 'order']),
]);
}
}
- when($request->search, ...) : si search est présent dans la requête (query string), on filtre par nom ou email. when($request->sort, ...) : si sort est présent, on trie par ce champ avec order (asc/desc) ; sinon ordre par défaut (name asc).
- paginate(...) : pagination Laravel. withQueryString() : conserve les paramètres (search, sort, order) dans les URLs des liens de pagination (sinon en cliquant sur « Page 2 » on perdrait le filtre de recherche).
Les filters sont renvoyés en prop pour que le formulaire de recherche et les selects de tri puissent afficher les valeurs courantes (champ pré-rempli, option sélectionnée).
Page liste avec recherche, tri et pagination – React
resources/js/Pages/Users/Index.jsx :
import React, { useState } from 'react';
import { Link, router } from '@inertiajs/react';
export default function Index({ users, filters }) {
const [search, setSearch] = useState(filters?.search ?? '');
const [sort, setSort] = useState(filters?.sort ?? 'name');
const [order, setOrder] = useState(filters?.order ?? 'asc');
const applyFilters = () => {
router.get(route('users.index'), {
search: search || undefined,
sort,
order,
}, { preserveState: true });
};
return (
<div>
<h1>Utilisateurs</h1>
{/* Filtres */}
<div className="flex gap-4 mb-6">
<input
type="text"
placeholder="Rechercher..."
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && applyFilters()}
className="rounded border-gray-300"
/>
<select
value={sort}
onChange={(e) => setSort(e.target.value)}
className="rounded border-gray-300"
>
<option value="name">Nom</option>
<option value="email">Email</option>
<option value="created_at">Date</option>
</select>
<select
value={order}
onChange={(e) => setOrder(e.target.value)}
className="rounded border-gray-300"
>
<option value="asc">Ascendant</option>
<option value="desc">Descendant</option>
</select>
<button type="button" onClick={applyFilters}>
Appliquer
</button>
</div>
{/* Liste */}
<ul className="space-y-2">
{users.data.map((user) => (
<li key={user.id}>
<Link href={route('users.show', user.id)}>
{user.name} – {user.email}
</Link>
</li>
))}
</ul>
{/* Pagination */}
<nav className="mt-6 flex gap-2">
{users.links.map((link, i) => (
<Link
key={i}
href={link.url}
preserveScroll
className={link.active ? 'font-bold' : ''}
dangerouslySetInnerHTML={{ __html: link.label }}
/>
))}
</nav>
</div>
);
}
- useState pour search, sort, order : état local pour les champs de filtre. On les initialise avec filters (props) pour que l’affichage reflète l’état courant après une requête.
- applyFilters :
router.get(route('users.index'), { search, sort, order })envoie une nouvelle requête ; Laravel renvoie une nouvelle page avec users et filters à jour. preserveState: true peut aider à garder l’état du formulaire si besoin. - users.links : tableau Laravel ; chaque élément a url, label (peut être du HTML comme « « Previous »), active. On utilise Link avec
href={link.url}et preserveScroll pour ne pas remonter en haut. Pour le label, si c’est du HTML, en React on utilise dangerouslySetInnerHTML (à utiliser avec précaution ; si label est du texte simple, afficherlink.labeldirectement).
Variante pagination simple (Précédent / Suivant) :
{users.prev_page_url && (
<Link href={users.prev_page_url} preserveScroll>Précédent</Link>
)}
<span>Page {users.current_page} / {users.last_page}</span>
{users.next_page_url && (
<Link href={users.next_page_url} preserveScroll>Suivant</Link>
)}
Page liste – Vue (même logique)
resources/js/Pages/Users/Index.vue :
<template>
<div>
<h1>Utilisateurs</h1>
<div class="flex gap-4 mb-6">
<input
v-model="search"
type="text"
placeholder="Rechercher..."
class="rounded border-gray-300"
@keydown.enter="applyFilters"
/>
<select v-model="sort" class="rounded border-gray-300">
<option value="name">Nom</option>
<option value="email">Email</option>
<option value="created_at">Date</option>
</select>
<select v-model="order" class="rounded border-gray-300">
<option value="asc">Ascendant</option>
<option value="desc">Descendant</option>
</select>
<button type="button" @click="applyFilters">Appliquer</button>
</div>
<ul class="space-y-2">
<li v-for="user in users.data" :key="user.id">
<Link :href="route('users.show', user.id)">
{{ user.name }} – {{ user.email }}
</Link>
</li>
</ul>
<nav class="mt-6 flex gap-2">
<Link
v-for="(link, i) in users.links"
:key="i"
:href="link.url"
preserve-scroll
:class="{ 'font-bold': link.active }"
v-html="link.label"
/>
</nav>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { Link, router } from '@inertiajs/vue3';
const props = defineProps({
users: Object,
filters: Object,
});
const search = ref(props.filters?.search ?? '');
const sort = ref(props.filters?.sort ?? 'name');
const order = ref(props.filters?.order ?? 'asc');
function applyFilters() {
router.get(route('users.index'), {
search: search.value || undefined,
sort: sort.value,
order: order.value,
}, { preserveState: true });
}
</script>
- v-model sur search, sort, order : liaison bidirectionnelle avec l’état local.
- users.links : même structure qu’en React ; Link avec
:href="link.url"et preserve-scroll.v-html="link.label"si le label contient du HTML (ex. flèches).
Composant Pagination réutilisable (React)
Pour éviter de dupliquer le bloc links dans chaque page, vous pouvez créer un composant Pagination :
resources/js/Components/Pagination.jsx :
import React from 'react';
import { Link } from '@inertiajs/react';
export default function Pagination({ links }) {
if (!links || links.length <= 1) return null;
return (
<nav className="flex gap-2" aria-label="Pagination">
{links.map((link, i) => (
<Link
key={i}
href={link.url}
preserveScroll
className={`px-3 py-1 rounded ${
link.active
? 'bg-indigo-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
} ${!link.url ? 'opacity-50 cursor-not-allowed' : ''}`}
dangerouslySetInnerHTML={link.label ? { __html: link.label } : undefined}
>
{!link.label && link.url ? i + 1 : null}
</Link>
))}
</nav>
);
}
Utilisation : <Pagination links={users.links} />. Même idée en Vue : composant Pagination.vue qui reçoit links en prop et affiche les Link. Adapter les classes et gérer le cas link.url vide (lien « désactivé » pour la page courante).
À retenir
- paginate() + withQueryString() côté Laravel ; passer users et filters (search, sort, order) en props.
- Côté client :
users.datapour la liste ;users.linksou prev_page_url / next_page_url pour la pagination. Toujours Link avec preserveScroll sur les liens de pagination. - Filtres : état local pour les champs (search, sort, order) ; au clic « Appliquer » (ou Enter), router.get(url, params) pour recharger la page avec les bons paramètres. Initialiser l’état local avec filters (props) pour cohérence.
- Composant Pagination réutilisable à partir de links pour un affichage propre (boutons ou numéros).
Dans le prochain module, nous verrons l’upload de fichiers (formulaires avec fichier, progression, validation Laravel).