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
1Route::middleware(['auth', 'verified'])->group(function (): void {2 Route::resource('students', StudentController::class);3});1Route::middleware(['auth', 'verified'])->group(function (): void {2 Route::resource('students', StudentController::class);3});
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:
1// Route Laravel2Route::resource('students', StudentController::class);1// Route Laravel2Route::resource('students', StudentController::class);
Génère côté frontend:
1// resources/js/routes/students/index.ts2export const index = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({3 url: index.url(options),4 method: 'get',5})67index.definition = {8 methods: ["get","head"],9 url: '/students',10} satisfies RouteDefinition<["get","head"]>11121314export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({15 url: create.url(options),16 method: 'get',17})1819create.definition = {20 methods: ["get","head"],21 url: '/students/create',22} satisfies RouteDefinition<["get","head"]>23.24.25.2627export const destroy = (args: { student: number | { id: number } } | [student: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'delete'> => ({28 url: destroy.url(args, options),29 method: 'delete',30})3132destroy.definition = {33 methods: ["delete"],34 url: '/students/{student}',35}1// resources/js/routes/students/index.ts2export const index = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({3 url: index.url(options),4 method: 'get',5})67index.definition = {8 methods: ["get","head"],9 url: '/students',10} satisfies RouteDefinition<["get","head"]>11121314export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({15 url: create.url(options),16 method: 'get',17})1819create.definition = {20 methods: ["get","head"],21 url: '/students/create',22} satisfies RouteDefinition<["get","head"]>23.24.25.2627export const destroy = (args: { student: number | { id: number } } | [student: number | { id: number } ] | number | { id: number }, options?: RouteQueryOptions): RouteDefinition<'delete'> => ({28 url: destroy.url(args, options),29 method: 'delete',30})3132destroy.definition = {33 methods: ["delete"],34 url: '/students/{student}',35}
Intégration avec le composant Form
Au lieu d'écrire manuellement:
1<Form action="/students" method="post">1<Form action="/students" method="post">
Vous utilisez Wayfinder avec le pattern de liaison dynamique:
2 ou2 ou
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.
1// ✅ Correct2StudentController.store.form()34// ❌ Erreur TypeScript5StudentController.stor.form() // Typo détectée1// ✅ Correct2StudentController.store.form()34// ❌ Erreur TypeScript5StudentController.stor.form() // Typo détectée
Paramètres dynamiques: Pour les routes avec paramètres:
1<!-- Pour create/store: pas de paramètre -->34<!-- Pour update: ID requis -->67<!-- Pour delete: ID requis -->1<!-- Pour create/store: pas de paramètre -->34<!-- Pour update: ID requis -->67<!-- Pour delete: ID requis -->
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):
1<template>2 <!-- URL codée en dur, risque d'erreur -->3 <Form action="/students" method="post">4 <!-- Champs -->5 </Form>67 <!-- Pour update, vous devez construire l'URL manuellement -->8 <Form :action="`/students/${student.id}`" method="put">9 <!-- Champs -->10 </Form>11</template>1<template>2 <!-- URL codée en dur, risque d'erreur -->3 <Form action="/students" method="post">4 <!-- Champs -->5 </Form>67 <!-- Pour update, vous devez construire l'URL manuellement -->8 <Form :action="`/students/${student.id}`" method="put">9 <!-- Champs -->10 </Form>11</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):
1<script setup lang="ts">2import StudentController from '@/actions/App/Http/Controllers/StudentController';3import type { Student } from '@/types/student';45interface Props {6 student?: Student;7}8const props = defineProps<Props>();9</script>1011<template>12 <!-- Pour création -->13 <Form v-bind="StudentController.store.form()">14 <!-- Champs -->15 </Form>1617 <!-- Pour mise à jour -->18 <Form v-bind="StudentController.update.form(props.student.id)">19 <!-- Champs -->20 </Form>21</template>1<script setup lang="ts">2import StudentController from '@/actions/App/Http/Controllers/StudentController';3import type { Student } from '@/types/student';45interface Props {6 student?: Student;7}8const props = defineProps<Props>();9</script>1011<template>12 <!-- Pour création -->13 <Form v-bind="StudentController.store.form()">14 <!-- Champs -->15 </Form>1617 <!-- Pour mise à jour -->18 <Form v-bind="StudentController.update.form(props.student.id)">19 <!-- Champs -->20 </Form>21</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
1resources/js/2├── actions/3│ └── App/4│ └── Http/5│ └── Controllers/6│ └── StudentController.ts7├── routes/8│ └── students/9│ └── index.ts10└── types/11 └── student.ts1resources/js/2├── actions/3│ └── App/4│ └── Http/5│ └── Controllers/6│ └── StudentController.ts7├── routes/8│ └── students/9│ └── index.ts10└── types/11 └── student.ts
Le fichier StudentController.ts expose des méthodes correspondant aux actions de votre contrôleur:
12export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({3 url: store.url(options),4 method: 'post',5})67store.definition = {8 methods: ["post"],9 url: '/students',10} satisfies RouteDefinition<["post"]>1112store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({13 url: store.url(options),14 method: 'post',15})16.17.18.12export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({3 url: store.url(options),4 method: 'post',5})67store.definition = {8 methods: ["post"],9 url: '/students',10} satisfies RouteDefinition<["post"]>1112store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({13 url: store.url(options),14 method: 'post',15})16.17.18.
3. Concepts clés du composant Form
Structure de base
Le composant nécessite deux attributs essentiels: action et method.
1<Form action="/users" method="post">2 <input type="text" name="name" />3 <button type="submit">Créer</button>4</Form>1<Form action="/users" method="post">2 <input type="text" name="name" />3 <button type="submit">Créer</button>4</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
1<Form action="/users" method="post" #default="{ errors, processing, wasSuccessful }">2 <input type="text" name="name" />45 <button type="submit" :disabled="processing">6 {{ processing ? 'Création...' : 'Créer' }}7 </button>8</Form>1<Form action="/users" method="post" #default="{ errors, processing, wasSuccessful }">2 <input type="text" name="name" />45 <button type="submit" :disabled="processing">6 {{ processing ? 'Création...' : 'Créer' }}7 </button>8</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
1<Form action="/users" method="post">3 <input type="text" name="address.street" />4 <input type="text" name="tags[]" />5</Form>1<Form action="/users" method="post">3 <input type="text" name="address.street" />4 <input type="text" name="tags[]" />5</Form>
Génère automatiquement:
1{2 "user": { "name": "John" },3 "address": { "street": "123 Main" },4 "tags": ["tag1"]5}1{2 "user": { "name": "John" },3 "address": { "street": "123 Main" },4 "tags": ["tag1"]5}
4. Création d'un formulaire
Contrôleur Laravel
1public function store(StudentRequest $request)2{3 $validated = $request->validated();4 $validated['profile'] = $this->handleProfileUpload($request);56 Student::create($validated);78 return redirect()->route('students.index')9 ->with('success', 'Étudiant créé avec succès.');10}1public function store(StudentRequest $request)2{3 $validated = $request->validated();4 $validated['profile'] = $this->handleProfileUpload($request);56 Student::create($validated);78 return redirect()->route('students.index')9 ->with('success', 'Étudiant créé avec succès.');10}
Form Request
1public function rules(): array2{3 return [4 'name' => ['required', 'string', 'max:255'],5 'email' => ['required', 'email', 'unique:students'],6 'profile' => ['nullable', 'image', 'max:2048'],7 'bio' => ['nullable', 'string', 'max:500'],8 ];9}1public function rules(): array2{3 return [4 'name' => ['required', 'string', 'max:255'],5 'email' => ['required', 'email', 'unique:students'],6 'profile' => ['nullable', 'image', 'max:2048'],7 'bio' => ['nullable', 'string', 'max:500'],8 ];9}
Composant Vue.js
1<script setup lang="ts">2import { Form, Head, Link } from '@inertiajs/vue3';3import StudentController from '@/actions/App/Http/Controllers/StudentController';4import * as studentsRoute from '@/routes/students';5import type { Student } from '@/types/student';67interface Props {8 student?: Student | null;9}10defineProps<Props>();11</script>1213<template>14 <Head title="Créer un étudiant" />1516 <Form17 v-bind="StudentController.store.form()"18 enctype="multipart/form-data"19 v-slot="{ errors, processing }"20 >21 <div class="form-group">22 <label for="name">Nom complet</label>23 <input24 id="name"25 name="name"26 type="text"27 />28 <span v-if="errors.name" class="text-red-500">{{ errors.name }}</span>29 </div>3031 <div class="form-group">32 <label for="email">Email</label>33 <input34 id="email"35 name="email"36 type="email"37 />38 <span v-if="errors.email" class="text-red-500">{{ errors.email }}</span>39 </div>4041 <div class="form-group">42 <label for="profile">Photo de profil</label>43 <input id="profile" name="profile" type="file" />44 <span v-if="errors.profile" class="text-red-500">{{ errors.profile }}</span>45 </div>4647 <div class="form-group">48 <label for="bio">Biographie</label>49 <textarea id="bio" name="bio" :value="student?.bio"></textarea>50 <span v-if="errors.bio" class="text-red-500">{{ errors.bio }}</span>51 </div>5253 <div class="form-actions">54 <Link :href="studentsRoute.index().url" class="btn-secondary">55 Retour56 </Link>57 <button type="submit" :disabled="processing" class="btn-primary">58 {{ processing ? "Ajout en cours... ": "Ajouter un étudiant" }}59 </button>60 </div>61 </Form>62</template>1<script setup lang="ts">2import { Form, Head, Link } from '@inertiajs/vue3';3import StudentController from '@/actions/App/Http/Controllers/StudentController';4import * as studentsRoute from '@/routes/students';5import type { Student } from '@/types/student';67interface Props {8 student?: Student | null;9}10defineProps<Props>();11</script>1213<template>14 <Head title="Créer un étudiant" />1516 <Form17 v-bind="StudentController.store.form()"18 enctype="multipart/form-data"19 v-slot="{ errors, processing }"20 >21 <div class="form-group">22 <label for="name">Nom complet</label>23 <input24 id="name"25 name="name"26 type="text"27 />28 <span v-if="errors.name" class="text-red-500">{{ errors.name }}</span>29 </div>3031 <div class="form-group">32 <label for="email">Email</label>33 <input34 id="email"35 name="email"36 type="email"37 />38 <span v-if="errors.email" class="text-red-500">{{ errors.email }}</span>39 </div>4041 <div class="form-group">42 <label for="profile">Photo de profil</label>43 <input id="profile" name="profile" type="file" />44 <span v-if="errors.profile" class="text-red-500">{{ errors.profile }}</span>45 </div>4647 <div class="form-group">48 <label for="bio">Biographie</label>49 <textarea id="bio" name="bio" :value="student?.bio"></textarea>50 <span v-if="errors.bio" class="text-red-500">{{ errors.bio }}</span>51 </div>5253 <div class="form-actions">54 <Link :href="studentsRoute.index().url" class="btn-secondary">55 Retour56 </Link>57 <button type="submit" :disabled="processing" class="btn-primary">58 {{ processing ? "Ajout en cours... ": "Ajouter un étudiant" }}59 </button>60 </div>61 </Form>62</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
1public function update(StudentRequest $request, Student $student)2{3 $validated = $request->validated();4 $validated['profile'] = $this->handleProfileUpload($request, $student);56 $student->update($validated);78 return redirect()->route('students.index')9 ->with('success', 'Étudiant mis à jour.');10}1public function update(StudentRequest $request, Student $student)2{3 $validated = $request->validated();4 $validated['profile'] = $this->handleProfileUpload($request, $student);56 $student->update($validated);78 return redirect()->route('students.index')9 ->with('success', 'Étudiant mis à jour.');10}
Règles de validation avec ignore
1public function rules(): array2{3 $studentId = $this->route('student')?->id;45 return [6 'email' => ['required', 'email', Rule::unique('students')->ignore($studentId)],7 // Autres règles...8 ];9}1public function rules(): array2{3 $studentId = $this->route('student')?->id;45 return [6 'email' => ['required', 'email', Rule::unique('students')->ignore($studentId)],7 // Autres règles...8 ];9}
Composant Vue.js
1<script setup lang="ts">2import { Form, Head, Link } from '@inertiajs/vue3';3import StudentController from '@/actions/App/Http/Controllers/StudentController';4import type { Student } from '@/types/student';56interface Props {7 student: Student;8}9 defineProps<Props>();10</script>1112<template>13 <Head title="Modifier un étudiant" />1415 <Form16 v-bind="StudentController.update.form(props.student.id)"17 enctype="multipart/form-data"18 v-slot="{ errors, processing }"19 >20 <div class="form-group">21 <label for="name">Nom complet</label>22 <input id="name" name="name" type="text" :value="student.name" />23 <span v-if="errors.name" class="text-red-500">{{ errors.name }}</span>24 </div>2526 <div class="form-group">27 <label for="email">Email</label>28 <input id="email" name="email" type="email" :value="student.email" />29 <span v-if="errors.email" class="text-red-500">{{ errors.email }}</span>30 </div>3132 <div class="form-group">33 <label for="bio">Biographie</label>34 <textarea id="bio" name="bio" :value="student.bio"></textarea>35 <span v-if="errors.bio" class="text-red-500">{{ errors.bio }}</span>36 </div>3738 <div class="form-actions">39 <button type="submit" :disabled="processing" class="btn-primary">40 <span v-if="processing">Enregistrement...</span>41 <span v-else>Mettre à jour</span>42 </button>43 </div>44 </Form>45</template>1<script setup lang="ts">2import { Form, Head, Link } from '@inertiajs/vue3';3import StudentController from '@/actions/App/Http/Controllers/StudentController';4import type { Student } from '@/types/student';56interface Props {7 student: Student;8}9 defineProps<Props>();10</script>1112<template>13 <Head title="Modifier un étudiant" />1415 <Form16 v-bind="StudentController.update.form(props.student.id)"17 enctype="multipart/form-data"18 v-slot="{ errors, processing }"19 >20 <div class="form-group">21 <label for="name">Nom complet</label>22 <input id="name" name="name" type="text" :value="student.name" />23 <span v-if="errors.name" class="text-red-500">{{ errors.name }}</span>24 </div>2526 <div class="form-group">27 <label for="email">Email</label>28 <input id="email" name="email" type="email" :value="student.email" />29 <span v-if="errors.email" class="text-red-500">{{ errors.email }}</span>30 </div>3132 <div class="form-group">33 <label for="bio">Biographie</label>34 <textarea id="bio" name="bio" :value="student.bio"></textarea>35 <span v-if="errors.bio" class="text-red-500">{{ errors.bio }}</span>36 </div>3738 <div class="form-actions">39 <button type="submit" :disabled="processing" class="btn-primary">40 <span v-if="processing">Enregistrement...</span>41 <span v-else>Mettre à jour</span>42 </button>43 </div>44 </Form>45</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.)
1<Form2 action="/posts"3 method="post"4 :transform="(data) => ({5 ...data,6 user_id: currentUser.id,7 timestamp: Date.now(),8 slug: data.title.toLowerCase().replace(/\s+/g, '-')9 })"10>11 <input type="text" name="title" />12 <textarea name="content"></textarea>13 <button type="submit">Publier</button>14</Form>1<Form2 action="/posts"3 method="post"4 :transform="(data) => ({5 ...data,6 user_id: currentUser.id,7 timestamp: Date.now(),8 slug: data.title.toLowerCase().replace(/\s+/g, '-')9 })"10>11 <input type="text" name="title" />12 <textarea name="content"></textarea>13 <button type="submit">Publier</button>14</Form>
Exemple avec formatage de données:
1<script setup>2const transformStudentData = (data) => {3 return {4 ...data,5 // Convertir le tableau en JSON si nécessaire6 programs: JSON.stringify(data.programs),7 // Formatter la date de naissance8 birth_date: data.birth_date ? new Date(data.birth_date).toISOString() : null,9 // Nettoyer les espaces10 name: data.name.trim(),11 email: data.email.toLowerCase().trim()12 };13};14</script>1516<template>17 <Form action="/students" method="post" :transform="transformStudentData">18 <!-- Champs du formulaire -->19 </Form>20</template>1<script setup>2const transformStudentData = (data) => {3 return {4 ...data,5 // Convertir le tableau en JSON si nécessaire6 programs: JSON.stringify(data.programs),7 // Formatter la date de naissance8 birth_date: data.birth_date ? new Date(data.birth_date).toISOString() : null,9 // Nettoyer les espaces10 name: data.name.trim(),11 email: data.email.toLowerCase().trim()12 };13};14</script>1516<template>17 <Form action="/students" method="post" :transform="transformStudentData">18 <!-- Champs du formulaire -->19 </Form>20</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.
1<Form2 action="/profile"3 method="put"4 :options="{5 preserveScroll: true,6 preserveState: true,7 only: ['user', 'flash']8 }"9>1<Form2 action="/profile"3 method="put"4 :options="{5 preserveScroll: true,6 preserveState: true,7 only: ['user', 'flash']8 }"9>
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.
1<!-- Idéal pour un formulaire de commentaire au milieu d'une longue page -->2<Form3 action="/comments"4 method="post"5 :options="{ preserveScroll: true }"6>1<!-- Idéal pour un formulaire de commentaire au milieu d'une longue page -->2<Form3 action="/comments"4 method="post"5 :options="{ preserveScroll: true }"6>
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.
1<!-- Garde l'état d'un formulaire de recherche ouvert -->2<Form3 action="/profile/update"4 method="put"5 :options="{ preserveState: true }"6>1<!-- Garde l'état d'un formulaire de recherche ouvert -->2<Form3 action="/profile/update"4 method="put"5 :options="{ preserveState: true }"6>
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.
1<Form2 action="/newsletter/subscribe"3 method="post"4 :options="{ preserveUrl: true }"5>1<Form2 action="/newsletter/subscribe"3 method="post"4 :options="{ preserveUrl: true }"5>
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.
1<Form2 action="/wizard/step-2"3 method="post"4 :options="{ replace: true }"5>1<Form2 action="/wizard/step-2"3 method="post"4 :options="{ replace: true }"5>
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.
1<!-- Ne recharge que le profil utilisateur et les messages flash -->2<Form3 action="/profile"4 method="put"5 :options="{ only: ['user', 'flash'] }"6>1<!-- Ne recharge que le profil utilisateur et les messages flash -->2<Form3 action="/profile"4 method="put"5 :options="{ only: ['user', 'flash'] }"6>
except (array): Exclut certaines propriétés du rechargement. Utile pour éviter de recharger des données volumineuses non affectées.
1<!-- Ne pas recharger la liste des produits lors de la mise à jour du profil -->2<Form3 action="/profile"4 method="put"5 :options="{ except: ['products', 'categories'] }"6>1<!-- Ne pas recharger la liste des produits lors de la mise à jour du profil -->2<Form3 action="/profile"4 method="put"5 :options="{ except: ['products', 'categories'] }"6>
reset (array): Réinitialise des propriétés spécifiques à leurs valeurs par défaut après la visite.
1<Form2 action="/search"3 method="get"4 :options="{ reset: ['page', 'filters'] }"5>1<Form2 action="/search"3 method="get"4 :options="{ reset: ['page', 'filters'] }"5>
Exemple combiné complet:
1<script setup lang="ts">2import { Form } from '@inertiajs/vue3';3import type { User } from '@/types';45interface Props {6 user: User;7}8const props = defineProps<Props>();9</script>1011<template>12 <Form13 v-bind="ProfileController.update.form(props.user.id)"14 :options="{15 preserveScroll: true, // Garde la position16 preserveState: true, // Conserve l'état des autres composants17 only: ['user', 'flash'], // Ne recharge que user et flash18 onSuccess: () => {19 // Callback après succès20 console.log('Profil mis à jour');21 }22 }"23 >24 <!-- Champs du formulaire -->25 </Form>26</template>1<script setup lang="ts">2import { Form } from '@inertiajs/vue3';3import type { User } from '@/types';45interface Props {6 user: User;7}8const props = defineProps<Props>();9</script>1011<template>12 <Form13 v-bind="ProfileController.update.form(props.user.id)"14 :options="{15 preserveScroll: true, // Garde la position16 preserveState: true, // Conserve l'état des autres composants17 only: ['user', 'flash'], // Ne recharge que user et flash18 onSuccess: () => {19 // Callback après succès20 console.log('Profil mis à jour');21 }22 }"23 >24 <!-- Champs du formulaire -->25 </Form>26</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.
1<!-- Réinitialiser tous les champs - Idéal pour formulaires de création -->2<Form action="/users" method="post" resetOnSuccess>3 <input type="text" name="name" />4 <input type="email" name="email" />5 <button type="submit">Créer</button>6</Form>1<!-- Réinitialiser tous les champs - Idéal pour formulaires de création -->2<Form action="/users" method="post" resetOnSuccess>3 <input type="text" name="name" />4 <input type="email" name="email" />5 <button type="submit">Créer</button>6</Form>
Réinitialisation sélective: Ne réinitialise que certains champs spécifiques.
1<!-- Réinitialiser uniquement le mot de passe après soumission -->2<Form action="/profile" method="put" :resetOnSuccess="['password', 'password_confirmation']">3 <input type="text" name="name" value="John Doe" />4 <input type="password" name="password" />5 <input type="password" name="password_confirmation" />6 <button type="submit">Mettre à jour</button>7</Form>1<!-- Réinitialiser uniquement le mot de passe après soumission -->2<Form action="/profile" method="put" :resetOnSuccess="['password', 'password_confirmation']">3 <input type="text" name="name" value="John Doe" />4 <input type="password" name="password" />5 <input type="password" name="password_confirmation" />6 <button type="submit">Mettre à jour</button>7</Form>
resetOnError: Réinitialise le formulaire en cas d'erreur. Moins courant mais utile pour certains cas.
1<!-- Réinitialiser tout en cas d'erreur -->2<Form action="/payment" method="post" resetOnError>3 <input type="text" name="card_number" />4 <button type="submit">Payer</button>5</Form>67<!-- Réinitialiser des champs sensibles uniquement -->8<Form action="/payment" method="post" :resetOnError="['cvv', 'pin']">9 <input type="text" name="card_number" />10 <input type="text" name="cvv" />11 <button type="submit">Payer</button>12</Form>1<!-- Réinitialiser tout en cas d'erreur -->2<Form action="/payment" method="post" resetOnError>3 <input type="text" name="card_number" />4 <button type="submit">Payer</button>5</Form>67<!-- Réinitialiser des champs sensibles uniquement -->8<Form action="/payment" method="post" :resetOnError="['cvv', 'pin']">9 <input type="text" name="card_number" />10 <input type="text" name="cvv" />11 <button type="submit">Payer</button>12</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.
1<Form action="/profile" method="put" setDefaultsOnSuccess>4 <button type="submit">Sauvegarder</button>5</Form>1<Form action="/profile" method="put" setDefaultsOnSuccess>4 <button type="submit">Sauvegarder</button>5</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é:
1<script setup lang="ts">2import { Form } from '@inertiajs/vue3';34const handleSuccess = () => {5 // Afficher une notification6 toast.success('Profil mis à jour avec succès');7};8</script>910<template>11 <!-- Formulaire d'édition avec mise à jour des valeurs par défaut -->12 <Form13 action="/profile"14 method="put"15 setDefaultsOnSuccess16 @success="handleSuccess"17 >18 <input type="text" name="name" :value="user.name" />19 <button type="submit">Sauvegarder</button>20 </Form>2122 <!-- Formulaire de création avec réinitialisation complète -->23 <Form24 action="/posts"25 method="post"26 resetOnSuccess27 @success="handleSuccess"28 >29 <input type="text" name="title" />30 <textarea name="content"></textarea>31 <button type="submit">Publier</button>32 </Form>3334 <!-- Formulaire sensible avec réinitialisation sélective -->35 <Form36 action="/password/update"37 method="put"38 :resetOnSuccess="['current_password', 'password', 'password_confirmation']"39 >40 <input type="password" name="current_password" />41 <input type="password" name="password" />42 <input type="password" name="password_confirmation" />43 <button type="submit">Changer le mot de passe</button>44 </Form>45</template>1<script setup lang="ts">2import { Form } from '@inertiajs/vue3';34const handleSuccess = () => {5 // Afficher une notification6 toast.success('Profil mis à jour avec succès');7};8</script>910<template>11 <!-- Formulaire d'édition avec mise à jour des valeurs par défaut -->12 <Form13 action="/profile"14 method="put"15 setDefaultsOnSuccess16 @success="handleSuccess"17 >18 <input type="text" name="name" :value="user.name" />19 <button type="submit">Sauvegarder</button>20 </Form>2122 <!-- Formulaire de création avec réinitialisation complète -->23 <Form24 action="/posts"25 method="post"26 resetOnSuccess27 @success="handleSuccess"28 >29 <input type="text" name="title" />30 <textarea name="content"></textarea>31 <button type="submit">Publier</button>32 </Form>3334 <!-- Formulaire sensible avec réinitialisation sélective -->35 <Form36 action="/password/update"37 method="put"38 :resetOnSuccess="['current_password', 'password', 'password_confirmation']"39 >40 <input type="password" name="current_password" />41 <input type="password" name="password" />42 <input type="password" name="password_confirmation" />43 <button type="submit">Changer le mot de passe</button>44 </Form>45</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.
1<Form2 action="/users"3 method="post"4 disable-while-processing5 class="inert:opacity-50 inert:pointer-events-none"6>7 <input type="text" name="name" />8 <button type="submit">Créer</button>9</Form>1<Form2 action="/users"3 method="post"4 disable-while-processing5 class="inert:opacity-50 inert:pointer-events-none"6>7 <input type="text" name="name" />8 <button type="submit">Créer</button>9</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:
12<Form3 disable-while-processing4 class="5 inert:opacity-506 inert:pointer-events-none7 inert:cursor-not-allowed8 transition-opacity duration-2009 "10>12<Form3 disable-while-processing4 class="5 inert:opacity-506 inert:pointer-events-none7 inert:cursor-not-allowed8 transition-opacity duration-2009 "10>
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é:
1<script setup>2import { ref } from 'vue';3</script>45<template>6 <Form7 action="/users"8 method="post"9 disable-while-processing10 class="relative inert:opacity-70"11 v-slot="{ processing }"12 >13 <!-- Overlay de chargement -->14 <div15 v-if="processing"16 class="absolute inset-0 flex items-center justify-center bg-white/80 z-10"17 >18 <svg class="animate-spin h-8 w-8 text-blue-500" viewBox="0 0 24 24">19 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>20 <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"/>21 </svg>22 </div>2324 <input type="text" name="name" />25 <button type="submit" :disabled="processing">26 {{ processing ? 'Création...' : 'Créer' }}27 </button>28 </Form>29</template>1<script setup>2import { ref } from 'vue';3</script>45<template>6 <Form7 action="/users"8 method="post"9 disable-while-processing10 class="relative inert:opacity-70"11 v-slot="{ processing }"12 >13 <!-- Overlay de chargement -->14 <div15 v-if="processing"16 class="absolute inset-0 flex items-center justify-center bg-white/80 z-10"17 >18 <svg class="animate-spin h-8 w-8 text-blue-500" viewBox="0 0 24 24">19 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>20 <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"/>21 </svg>22 </div>2324 <input type="text" name="name" />25 <button type="submit" :disabled="processing">26 {{ processing ? 'Création...' : 'Créer' }}27 </button>28 </Form>29</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.
1<Form2 action="/users"3 method="post"4 @before="handleBefore"5 @start="handleStart"6 @progress="handleProgress"7 @success="handleSuccess"8 @error="handleError"9 @finish="handleFinish"10 @cancel="handleCancel"11>1<Form2 action="/users"3 method="post"4 @before="handleBefore"5 @start="handleStart"6 @progress="handleProgress"7 @success="handleSuccess"8 @error="handleError"9 @finish="handleFinish"10 @cancel="handleCancel"11>
Description détaillée de chaque événement:
@before: Se déclenche avant l'envoi de la requête. Retourner false annule la soumission.
1<script setup>2const handleBefore = (visit) => {3 // Validation personnalisée avant soumission4 if (!confirm('Êtes-vous sûr de vouloir continuer ?')) {5 return false; // Annule la soumission6 }78 // Logger l'action9 console.log('Formulaire sur le point d\'être soumis', visit);10};11</script>1213<template>14 <Form action="/users" method="post" @before="handleBefore">15 <input type="text" name="name" />16 </Form>17</template>1<script setup>2const handleBefore = (visit) => {3 // Validation personnalisée avant soumission4 if (!confirm('Êtes-vous sûr de vouloir continuer ?')) {5 return false; // Annule la soumission6 }78 // Logger l'action9 console.log('Formulaire sur le point d\'être soumis', visit);10};11</script>1213<template>14 <Form action="/users" method="post" @before="handleBefore">15 <input type="text" name="name" />16 </Form>17</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.
1<script setup lang="ts">2import { Form } from '@inertiajs/vue3';34interface Props {5 action: string;6 method: string;7 preserveScroll?: boolean;8}910const props = withDefaults(defineProps<Props>(), {11 preserveScroll: true12});13</script>1415<template>16 <Form17 :action="props.action"18 :method="props.method"19 :options="{ preserveScroll: props.preserveScroll }"20 disable-while-processing21 class="inert:opacity-50 inert:pointer-events-none transition-opacity"22 >23 <slot />24 </Form>25</template>1<script setup lang="ts">2import { Form } from '@inertiajs/vue3';34interface Props {5 action: string;6 method: string;7 preserveScroll?: boolean;8}910const props = withDefaults(defineProps<Props>(), {11 preserveScroll: true12});13</script>1415<template>16 <Form17 :action="props.action"18 :method="props.method"19 :options="{ preserveScroll: props.preserveScroll }"20 disable-while-processing21 class="inert:opacity-50 inert:pointer-events-none transition-opacity"22 >23 <slot />24 </Form>25</template>
Utilisation:
1<template>2 <AppForm action="/users" method="post">3 <input type="text" name="name" />4 <button type="submit">Créer</button>5 </AppForm>6</template>1<template>2 <AppForm action="/users" method="post">3 <input type="text" name="name" />4 <button type="submit">Créer</button>5 </AppForm>6</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.
1<script setup lang="ts">2interface Props {3 name: string;4 label: string;5 type?: string;6 error?: string;7 modelValue?: string;8 placeholder?: string;9 required?: boolean;10}1112const props = withDefaults(defineProps<Props>(), {13 type: 'text',14 required: false15});16</script>1718<template>19 <div class="form-group mb-4">20 <label21 :for="props.name"22 class="block text-sm font-medium text-gray-700 mb-1"23 >24 {{ props.label }}25 <span v-if="props.required" class="text-red-500">*</span>26 </label>2728 <input29 :id="props.name"30 :name="props.name"31 :type="props.type"32 :value="props.modelValue"33 :placeholder="props.placeholder"34 :required="props.required"35 class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"36 :class="{ 'border-red-500': props.error }"37 />3839 <span v-if="props.error" class="text-red-500 text-sm mt-1 block">40 {{ props.error }}41 </span>42 </div>43</template>1<script setup lang="ts">2interface Props {3 name: string;4 label: string;5 type?: string;6 error?: string;7 modelValue?: string;8 placeholder?: string;9 required?: boolean;10}1112const props = withDefaults(defineProps<Props>(), {13 type: 'text',14 required: false15});16</script>1718<template>19 <div class="form-group mb-4">20 <label21 :for="props.name"22 class="block text-sm font-medium text-gray-700 mb-1"23 >24 {{ props.label }}25 <span v-if="props.required" class="text-red-500">*</span>26 </label>2728 <input29 :id="props.name"30 :name="props.name"31 :type="props.type"32 :value="props.modelValue"33 :placeholder="props.placeholder"34 :required="props.required"35 class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"36 :class="{ 'border-red-500': props.error }"37 />3839 <span v-if="props.error" class="text-red-500 text-sm mt-1 block">40 {{ props.error }}41 </span>42 </div>43</template>
Utilisation:
1<template>2 <Form v-bind="StudentController.store.form()" v-slot="{ errors }">3 <FormField4 name="name"5 label="Nom complet"6 :error="errors.name"7 :model-value="student?.name"8 placeholder="ex: Jean Dupont"9 required10 />1112 <FormField13 name="email"14 label="Email"15 type="email"16 :error="errors.email"17 :model-value="student?.email"18 required19 />2021 <button type="submit">Créer</button>22 </Form>23</template>1<template>2 <Form v-bind="StudentController.store.form()" v-slot="{ errors }">3 <FormField4 name="name"5 label="Nom complet"6 :error="errors.name"7 :model-value="student?.name"8 placeholder="ex: Jean Dupont"9 required10 />1112 <FormField13 name="email"14 label="Email"15 type="email"16 :error="errors.email"17 :model-value="student?.email"18 required19 />2021 <button type="submit">Créer</button>22 </Form>23</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.
1<script setup>2import { ref } from 'vue';34const localErrors = ref({});56const handleBefore = (event) => {7 localErrors.value = {};89 const formData = new FormData(event.target);10 const email = formData.get('email');11 const password = formData.get('password');1213 // Validation email14 if (!email || !email.includes('@')) {15 localErrors.value.email = 'Format d\'email invalide';16 }1718 // Validation mot de passe19 if (!password || password.length < 8) {20 localErrors.value.password = 'Le mot de passe doit contenir au moins 8 caractères';21 }2223 // Annuler si erreurs24 if (Object.keys(localErrors.value).length > 0) {25 return false;26 }2728 return true;29};30</script>3132<template>33 <Form action="/users" method="post" @before="handleBefore" v-slot="{ errors }">34 <div class="form-group">35 <input type="email" name="email" />36 <span v-if="localErrors.email || errors.email" class="text-red-500">37 {{ localErrors.email || errors.email }}38 </span>39 </div>4041 <div class="form-group">42 <input type="password" name="password" />43 <span v-if="localErrors.password || errors.password" class="text-red-500">44 {{ localErrors.password || errors.password }}45 </span>46 </div>4748 <button type="submit">Créer</button>49 </Form>50</template>1<script setup>2import { ref } from 'vue';34const localErrors = ref({});56const handleBefore = (event) => {7 localErrors.value = {};89 const formData = new FormData(event.target);10 const email = formData.get('email');11 const password = formData.get('password');1213 // Validation email14 if (!email || !email.includes('@')) {15 localErrors.value.email = 'Format d\'email invalide';16 }1718 // Validation mot de passe19 if (!password || password.length < 8) {20 localErrors.value.password = 'Le mot de passe doit contenir au moins 8 caractères';21 }2223 // Annuler si erreurs24 if (Object.keys(localErrors.value).length > 0) {25 return false;26 }2728 return true;29};30</script>3132<template>33 <Form action="/users" method="post" @before="handleBefore" v-slot="{ errors }">34 <div class="form-group">35 <input type="email" name="email" />36 <span v-if="localErrors.email || errors.email" class="text-red-500">37 {{ localErrors.email || errors.email }}38 </span>39 </div>4041 <div class="form-group">42 <input type="password" name="password" />43 <span v-if="localErrors.password || errors.password" class="text-red-500">44 {{ localErrors.password || errors.password }}45 </span>46 </div>4748 <button type="submit">Créer</button>49 </Form>50</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.
1<script setup>2import { ref } from 'vue';3import { Form } from '@inertiajs/vue3';45const formRef = ref();6const autoSaveEnabled = ref(true);78// Soumission manuelle9const submitForm = () => {10 if (formRef.value) {11 formRef.value.submit();12 }13};1415// Réinitialisation manuelle16const resetForm = () => {17 if (formRef.value) {18 formRef.value.reset();19 }20};2122// Auto-save toutes les 30 secondes23setInterval(() => {24 if (autoSaveEnabled.value && formRef.value) {25 formRef.value.submit();26 }27}, 30000);28</script>2930<template>31 <div>32 <Form ref="formRef" action="/draft/save" method="post">33 <textarea name="content" rows="10"></textarea>34 </Form>3536 <div class="actions mt-4 space-x-2">37 <button @click="submitForm" class="btn-primary">38 Sauvegarder maintenant39 </button>4041 <button @click="resetForm" class="btn-secondary">42 Réinitialiser43 </button>4445 <label class="inline-flex items-center">46 <input47 type="checkbox"48 v-model="autoSaveEnabled"49 class="mr-2"50 />51 Auto-save activé52 </label>53 </div>54 </div>55</template>1<script setup>2import { ref } from 'vue';3import { Form } from '@inertiajs/vue3';45const formRef = ref();6const autoSaveEnabled = ref(true);78// Soumission manuelle9const submitForm = () => {10 if (formRef.value) {11 formRef.value.submit();12 }13};1415// Réinitialisation manuelle16const resetForm = () => {17 if (formRef.value) {18 formRef.value.reset();19 }20};2122// Auto-save toutes les 30 secondes23setInterval(() => {24 if (autoSaveEnabled.value && formRef.value) {25 formRef.value.submit();26 }27}, 30000);28</script>2930<template>31 <div>32 <Form ref="formRef" action="/draft/save" method="post">33 <textarea name="content" rows="10"></textarea>34 </Form>3536 <div class="actions mt-4 space-x-2">37 <button @click="submitForm" class="btn-primary">38 Sauvegarder maintenant39 </button>4041 <button @click="resetForm" class="btn-secondary">42 Réinitialiser43 </button>4445 <label class="inline-flex items-center">46 <input47 type="checkbox"48 v-model="autoSaveEnabled"49 class="mr-2"50 />51 Auto-save activé52 </label>53 </div>54 </div>55</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.