Aller au contenu principal

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, afficher link.label directement).

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.data pour la liste ; users.links ou 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).