Module 5 – Formulaires et validation
Niveau 5.2 – Laravel Inertia (React & Vue)
Objectif
À la fin de ce module vous saurez :
- Soumettre un formulaire vers une route Laravel sans rechargement de page (via Inertia).
- Utiliser le hook useForm (React) et useForm (Vue) pour gérer les champs, la soumission et les erreurs de validation.
- Afficher les messages d’erreur Laravel à côté de chaque champ et désactiver le bouton pendant l’envoi (processing).
- Comprendre le flux : soumission → validation Laravel → soit redirect avec erreurs (Inertia les expose), soit redirect succès.
Nous traitons un formulaire de création d’utilisateur (nom, email) en Laravel + React et Laravel + Vue, avec validation et feedback complet.
Flux formulaire avec Inertia
- L’utilisateur remplit le formulaire (champs contrôlés par l’état useForm).
- À la soumission, Inertia envoie une requête POST (ou PUT/PATCH) vers l’URL choisie, avec les données du formulaire en corps de requête (comme un formulaire HTML classique, ou en JSON selon la config).
- Laravel reçoit la requête, exécute la validation (
$request->validate([...])). - Si la validation échoue : Laravel renvoie une redirect back avec les erreurs (en session). Inertia reçoit cette réponse et met à jour la page : les errors sont exposés (par champ) et useForm les fournit. Vous affichez les messages à côté des champs.
- Si la validation réussit : le contrôleur fait son travail (création, mise à jour, etc.) puis redirect (ex. vers la liste). Inertia charge la nouvelle page ; vous pouvez afficher un message de succès (flash) sur la page de destination.
Aucun fetch manuel : useForm appelle router.post() (ou put(), patch()) en interne. Inertia gère aussi le CSRF (token envoyé automatiquement) et l’état processing (true pendant la requête) pour désactiver le bouton.
useForm – rappel rapide
useForm retourne (entre autres) :
- data : objet contenant les valeurs des champs (pour liaison bidirectionnelle).
- setData (React) ou form.xxx (Vue, liaison directe) : pour mettre à jour un champ.
- post, put, patch : fonctions pour envoyer le formulaire vers une URL.
- processing : booléen, true pendant la requête (pour désactiver le bouton submit).
- errors : objet dont les clés sont les noms des champs et les valeurs les messages d’erreur Laravel (ex.
errors.email= "Le champ email est obligatoire.").
Nous détaillons les deux syntaxes (React et Vue) dans les exemples ci-dessous.
Côté Laravel – route et contrôleur
Route – routes/web.php :
use App\Http\Controllers\UserController;
Route::get('/users/create', [UserController::class, 'create'])->name('users.create');
Route::post('/users', [UserController::class, 'store'])->name('users.store');
Contrôleur – 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 create()
{
return Inertia::render('Users/Create');
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
], [
'name.required' => 'Le nom est obligatoire.',
'email.required' => 'L’email est obligatoire.',
'email.email' => 'L’email doit être une adresse valide.',
'email.unique' => 'Cet email est déjà utilisé.',
]);
User::create($validated);
return redirect()->route('users.index')->with('success', 'Utilisateur créé.');
}
}
- create : affiche la page du formulaire (sans données particulières ici).
- store : reçoit POST, valide, crée l’utilisateur, redirige vers la liste avec un message flash
success. Si la validation échoue, Laravel redirige automatiquement vers la page précédente (create) avec les erreurs ; Inertia les rend disponibles dans errors.
Formulaire complet – React (Users/Create)
resources/js/Pages/Users/Create.jsx :
import React from 'react';
import { useForm } from '@inertiajs/react';
import AppLayout from '@/Layouts/AppLayout';
export default function Create() {
const { data, setData, post, processing, errors } = useForm({
name: '',
email: '',
});
const handleSubmit = (e) => {
e.preventDefault();
post(route('users.store'));
};
return (
<AppLayout>
<h1>Créer un utilisateur</h1>
<form onSubmit={handleSubmit} className="space-y-4 max-w-md">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Nom
</label>
<input
id="name"
type="text"
value={data.name}
onChange={(e) => setData('name', e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
)}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email
</label>
<input
id="email"
type="email"
value={data.email}
onChange={(e) => setData('email', e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div>
<button
type="submit"
disabled={processing}
className="inline-flex justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50"
>
{processing ? 'Création...' : 'Créer'}
</button>
</form>
</AppLayout>
);
}
Points importants :
useForm({ name: '', email: '' }): état initial du formulaire. data contient ces valeurs ; setData('name', value) met à jour data.name.- post(route('users.store')) : envoie data en POST vers l’URL de la route users.store. Si vous n’utilisez pas Ziggy pour les noms de routes, remplacez par
post('/users'). - errors.name, errors.email : affichés sous chaque champ ; Laravel les remplit après une validation échouée.
- processing : désactive le bouton et change le libellé pendant l’envoi.
Note : la fonction route() (URL Laravel depuis le front) est souvent fournie par le package ziggy (à installer et configurer côté Laravel). Si vous ne l’avez pas, utilisez l’URL en dur /users pour post.
Formulaire complet – Vue (Users/Create)
resources/js/Pages/Users/Create.vue :
<template>
<AppLayout>
<h1>Créer un utilisateur</h1>
<form @submit.prevent="form.post(route('users.store'))" class="space-y-4 max-w-md">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">
Nom
</label>
<input
id="name"
v-model="form.name"
type="text"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
<p v-if="form.errors.name" class="mt-1 text-sm text-red-600">
{{ form.errors.name }}
</p>
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700">
Email
</label>
<input
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
<p v-if="form.errors.email" class="mt-1 text-sm text-red-600">
{{ form.errors.email }}
</p>
</div>
<button
type="submit"
:disabled="form.processing"
class="inline-flex justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50"
>
{{ form.processing ? 'Création...' : 'Créer' }}
</button>
</form>
</AppLayout>
</template>
<script setup>
import { useForm } from '@inertiajs/vue3';
import AppLayout from '@/Layouts/AppLayout.vue';
const form = useForm({
name: '',
email: '',
});
</script>
useForm({ name: '', email: '' }): retourne un objet form réactif. En Vue, v-model="form.name" lie directement l’input à form.name ; pas besoin de setData.- @submit.prevent="form.post(route('users.store'))" : empêche le submit natif et envoie le formulaire en POST. form.post envoie les champs de form.
- form.errors.name, form.errors.email : erreurs Laravel par champ.
- form.processing : true pendant l’envoi.
Édition (formulaire pré-rempli)
Pour un formulaire d’édition, vous pré-remplissez useForm avec les données de la ressource (passées en prop depuis Laravel).
Laravel – contrôleur :
public function edit(User $user)
{
return Inertia::render('Users/Edit', [
'user' => $user->only(['id', 'name', 'email']),
]);
}
public function update(Request $request, User $user)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . $user->id,
]);
$user->update($validated);
return redirect()->route('users.index')->with('success', 'Utilisateur mis à jour.');
}
React – page Edit :
const { data, setData, put, processing, errors } = useForm({
name: props.user.name,
email: props.user.email,
});
const handleSubmit = (e) => {
e.preventDefault();
put(route('users.update', props.user.id));
};
Vue – page Edit :
const form = useForm({
name: props.user.name,
email: props.user.email,
});
// Soumission : form.put(route('users.update', props.user.id))
- put (ou patch) pour une mise à jour ; l’URL doit être la route qui cible l’utilisateur (ex. PUT /users/123). Laravel attend souvent ** _method** en champ ou une requête PUT réelle ; Inertia gère la méthode HTTP correcte.
Réinitialiser les erreurs (optionnel)
Quand l’utilisateur modifie un champ après une erreur, vous pouvez réinitialiser l’erreur de ce champ pour un meilleur feedback. En React, setData ne réinitialise pas automatiquement errors. Vous pouvez appeler clearErrors('name') (si disponible dans votre version de useForm) ou gérer un état local. En Vue, form.clearErrors() ou form.clearErrors('name') existe selon les versions. Consultez la doc Inertia pour l’API exacte de votre version.
À retenir
- Validation : toujours côté Laravel ; en cas d’échec, redirect back avec erreurs ; Inertia les expose dans useForm (errors / form.errors).
- useForm : gère data, setData (React) ou v-model (Vue), post / put / patch, processing, errors.
- Toujours afficher errors.xxx sous chaque champ et désactiver le bouton avec processing.
- Pour l’édition : initialiser useForm avec les props de la ressource et utiliser put (ou patch) vers l’URL de mise à jour.
Dans le prochain module, nous verrons les liens Inertia (Link), preserveState, preserveScroll et les visites partielles.