#Introduction
Le nouveau composant <Form> d'Inertia.js révolutionne la gestion des formulaires en encapsulant le hook useForm et en automatisant le code répétitif. Il gère automatiquement la validation serveur, les erreurs, l'état de traitement et le téléchargement de fichiers, le tout sans rechargement de page. Cet article vous montre comment l'utiliser efficacement dans vos projets Laravel avec Vue.js.
#Sommaire
- Prérequis techniques
- Comprendre Wayfinder et la génération des routes
- Concepts clés du composant Form
- Création d'un formulaire
- Modification avec formulaire
- Propriétés et options essentielles
- Bonnes pratiques
- Conclusion
#1. Prérequis techniques
#Backend Laravel
- Laravel 12 avec inertiajs/inertia-laravel ≥ v2.0.10
- Routes resourceful et Form Request pour validation
- laravel/wayfinder
- Contrôleur RESTful
#Frontend Vue.js
- Vue.js 3 avec Composition API
- Inertiajs/vue3 version 2.1.0+
- TypeScript configuré
#Route Laravel
Route::middleware(['auth', 'verified'])->group(function (): void {Route::resource('students', StudentController::class);});Route::middleware(['auth', 'verified'])->group(function (): void {Route::resource('students', StudentController::class);});
#2. Comprendre Wayfinder et la génération des routes
#Qu'est-ce que Wayfinder?
Wayfinder est un package Laravel officiel qui génère automatiquement des helpers TypeScript pour vos routes Laravel côté frontend. Il élimine les URL codées en dur et garantit la synchronisation parfaite entre votre backend et frontend.
#Génération des routes
Wayfinder analyse vos routes Laravel et crée des helpers JavaScript/TypeScript:
// Route LaravelRoute::resource('students', StudentController::class);// Route LaravelRoute::resource('students', StudentController::class);
Génère côté frontend:
// resources/js/routes/students/index.tsexport const index = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({url: index.url(options),method: 'get',})index.definition = {methods: ["get","head"],url: '/students',} satisfies RouteDefinition<["get","head"]>export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({url: create.url(options),method: 'get',})create.definition = {methods: ["get","head"],url: '/students/create',} satisfies RouteDefinition<["get","head"]>...export const destroy = (args: { student: number | { id: number } } | [student: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'delete'> => ({url: destroy.url(args, options),method: 'delete',})destroy.definition = {methods: ["delete"],url: '/students/{student}',}// resources/js/routes/students/index.tsexport const index = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({url: index.url(options),method: 'get',})index.definition = {methods: ["get","head"],url: '/students',} satisfies RouteDefinition<["get","head"]>export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({url: create.url(options),method: 'get',})create.definition = {methods: ["get","head"],url: '/students/create',} satisfies RouteDefinition<["get","head"]>...export const destroy = (args: { student: number | { id: number } } | [student: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'delete'> => ({url: destroy.url(args, options),method: 'delete',})destroy.definition = {methods: ["delete"],url: '/students/{student}',}
#Intégration avec le composant Form
Au lieu d'écrire manuellement:
<Form action="/students" method="post"><Form action="/students" method="post">
Vous utilisez Wayfinder avec le pattern de liaison dynamique:
<Form v-bind="StudentController.store.form()">ou<Form :action="studentsRoutes.store.form().url"><Form v-bind="StudentController.store.form()">ou<Form :action="studentsRoutes.store.form().url">
Cette syntaxe se décompose ainsi:
StudentController.storecorrespond à la méthodestorede votre contrôleur.form()retourne un objet contenant{ action: '/students', method: 'POST' }v-binddistribue ces propriétés au composant Form:actiondéfinit dynamiquement l'URL de soumission du formulaire
#Avantages de Wayfinder
Type Safety: TypeScript détecte les erreurs de routes à la compilation.
// ✅ CorrectStudentController.store.form()// ❌ Erreur TypeScriptStudentController.stor.form() // Typo détectée// ✅ CorrectStudentController.store.form()// ❌ Erreur TypeScriptStudentController.stor.form() // Typo détectée
Paramètres dynamiques: Pour les routes avec paramètres:
<!-- Pour create/store: pas de paramètre --><Form v-bind="StudentController.store.form()"><!-- Pour update: ID requis --><Form v-bind="StudentController.update.form(props.student.id)"><!-- Pour delete: ID requis --><Link v-bind="StudentController.destroy.link(student.id)" method="delete"><!-- Pour create/store: pas de paramètre --><Form v-bind="StudentController.store.form()"><!-- Pour update: ID requis --><Form v-bind="StudentController.update.form(props.student.id)"><!-- Pour delete: ID requis --><Link v-bind="StudentController.destroy.link(student.id)" method="delete">
Refactoring sécurisé: Si vous renommez une route Laravel, Wayfinder met à jour automatiquement les helpers et TypeScript vous alertera sur tous les usages obsolètes.
#Différence entre action manuelle et Wayfinder
Approche manuelle (sans Wayfinder):
<template><!-- URL codée en dur, risque d'erreur --><Form action="/students" method="post"><!-- Champs --></Form><!-- Pour update, vous devez construire l'URL manuellement --><Form :action="`/students/${student.id}`" method="put"><!-- Champs --></Form></template><template><!-- URL codée en dur, risque d'erreur --><Form action="/students" method="post"><!-- Champs --></Form><!-- Pour update, vous devez construire l'URL manuellement --><Form :action="`/students/${student.id}`" method="put"><!-- Champs --></Form></template>
Problèmes:
- URL codées en dur
- Pas de vérification TypeScript
- Erreurs difficiles à détecter
- Maintenance complexe lors de changements de routes
Approche Wayfinder (recommandée):
<script setup lang="ts">import StudentController from '@/actions/App/Http/Controllers/StudentController';import type { Student } from '@/types/student';interface Props {student?: Student;}const props = defineProps<Props>();</script><template><!-- Pour création --><Form v-bind="StudentController.store.form()"><!-- Champs --></Form><!-- Pour mise à jour --><Form v-bind="StudentController.update.form(props.student.id)"><!-- Champs --></Form></template><script setup lang="ts">import StudentController from '@/actions/App/Http/Controllers/StudentController';import type { Student } from '@/types/student';interface Props {student?: Student;}const props = defineProps<Props>();</script><template><!-- Pour création --><Form v-bind="StudentController.store.form()"><!-- Champs --></Form><!-- Pour mise à jour --><Form v-bind="StudentController.update.form(props.student.id)"><!-- Champs --></Form></template>
Avantages:
- Type safety complet
- Auto-complétion dans l'IDE
- Détection des erreurs à la compilation
- Maintenance simplifiée
#Structure des fichiers générés
resources/js/├── actions/│ └── App/│ └── Http/│ └── Controllers/│ └── StudentController.ts├── routes/│ └── students/│ └── index.ts└── types/└── student.tsresources/js/├── actions/│ └── App/│ └── Http/│ └── Controllers/│ └── StudentController.ts├── routes/│ └── students/│ └── index.ts└── types/└── student.ts
Le fichier StudentController.ts expose des méthodes correspondant aux actions de votre contrôleur:
export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({url: store.url(options),method: 'post',})store.definition = {methods: ["post"],url: '/students',} satisfies RouteDefinition<["post"]>store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({url: store.url(options),method: 'post',})...export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({url: store.url(options),method: 'post',})store.definition = {methods: ["post"],url: '/students',} satisfies RouteDefinition<["post"]>store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({url: store.url(options),method: 'post',})...
#3. Concepts clés du composant Form
#Structure de base
Le composant nécessite deux attributs essentiels: action et method.
<Form action="/users" method="post"><input type="text" name="name" /><button type="submit">Créer</button></Form><Form action="/users" method="post"><input type="text" name="name" /><button type="submit">Créer</button></Form>
Point clé: Pas besoin de v-model, juste l'attribut name suffit. Le composant collecte automatiquement les valeurs.
#Accès à l'état via slot props
<Form action="/users" method="post" #default="{ errors, processing, wasSuccessful }"><input type="text" name="name" /><span v-if="errors.name">{{ errors.name }}</span><button type="submit" :disabled="processing">{{ processing ? 'Création...' : 'Créer' }}</button></Form><Form action="/users" method="post" #default="{ errors, processing, wasSuccessful }"><input type="text" name="name" /><span v-if="errors.name">{{ errors.name }}</span><button type="submit" :disabled="processing">{{ processing ? 'Création...' : 'Créer' }}</button></Form>
Propriétés disponibles:
errors: Erreurs de validationprocessing: État de soumissionwasSuccessful: Succès de la dernière soumissionisDirty: Formulaire modifiésetError,clearErrors: Manipulation des erreurs
#Support des structures imbriquées
<Form action="/users" method="post"><input type="text" name="user.name" /><input type="text" name="address.street" /><input type="text" name="tags[]" /></Form><Form action="/users" method="post"><input type="text" name="user.name" /><input type="text" name="address.street" /><input type="text" name="tags[]" /></Form>
Génère automatiquement:
{"user": { "name": "John" },"address": { "street": "123 Main" },"tags": ["tag1"]}{"user": { "name": "John" },"address": { "street": "123 Main" },"tags": ["tag1"]}
#4. Création d'un formulaire
#Contrôleur Laravel
public function store(StudentRequest $request){$validated = $request->validated();$validated['profile'] = $this->handleProfileUpload($request);Student::create($validated);return redirect()->route('students.index')->with('success', 'Étudiant créé avec succès.');}public function store(StudentRequest $request){$validated = $request->validated();$validated['profile'] = $this->handleProfileUpload($request);Student::create($validated);return redirect()->route('students.index')->with('success', 'Étudiant créé avec succès.');}
#Form Request
public function rules(): array{return ['name' => ['required', 'string', 'max:255'],'email' => ['required', 'email', 'unique:students'],'profile' => ['nullable', 'image', 'max:2048'],'bio' => ['nullable', 'string', 'max:500'],];}public function rules(): array{return ['name' => ['required', 'string', 'max:255'],'email' => ['required', 'email', 'unique:students'],'profile' => ['nullable', 'image', 'max:2048'],'bio' => ['nullable', 'string', 'max:500'],];}
#Composant Vue.js
<script setup lang="ts">import { Form, Head, Link } from '@inertiajs/vue3';import StudentController from '@/actions/App/Http/Controllers/StudentController';import * as studentsRoute from '@/routes/students';import type { Student } from '@/types/student';interface Props {student?: Student | null;}defineProps<Props>();</script><template><Head title="Créer un étudiant" /><Formv-bind="StudentController.store.form()"enctype="multipart/form-data"v-slot="{ errors, processing }"><div class="form-group"><label for="name">Nom complet</label><inputid="name"name="name"type="text"/><span v-if="errors.name" class="text-red-500">{{ errors.name }}</span></div><div class="form-group"><label for="email">Email</label><inputid="email"name="email"type="email"/><span v-if="errors.email" class="text-red-500">{{ errors.email }}</span></div><div class="form-group"><label for="profile">Photo de profil</label><input id="profile" name="profile" type="file" /><span v-if="errors.profile" class="text-red-500">{{ errors.profile }}</span></div><div class="form-group"><label for="bio">Biographie</label><textarea id="bio" name="bio" :value="student?.bio"></textarea><span v-if="errors.bio" class="text-red-500">{{ errors.bio }}</span></div><div class="form-actions"><Link :href="studentsRoute.index().url" class="btn-secondary">Retour</Link><button type="submit" :disabled="processing" class="btn-primary">{{ processing ? "Ajout en cours... ": "Ajouter un étudiant" }}</button></div></Form></template><script setup lang="ts">import { Form, Head, Link } from '@inertiajs/vue3';import StudentController from '@/actions/App/Http/Controllers/StudentController';import * as studentsRoute from '@/routes/students';import type { Student } from '@/types/student';interface Props {student?: Student | null;}defineProps<Props>();</script><template><Head title="Créer un étudiant" /><Formv-bind="StudentController.store.form()"enctype="multipart/form-data"v-slot="{ errors, processing }"><div class="form-group"><label for="name">Nom complet</label><inputid="name"name="name"type="text"/><span v-if="errors.name" class="text-red-500">{{ errors.name }}</span></div><div class="form-group"><label for="email">Email</label><inputid="email"name="email"type="email"/><span v-if="errors.email" class="text-red-500">{{ errors.email }}</span></div><div class="form-group"><label for="profile">Photo de profil</label><input id="profile" name="profile" type="file" /><span v-if="errors.profile" class="text-red-500">{{ errors.profile }}</span></div><div class="form-group"><label for="bio">Biographie</label><textarea id="bio" name="bio" :value="student?.bio"></textarea><span v-if="errors.bio" class="text-red-500">{{ errors.bio }}</span></div><div class="form-actions"><Link :href="studentsRoute.index().url" class="btn-secondary">Retour</Link><button type="submit" :disabled="processing" class="btn-primary">{{ processing ? "Ajout en cours... ": "Ajouter un étudiant" }}</button></div></Form></template>
Points clés:
v-bind="StudentController.store.form()": Génère automatiquement action et method via Wayfinderenctype="multipart/form-data": Pour les uploads de fichiers- Pas de
v-model processingpour désactiver les boutons pendant la soumission
#5. Modification avec formulaire
#Contrôleur
public function update(StudentRequest $request, Student $student){$validated = $request->validated();$validated['profile'] = $this->handleProfileUpload($request, $student);$student->update($validated);return redirect()->route('students.index')->with('success', 'Étudiant mis à jour.');}public function update(StudentRequest $request, Student $student){$validated = $request->validated();$validated['profile'] = $this->handleProfileUpload($request, $student);$student->update($validated);return redirect()->route('students.index')->with('success', 'Étudiant mis à jour.');}
#Règles de validation avec ignore
public function rules(): array{$studentId = $this->route('student')?->id;return ['email' => ['required', 'email', Rule::unique('students')->ignore($studentId)],// Autres règles...];}public function rules(): array{$studentId = $this->route('student')?->id;return ['email' => ['required', 'email', Rule::unique('students')->ignore($studentId)],// Autres règles...];}
#Composant Vue.js
<script setup lang="ts">import { Form, Head, Link } from '@inertiajs/vue3';import StudentController from '@/actions/App/Http/Controllers/StudentController';import type { Student } from '@/types/student';interface Props {student: Student;}defineProps<Props>();</script><template><Head title="Modifier un étudiant" /><Formv-bind="StudentController.update.form(props.student.id)"enctype="multipart/form-data"v-slot="{ errors, processing }"><div class="form-group"><label for="name">Nom complet</label><input id="name" name="name" type="text" :value="student.name" /><span v-if="errors.name" class="text-red-500">{{ errors.name }}</span></div><div class="form-group"><label for="email">Email</label><input id="email" name="email" type="email" :value="student.email" /><span v-if="errors.email" class="text-red-500">{{ errors.email }}</span></div><div class="form-group"><label for="bio">Biographie</label><textarea id="bio" name="bio" :value="student.bio"></textarea><span v-if="errors.bio" class="text-red-500">{{ errors.bio }}</span></div><div class="form-actions"><button type="submit" :disabled="processing" class="btn-primary"><span v-if="processing">Enregistrement...</span><span v-else>Mettre à jour</span></button></div></Form></template><script setup lang="ts">import { Form, Head, Link } from '@inertiajs/vue3';import StudentController from '@/actions/App/Http/Controllers/StudentController';import type { Student } from '@/types/student';interface Props {student: Student;}defineProps<Props>();</script><template><Head title="Modifier un étudiant" /><Formv-bind="StudentController.update.form(props.student.id)"enctype="multipart/form-data"v-slot="{ errors, processing }"><div class="form-group"><label for="name">Nom complet</label><input id="name" name="name" type="text" :value="student.name" /><span v-if="errors.name" class="text-red-500">{{ errors.name }}</span></div><div class="form-group"><label for="email">Email</label><input id="email" name="email" type="email" :value="student.email" /><span v-if="errors.email" class="text-red-500">{{ errors.email }}</span></div><div class="form-group"><label for="bio">Biographie</label><textarea id="bio" name="bio" :value="student.bio"></textarea><span v-if="errors.bio" class="text-red-500">{{ errors.bio }}</span></div><div class="form-actions"><button type="submit" :disabled="processing" class="btn-primary"><span v-if="processing">Enregistrement...</span><span v-else>Mettre à jour</span></button></div></Form></template>
Différences avec la création:
StudentController.update.form(props.student.id): Route dynamique avec ID- Méthode PUT et PATCH générée automatiquement par Wayfinder
- Valeurs pré-remplies depuis
props.student
#6. Propriétés et options essentielles
#Transform: Modifier les données avant soumission
La propriété transform vous permet de manipuler les données du formulaire juste avant leur envoi au serveur. Cette fonction intercepte les données collectées et retourne l'objet final qui sera soumis.
Cas d'usage courants:
- Ajouter des champs cachés ou calculés
- Formatter des dates ou des nombres
- Nettoyer ou transformer des valeurs
- Ajouter des métadonnées (timestamps, user_id, etc.)
<Formaction="/posts"method="post":transform="(data) => ({...data,user_id: currentUser.id,timestamp: Date.now(),slug: data.title.toLowerCase().replace(/\s+/g, '-')})"><input type="text" name="title" /><textarea name="content"></textarea><button type="submit">Publier</button></Form><Formaction="/posts"method="post":transform="(data) => ({...data,user_id: currentUser.id,timestamp: Date.now(),slug: data.title.toLowerCase().replace(/\s+/g, '-')})"><input type="text" name="title" /><textarea name="content"></textarea><button type="submit">Publier</button></Form>
Exemple avec formatage de données:
<script setup>const transformStudentData = (data) => {return {...data,// Convertir le tableau en JSON si nécessaireprograms: JSON.stringify(data.programs),// Formatter la date de naissancebirth_date: data.birth_date ? new Date(data.birth_date).toISOString() : null,// Nettoyer les espacesname: data.name.trim(),email: data.email.toLowerCase().trim()};};</script><template><Form action="/students" method="post" :transform="transformStudentData"><!-- Champs du formulaire --></Form></template><script setup>const transformStudentData = (data) => {return {...data,// Convertir le tableau en JSON si nécessaireprograms: JSON.stringify(data.programs),// Formatter la date de naissancebirth_date: data.birth_date ? new Date(data.birth_date).toISOString() : null,// Nettoyer les espacesname: data.name.trim(),email: data.email.toLowerCase().trim()};};</script><template><Form action="/students" method="post" :transform="transformStudentData"><!-- Champs du formulaire --></Form></template>
#Options de visite
Les options de visite contrôlent le comportement d'Inertia lors de la soumission du formulaire. Elles sont regroupées sous la propriété options pour éviter la confusion entre les propriétés de soumission et celles de rechargement.
<Formaction="/profile"method="put":options="{preserveScroll: true,preserveState: true,only: ['user', 'flash']}"><Formaction="/profile"method="put":options="{preserveScroll: true,preserveState: true,only: ['user', 'flash']}">
Explication des options principales:
preserveScroll (boolean): Maintient la position de défilement de la page après la soumission. Très utile pour les formulaires situés en milieu de page.
<!-- Idéal pour un formulaire de commentaire au milieu d'une longue page --><Formaction="/comments"method="post":options="{ preserveScroll: true }"><!-- Idéal pour un formulaire de commentaire au milieu d'une longue page --><Formaction="/comments"method="post":options="{ preserveScroll: true }">
preserveState (boolean): Conserve l'état local des composants qui ne sont pas rechargés. Empêche la perte de données dans d'autres parties de votre interface.
<!-- Garde l'état d'un formulaire de recherche ouvert --><Formaction="/profile/update"method="put":options="{ preserveState: true }"><!-- Garde l'état d'un formulaire de recherche ouvert --><Formaction="/profile/update"method="put":options="{ preserveState: true }">
preserveUrl (boolean): Empêche la modification de l'URL du navigateur après la soumission. Utile pour les formulaires modaux ou les actions qui ne devraient pas changer l'URL.
<Formaction="/newsletter/subscribe"method="post":options="{ preserveUrl: true }"><Formaction="/newsletter/subscribe"method="post":options="{ preserveUrl: true }">
replace (boolean): Remplace l'entrée actuelle de l'historique au lieu d'en créer une nouvelle. Évite que l'utilisateur ne revienne sur des états intermédiaires.
<Formaction="/wizard/step-2"method="post":options="{ replace: true }"><Formaction="/wizard/step-2"method="post":options="{ replace: true }">
only (array): Ne recharge que les propriétés spécifiées depuis le serveur. Optimise les performances en réduisant la taille de la réponse.
<!-- Ne recharge que le profil utilisateur et les messages flash --><Formaction="/profile"method="put":options="{ only: ['user', 'flash'] }"><!-- Ne recharge que le profil utilisateur et les messages flash --><Formaction="/profile"method="put":options="{ only: ['user', 'flash'] }">
except (array): Exclut certaines propriétés du rechargement. Utile pour éviter de recharger des données volumineuses non affectées.
<!-- Ne pas recharger la liste des produits lors de la mise à jour du profil --><Formaction="/profile"method="put":options="{ except: ['products', 'categories'] }"><!-- Ne pas recharger la liste des produits lors de la mise à jour du profil --><Formaction="/profile"method="put":options="{ except: ['products', 'categories'] }">
reset (array): Réinitialise des propriétés spécifiques à leurs valeurs par défaut après la visite.
<Formaction="/search"method="get":options="{ reset: ['page', 'filters'] }"><Formaction="/search"method="get":options="{ reset: ['page', 'filters'] }">
Exemple combiné complet:
<script setup lang="ts">import { Form } from '@inertiajs/vue3';import type { User } from '@/types';interface Props {user: User;}const props = defineProps<Props>();</script><template><Formv-bind="ProfileController.update.form(props.user.id)":options="{preserveScroll: true, // Garde la positionpreserveState: true, // Conserve l'état des autres composantsonly: ['user', 'flash'], // Ne recharge que user et flashonSuccess: () => {// Callback après succèsconsole.log('Profil mis à jour');}}"><!-- Champs du formulaire --></Form></template><script setup lang="ts">import { Form } from '@inertiajs/vue3';import type { User } from '@/types';interface Props {user: User;}const props = defineProps<Props>();</script><template><Formv-bind="ProfileController.update.form(props.user.id)":options="{preserveScroll: true, // Garde la positionpreserveState: true, // Conserve l'état des autres composantsonly: ['user', 'flash'], // Ne recharge que user et flashonSuccess: () => {// Callback après succèsconsole.log('Profil mis à jour');}}"><!-- Champs du formulaire --></Form></template>
#Réinitialisation automatique
Les propriétés de réinitialisation automatisent le nettoyage des formulaires après soumission, améliorant l'expérience utilisateur.
resetOnSuccess: Réinitialise le formulaire après une soumission réussie.
<!-- Réinitialiser tous les champs - Idéal pour formulaires de création --><Form action="/users" method="post" resetOnSuccess><input type="text" name="name" /><input type="email" name="email" /><button type="submit">Créer</button></Form><!-- Réinitialiser tous les champs - Idéal pour formulaires de création --><Form action="/users" method="post" resetOnSuccess><input type="text" name="name" /><input type="email" name="email" /><button type="submit">Créer</button></Form>
Réinitialisation sélective: Ne réinitialise que certains champs spécifiques.
<!-- Réinitialiser uniquement le mot de passe après soumission --><Form action="/profile" method="put" :resetOnSuccess="['password', 'password_confirmation']"><input type="text" name="name" value="John Doe" /><input type="password" name="password" /><input type="password" name="password_confirmation" /><button type="submit">Mettre à jour</button></Form><!-- Réinitialiser uniquement le mot de passe après soumission --><Form action="/profile" method="put" :resetOnSuccess="['password', 'password_confirmation']"><input type="text" name="name" value="John Doe" /><input type="password" name="password" /><input type="password" name="password_confirmation" /><button type="submit">Mettre à jour</button></Form>
resetOnError: Réinitialise le formulaire en cas d'erreur. Moins courant mais utile pour certains cas.
<!-- Réinitialiser tout en cas d'erreur --><Form action="/payment" method="post" resetOnError><input type="text" name="card_number" /><button type="submit">Payer</button></Form><!-- Réinitialiser des champs sensibles uniquement --><Form action="/payment" method="post" :resetOnError="['cvv', 'pin']"><input type="text" name="card_number" /><input type="text" name="cvv" /><button type="submit">Payer</button></Form><!-- Réinitialiser tout en cas d'erreur --><Form action="/payment" method="post" resetOnError><input type="text" name="card_number" /><button type="submit">Payer</button></Form><!-- Réinitialiser des champs sensibles uniquement --><Form action="/payment" method="post" :resetOnError="['cvv', 'pin']"><input type="text" name="card_number" /><input type="text" name="cvv" /><button type="submit">Payer</button></Form>
setDefaultsOnSuccess: Définit les valeurs actuelles comme nouvelles valeurs par défaut après succès. Très utile pour les formulaires d'édition.
<Form action="/profile" method="put" setDefaultsOnSuccess><input type="text" name="name" :value="user.name" /><input type="email" name="email" :value="user.email" /><button type="submit">Sauvegarder</button></Form><Form action="/profile" method="put" setDefaultsOnSuccess><input type="text" name="name" :value="user.name" /><input type="email" name="email" :value="user.email" /><button type="submit">Sauvegarder</button></Form>
Pourquoi c'est important: Sans setDefaultsOnSuccess, après avoir sauvegardé un formulaire d'édition, le formulaire serait marqué comme "modifié" (isDirty: true) même sans changement, car les valeurs par défaut seraient toujours les anciennes. Cette propriété synchronise les valeurs par défaut avec les nouvelles valeurs sauvegardées.
Exemple pratique combiné:
<script setup lang="ts">import { Form } from '@inertiajs/vue3';const handleSuccess = () => {// Afficher une notificationtoast.success('Profil mis à jour avec succès');};</script><template><!-- Formulaire d'édition avec mise à jour des valeurs par défaut --><Formaction="/profile"method="put"setDefaultsOnSuccess@success="handleSuccess"><input type="text" name="name" :value="user.name" /><button type="submit">Sauvegarder</button></Form><!-- Formulaire de création avec réinitialisation complète --><Formaction="/posts"method="post"resetOnSuccess@success="handleSuccess"><input type="text" name="title" /><textarea name="content"></textarea><button type="submit">Publier</button></Form><!-- Formulaire sensible avec réinitialisation sélective --><Formaction="/password/update"method="put":resetOnSuccess="['current_password', 'password', 'password_confirmation']"><input type="password" name="current_password" /><input type="password" name="password" /><input type="password" name="password_confirmation" /><button type="submit">Changer le mot de passe</button></Form></template><script setup lang="ts">import { Form } from '@inertiajs/vue3';const handleSuccess = () => {// Afficher une notificationtoast.success('Profil mis à jour avec succès');};</script><template><!-- Formulaire d'édition avec mise à jour des valeurs par défaut --><Formaction="/profile"method="put"setDefaultsOnSuccess@success="handleSuccess"><input type="text" name="name" :value="user.name" /><button type="submit">Sauvegarder</button></Form><!-- Formulaire de création avec réinitialisation complète --><Formaction="/posts"method="post"resetOnSuccess@success="handleSuccess"><input type="text" name="title" /><textarea name="content"></textarea><button type="submit">Publier</button></Form><!-- Formulaire sensible avec réinitialisation sélective --><Formaction="/password/update"method="put":resetOnSuccess="['current_password', 'password', 'password_confirmation']"><input type="password" name="current_password" /><input type="password" name="password" /><input type="password" name="password_confirmation" /><button type="submit">Changer le mot de passe</button></Form></template>
#Désactivation pendant traitement
La propriété disable-while-processing ajoute automatiquement l'attribut HTML inert au formulaire pendant sa soumission. Cet attribut natif du navigateur désactive toutes les interactions avec le formulaire.
<Formaction="/users"method="post"disable-while-processingclass="inert:opacity-50 inert:pointer-events-none"><input type="text" name="name" /><button type="submit">Créer</button></Form><Formaction="/users"method="post"disable-while-processingclass="inert:opacity-50 inert:pointer-events-none"><input type="text" name="name" /><button type="submit">Créer</button></Form>
Comment ça fonctionne:
- L'utilisateur clique sur "Soumettre"
- Le formulaire reçoit l'attribut
inert - Tous les champs et boutons deviennent non interactifs
- Les styles CSS s'appliquent automatiquement via le sélecteur
:inert - Après la réponse du serveur, l'attribut
inertest retiré
Styles CSS/Tailwind pour l'état inert:
<Formdisable-while-processingclass="inert:opacity-50inert:pointer-events-noneinert:cursor-not-allowedtransition-opacity duration-200"><Formdisable-while-processingclass="inert:opacity-50inert:pointer-events-noneinert:cursor-not-allowedtransition-opacity duration-200">
Avantages:
- Empêche les doubles soumissions
- Feedback visuel immédiat pour l'utilisateur
- Aucun JavaScript manuel nécessaire
- Compatible avec tous les éléments du formulaire
Exemple avec indicateur de chargement personnalisé:
<script setup>import { ref } from 'vue';</script><template><Formaction="/users"method="post"disable-while-processingclass="relative inert:opacity-70"v-slot="{ processing }"><!-- Overlay de chargement --><divv-if="processing"class="absolute inset-0 flex items-center justify-center bg-white/80 z-10"><svg class="animate-spin h-8 w-8 text-blue-500" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/></svg></div><input type="text" name="name" /><button type="submit" :disabled="processing">{{ processing ? 'Création...' : 'Créer' }}</button></Form></template><script setup>import { ref } from 'vue';</script><template><Formaction="/users"method="post"disable-while-processingclass="relative inert:opacity-70"v-slot="{ processing }"><!-- Overlay de chargement --><divv-if="processing"class="absolute inset-0 flex items-center justify-center bg-white/80 z-10"><svg class="animate-spin h-8 w-8 text-blue-500" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/></svg></div><input type="text" name="name" /><button type="submit" :disabled="processing">{{ processing ? 'Création...' : 'Créer' }}</button></Form></template>
#Événements
Le composant <Form> émet tous les événements du cycle de vie d'une visite Inertia. Ces événements permettent d'exécuter du code à différentes étapes de la soumission.
<Formaction="/users"method="post"@before="handleBefore"@start="handleStart"@progress="handleProgress"@success="handleSuccess"@error="handleError"@finish="handleFinish"@cancel="handleCancel"><Formaction="/users"method="post"@before="handleBefore"@start="handleStart"@progress="handleProgress"@success="handleSuccess"@error="handleError"@finish="handleFinish"@cancel="handleCancel">
Description détaillée de chaque événement:
@before: Se déclenche avant l'envoi de la requête. Retourner false annule la soumission.
<script setup>const handleBefore = (visit) => {// Validation personnalisée avant soumissionif (!confirm('Êtes-vous sûr de vouloir continuer ?')) {return false; // Annule la soumission}// Logger l'actionconsole.log('Formulaire sur le point d\'être soumis', visit);};</script><template><Form action="/users" method="post" @before="handleBefore"><input type="text" name="name" /></Form></template><script setup>const handleBefore = (visit) => {// Validation personnalisée avant soumissionif (!confirm('Êtes-vous sûr de vouloir continuer ?')) {return false; // Annule la soumission}// Logger l'actionconsole.log('Formulaire sur le point d\'être soumis', visit);};</script><template><Form action="/users" method="post" @before="handleBefore"><input type="text" name="name" /></Form></template>
. . .
#7. Bonnes pratiques
#1. Composant wrapper centralisé
Créez un composant wrapper pour centraliser la configuration commune de vos formulaires. Cela évite la répétition et facilite la maintenance.
<script setup lang="ts">import { Form } from '@inertiajs/vue3';interface Props {action: string;method: string;preserveScroll?: boolean;}const props = withDefaults(defineProps<Props>(), {preserveScroll: true});</script><template><Form:action="props.action":method="props.method":options="{ preserveScroll: props.preserveScroll }"disable-while-processingclass="inert:opacity-50 inert:pointer-events-none transition-opacity"><slot /></Form></template><script setup lang="ts">import { Form } from '@inertiajs/vue3';interface Props {action: string;method: string;preserveScroll?: boolean;}const props = withDefaults(defineProps<Props>(), {preserveScroll: true});</script><template><Form:action="props.action":method="props.method":options="{ preserveScroll: props.preserveScroll }"disable-while-processingclass="inert:opacity-50 inert:pointer-events-none transition-opacity"><slot /></Form></template>
Utilisation:
<template><AppForm action="/users" method="post"><input type="text" name="name" /><button type="submit">Créer</button></AppForm></template><template><AppForm action="/users" method="post"><input type="text" name="name" /><button type="submit">Créer</button></AppForm></template>
Avantages:
- Configuration DRY (Don't Repeat Yourself)
- Styles et comportements cohérents dans toute l'application
- Modifications centralisées faciles
#2. Composant de champ réutilisable
Encapsulez vos champs de formulaire dans des composants réutilisables incluant labels, erreurs et styles.
<script setup lang="ts">interface Props {name: string;label: string;type?: string;error?: string;modelValue?: string;placeholder?: string;required?: boolean;}const props = withDefaults(defineProps<Props>(), {type: 'text',required: false});</script><template><div class="form-group mb-4"><label:for="props.name"class="block text-sm font-medium text-gray-700 mb-1">{{ props.label }}<span v-if="props.required" class="text-red-500">*</span></label><input:id="props.name":name="props.name":type="props.type":value="props.modelValue":placeholder="props.placeholder":required="props.required"class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500":class="{ 'border-red-500': props.error }"/><span v-if="props.error" class="text-red-500 text-sm mt-1 block">{{ props.error }}</span></div></template><script setup lang="ts">interface Props {name: string;label: string;type?: string;error?: string;modelValue?: string;placeholder?: string;required?: boolean;}const props = withDefaults(defineProps<Props>(), {type: 'text',required: false});</script><template><div class="form-group mb-4"><label:for="props.name"class="block text-sm font-medium text-gray-700 mb-1">{{ props.label }}<span v-if="props.required" class="text-red-500">*</span></label><input:id="props.name":name="props.name":type="props.type":value="props.modelValue":placeholder="props.placeholder":required="props.required"class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500":class="{ 'border-red-500': props.error }"/><span v-if="props.error" class="text-red-500 text-sm mt-1 block">{{ props.error }}</span></div></template>
Utilisation:
<template><Form v-bind="StudentController.store.form()" v-slot="{ errors }"><FormFieldname="name"label="Nom complet":error="errors.name":model-value="student?.name"placeholder="ex: Jean Dupont"required/><FormFieldname="email"label="Email"type="email":error="errors.email":model-value="student?.email"required/><button type="submit">Créer</button></Form></template><template><Form v-bind="StudentController.store.form()" v-slot="{ errors }"><FormFieldname="name"label="Nom complet":error="errors.name":model-value="student?.name"placeholder="ex: Jean Dupont"required/><FormFieldname="email"label="Email"type="email":error="errors.email":model-value="student?.email"required/><button type="submit">Créer</button></Form></template>
#3. Validation avant soumission
Implémentez une validation côté client avant d'envoyer la requête au serveur pour améliorer l'expérience utilisateur.
<script setup>import { ref } from 'vue';const localErrors = ref({});const handleBefore = (event) => {localErrors.value = {};const formData = new FormData(event.target);const email = formData.get('email');const password = formData.get('password');// Validation emailif (!email || !email.includes('@')) {localErrors.value.email = 'Format d\'email invalide';}// Validation mot de passeif (!password || password.length < 8) {localErrors.value.password = 'Le mot de passe doit contenir au moins 8 caractères';}// Annuler si erreursif (Object.keys(localErrors.value).length > 0) {return false;}return true;};</script><template><Form action="/users" method="post" @before="handleBefore" v-slot="{ errors }"><div class="form-group"><input type="email" name="email" /><span v-if="localErrors.email || errors.email" class="text-red-500">{{ localErrors.email || errors.email }}</span></div><div class="form-group"><input type="password" name="password" /><span v-if="localErrors.password || errors.password" class="text-red-500">{{ localErrors.password || errors.password }}</span></div><button type="submit">Créer</button></Form></template><script setup>import { ref } from 'vue';const localErrors = ref({});const handleBefore = (event) => {localErrors.value = {};const formData = new FormData(event.target);const email = formData.get('email');const password = formData.get('password');// Validation emailif (!email || !email.includes('@')) {localErrors.value.email = 'Format d\'email invalide';}// Validation mot de passeif (!password || password.length < 8) {localErrors.value.password = 'Le mot de passe doit contenir au moins 8 caractères';}// Annuler si erreursif (Object.keys(localErrors.value).length > 0) {return false;}return true;};</script><template><Form action="/users" method="post" @before="handleBefore" v-slot="{ errors }"><div class="form-group"><input type="email" name="email" /><span v-if="localErrors.email || errors.email" class="text-red-500">{{ localErrors.email || errors.email }}</span></div><div class="form-group"><input type="password" name="password" /><span v-if="localErrors.password || errors.password" class="text-red-500">{{ localErrors.password || errors.password }}</span></div><button type="submit">Créer</button></Form></template>
Avantages:
- Feedback immédiat sans attendre la réponse serveur
- Réduit le nombre de requêtes inutiles
- Meilleure expérience utilisateur
#4. Accès programmatique
Utilisez les refs pour contrôler le formulaire depuis l'extérieur ou déclencher des actions personnalisées.
<script setup>import { ref } from 'vue';import { Form } from '@inertiajs/vue3';const formRef = ref();const autoSaveEnabled = ref(true);// Soumission manuelleconst submitForm = () => {if (formRef.value) {formRef.value.submit();}};// Réinitialisation manuelleconst resetForm = () => {if (formRef.value) {formRef.value.reset();}};// Auto-save toutes les 30 secondessetInterval(() => {if (autoSaveEnabled.value && formRef.value) {formRef.value.submit();}}, 30000);</script><template><div><Form ref="formRef" action="/draft/save" method="post"><textarea name="content" rows="10"></textarea></Form><div class="actions mt-4 space-x-2"><button @click="submitForm" class="btn-primary">Sauvegarder maintenant</button><button @click="resetForm" class="btn-secondary">Réinitialiser</button><label class="inline-flex items-center"><inputtype="checkbox"v-model="autoSaveEnabled"class="mr-2"/>Auto-save activé</label></div></div></template><script setup>import { ref } from 'vue';import { Form } from '@inertiajs/vue3';const formRef = ref();const autoSaveEnabled = ref(true);// Soumission manuelleconst submitForm = () => {if (formRef.value) {formRef.value.submit();}};// Réinitialisation manuelleconst resetForm = () => {if (formRef.value) {formRef.value.reset();}};// Auto-save toutes les 30 secondessetInterval(() => {if (autoSaveEnabled.value && formRef.value) {formRef.value.submit();}}, 30000);</script><template><div><Form ref="formRef" action="/draft/save" method="post"><textarea name="content" rows="10"></textarea></Form><div class="actions mt-4 space-x-2"><button @click="submitForm" class="btn-primary">Sauvegarder maintenant</button><button @click="resetForm" class="btn-secondary">Réinitialiser</button><label class="inline-flex items-center"><inputtype="checkbox"v-model="autoSaveEnabled"class="mr-2"/>Auto-save activé</label></div></div></template>
Cas d'usage:
- Auto-save de brouillons
- Soumission via raccourcis clavier
- Validation personnalisée complexe
- Intégration avec d'autres composants
#Conclusion
Le composant <Form> d'Inertia.js simplifie drastiquement la gestion des formulaires en éliminant le code répétitif. Avec Wayfinder pour la génération automatique des routes et la gestion intégrée des erreurs de validation, vous pouvez construire des formulaires robustes et maintenables en quelques lignes de code. Adoptez ces patterns pour optimiser vos projets Laravel avec Vue.js.