Formulaire
Formulaires avec Inertia.js
Dans ce chapitre, nous allons explorer les deux approches pour gérer les formulaires avec Inertia.js : le composant <Form> (recommandé pour la majorité des cas) et le helper useForm (pour un contrôle programmatique avancé). Nous verrons également comment gérer l'upload de fichiers et l'affichage de la progression.
Le Composant <Form> : L'Approche Moderne
Le composant <Form> est l'approche recommandée pour créer des formulaires avec Inertia.js. Il se comporte comme un formulaire HTML classique, sans nécessiter de v-model ni de gestion manuelle de l'état.
Exemple : Créer une Maison
Voici un formulaire complet pour créer un bien immobilier dans une application d'agence :
<script setup>
import { Form } from '@inertiajs/vue3';
import { store, index } from '@/actions/App/Http/Controllers/HouseController';
import AppLayout from '@/layouts/AppLayout.vue';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
const props = defineProps({
houseTypes: Array,
});
</script>
<template>
<AppLayout>
<div class="container mx-auto max-w-3xl px-3 py-8">
<Card>
<CardHeader>
<CardTitle>Ajouter une maison</CardTitle>
<CardDescription>
Remplissez les informations ci-dessous pour ajouter un
nouveau bien immobilier
</CardDescription>
</CardHeader>
<CardContent>
<Form
:action="store()"
#default="{ errors, processing }"
class="space-y-6"
>
<!-- Titre -->
<div class="space-y-2">
<Label for="title">
Titre
<span class="text-destructive">*</span>
</Label>
<Input
id="title"
type="text"
name="title"
placeholder="Ex: Belle maison à Paris"
/>
<p
v-if="errors.title"
class="text-sm text-destructive"
>
{{ errors.title }}
</p>
</div>
<!-- Type de bien -->
<div class="space-y-2">
<Label for="house_type_id">
Type de bien
<span class="text-destructive">*</span>
</Label>
<select
id="house_type_id"
name="house_type_id"
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors placeholder:text-muted-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
>
<option value="">Sélectionnez un type</option>
<option
v-for="type in houseTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</option>
</select>
<p
v-if="errors.house_type_id"
class="text-sm text-destructive"
>
{{ errors.house_type_id }}
</p>
</div>
<!-- Prix -->
<div class="space-y-2">
<Label for="price">
Prix (€)
<span class="text-destructive">*</span>
</Label>
<Input
id="price"
type="number"
name="price"
step="0.01"
placeholder="250000"
/>
<p
v-if="errors.price"
class="text-sm text-destructive"
>
{{ errors.price }}
</p>
</div>
<!-- Adresse -->
<div class="space-y-2">
<Label for="address">
Adresse
<span class="text-destructive">*</span>
</Label>
<Input
id="address"
type="text"
name="address"
placeholder="123 rue de la Paix, 75001 Paris"
/>
<p
v-if="errors.address"
class="text-sm text-destructive"
>
{{ errors.address }}
</p>
</div>
<!-- Chambres et Taille -->
<div class="grid gap-4 md:grid-cols-2">
<div class="space-y-2">
<Label for="bedrooms">
Nombre de chambres
<span class="text-destructive">*</span>
</Label>
<Input
id="bedrooms"
type="number"
name="bedrooms"
placeholder="3"
min="0"
/>
<p
v-if="errors.bedrooms"
class="text-sm text-destructive"
>
{{ errors.bedrooms }}
</p>
</div>
<div class="space-y-2">
<Label for="size">
Taille (m²)
<span class="text-destructive">*</span>
</Label>
<Input
id="size"
type="number"
name="size"
step="0.01"
placeholder="120.50"
min="0"
/>
<p
v-if="errors.size"
class="text-sm text-destructive"
>
{{ errors.size }}
</p>
</div>
</div>
<!-- Description -->
<div class="space-y-2">
<Label for="description">Description</Label>
<Textarea
id="description"
name="description"
rows="5"
placeholder="Décrivez le bien..."
/>
<p
v-if="errors.description"
class="text-sm text-destructive"
>
{{ errors.description }}
</p>
</div>
<!-- Boutons -->
<div class="flex gap-4">
<Button type="submit" :disabled="processing">
{{
processing
? 'Enregistrement...'
: 'Créer la maison'
}}
</Button>
<Button
type="button"
variant="outline"
@click="$inertia.visit(index())"
>
Annuler
</Button>
</div>
</Form>
</CardContent>
</Card>
</div>
</AppLayout>
</template>
Points Clés du Composant <Form>
-
Pas de
v-model: Chaque input a simplement un attributname. Le composant gère automatiquement la collecte des données. -
Intégration Wayfinder : L'action
:action="store()"utilise Wayfinder pour générer automatiquement l'URL et la méthode HTTP. -
Slot Props :
#default="{ errors, processing }"expose les erreurs de validation et l'état de traitement du formulaire. -
Affichage des Erreurs : Les erreurs sont accessibles via
errors.nomDuChampet s'affichent automatiquement après validation côté serveur. -
État de Chargement : La propriété
processingpermet de désactiver le bouton pendant la soumission et d'afficher un spinner.
Modification avec Valeurs Par Défaut
Pour modifier une ressource existante, utilisez l'attribut defaultValue (ou selected pour les <select>) :
<script setup>
import { Form } from '@inertiajs/vue3';
import { update, index } from '@/actions/App/Http/Controllers/HouseController';
const props = defineProps({
house: Object,
houseTypes: Array,
});
</script>
<template>
<Form
:action="update(house.id)"
#default="{ errors, processing }"
class="space-y-6"
>
<!-- Titre avec valeur par défaut -->
<div class="space-y-2">
<Label for="title">Titre</Label>
<Input
id="title"
type="text"
name="title"
:defaultValue="house.title"
placeholder="Ex: Belle maison à Paris"
/>
<p v-if="errors.title" class="text-sm text-destructive">
{{ errors.title }}
</p>
</div>
<!-- Select avec valeur par défaut -->
<div class="space-y-2">
<Label for="house_type_id">Type de bien</Label>
<select id="house_type_id" name="house_type_id">
<option
v-for="type in houseTypes"
:key="type.id"
:value="type.id"
:selected="type.id === house.house_type_id"
>
{{ type.name }}
</option>
</select>
</div>
<!-- Prix avec valeur par défaut -->
<div class="space-y-2">
<Label for="price">Prix (€)</Label>
<Input
id="price"
type="number"
name="price"
step="0.01"
:defaultValue="house.price"
/>
</div>
<!-- Autres champs... -->
<Button type="submit" :disabled="processing">
Mettre à jour
</Button>
</Form>
</template>
Note importante : Utilisez :defaultValue (avec deux-points) pour les valeurs dynamiques, et defaultValue (sans deux-points) pour les valeurs statiques.
Gestion Côté Serveur avec Laravel
Création d'une Ressource
use Illuminate\Http\Request;
use App\Models\House;
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'house_type_id' => 'required|exists:house_types,id',
'price' => 'required|numeric|min:0',
'address' => 'required|string|max:255',
'bedrooms' => 'required|integer|min:0',
'size' => 'required|numeric|min:0',
'description' => 'nullable|string',
]);
House::create($validated);
return redirect()->route('houses.index')
->with('success', 'Maison créée avec succès.');
}
Mise à Jour d'une Ressource
public function update(Request $request, House $house)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'house_type_id' => 'required|exists:house_types,id',
'price' => 'required|numeric|min:0',
'address' => 'required|string|max:255',
'bedrooms' => 'required|integer|min:0',
'size' => 'required|numeric|min:0',
'description' => 'nullable|string',
]);
$house->update($validated);
return redirect()->route('houses.index')
->with('success', 'Maison mise à jour avec succès.');
}
Note : Inertia gère automatiquement les erreurs de validation. Si $request->validate() échoue, les erreurs sont renvoyées au frontend et accessibles via errors dans le slot props.
Le Helper useForm : Pour un Contrôle Avancé
Le helper useForm est utile lorsque vous avez besoin d'un contrôle programmatique sur vos formulaires. C'est particulièrement adapté pour :
- Les uploads de fichiers avec gestion manuelle
- Les transformations de données complexes avant soumission
- Les formulaires avec logique métier sophistiquée
Exemple : Upload d'Images
Dans notre application d'agence immobilière, l'upload d'images pour une maison est géré via useForm :
<script setup>
import { useForm } from '@inertiajs/vue3';
const props = defineProps({
house: Object,
});
// Form pour l'upload d'images
const imageForm = useForm({
images: null,
});
const handleImageChange = (event) => {
const files = event.target.files;
if (files && files.length > 0) {
// Convertir FileList en Array
imageForm.images = Array.from(files);
}
};
const uploadImages = () => {
imageForm.post(`/houses/${props.house.id}/images`, {
preserveScroll: true,
onSuccess: () => {
imageForm.reset();
// Reset l'input file
const input = document.getElementById('images');
if (input) {
input.value = '';
}
},
});
};
</script>
<template>
<form @submit.prevent="uploadImages" class="space-y-4">
<div class="space-y-2">
<Label for="images">Ajouter de nouvelles images</Label>
<input
id="images"
type="file"
multiple
accept="image/*"
@input="handleImageChange"
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
/>
<p class="text-sm text-muted-foreground">
Vous pouvez sélectionner plusieurs images (max 2MB par image)
</p>
<p
v-if="imageForm.errors['images.0']"
class="text-sm text-destructive"
>
{{ imageForm.errors['images.0'] }}
</p>
</div>
<!-- Progression upload -->
<div v-if="imageForm.progress" class="space-y-2">
<div class="flex justify-between text-sm">
<span>Upload en cours...</span>
<span class="font-medium">
{{ imageForm.progress.percentage }}%
</span>
</div>
<div class="h-2 w-full rounded-full bg-secondary">
<div
class="h-2 rounded-full bg-primary transition-all"
:style="{
width: `${imageForm.progress.percentage}%`,
}"
/>
</div>
</div>
<Button
type="submit"
:disabled="imageForm.processing || !imageForm.images"
>
{{
imageForm.processing
? 'Upload en cours...'
: 'Ajouter les images'
}}
</Button>
</form>
</template>
Points Clés du useForm
-
Initialisation :
const imageForm = useForm({ images: null }) -
Liaison Manuelle : Avec
useForm, vous gérez manuellement les changements viav-modelou des handlers d'événements. -
Méthodes de Soumission :
imageForm.post(url, options)imageForm.put(url, options)imageForm.patch(url, options)imageForm.delete(url, options)
-
Options de Soumission :
preserveScroll: true: Maintient la position du scrollonSuccess: () => {}: Callback après succèsonError: () => {}: Callback en cas d'erreur
-
Propriétés Disponibles :
imageForm.processing: Indique si le formulaire est en cours de soumissionimageForm.progress: Objet contenantpercentagepour l'upload de fichiersimageForm.errors: Objet contenant les erreurs de validationimageForm.wasSuccessful:trueaprès une soumission réussie
-
Méthodes Utiles :
imageForm.reset(): Réinitialise le formulaireimageForm.clearErrors(): Efface toutes les erreursimageForm.setError('field', 'message'): Définit une erreur manuellement
Upload de Fichiers : Gestion Côté Serveur
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use App\Models\House;
use App\Models\HouseImage;
public function uploadImages(Request $request, House $house)
{
$request->validate([
'images' => 'required|array',
'images.*' => 'image|mimes:jpeg,png,jpg,gif|max:2048',
]);
foreach ($request->file('images') as $image) {
$path = $image->store('houses', 'public');
HouseImage::create([
'house_id' => $house->id,
'path' => $path,
]);
}
return back()->with('success', 'Images ajoutées avec succès.');
}
Note : Inertia convertit automatiquement les requêtes contenant des fichiers en FormData. Aucune configuration supplémentaire n'est nécessaire.
Suppression d'une Image
Pour supprimer une image, utilisez directement le router d'Inertia :
<script setup>
import { router } from '@inertiajs/vue3';
const props = defineProps({
house: Object,
});
const deleteImage = (imageId) => {
if (confirm('Êtes-vous sûr de vouloir supprimer cette image ?')) {
router.delete(`/houses/${props.house.id}/images/${imageId}`, {
preserveScroll: true,
});
}
};
</script>
<template>
<Button
size="sm"
variant="destructive"
@click="deleteImage(image.id)"
>
Supprimer
</Button>
</template>
Côté Serveur :
use App\Models\HouseImage;
use Illuminate\Support\Facades\Storage;
public function deleteImage(House $house, HouseImage $image)
{
// Vérifier que l'image appartient bien à cette maison
if ($image->house_id !== $house->id) {
abort(403);
}
// Supprimer le fichier physique
Storage::disk('public')->delete($image->path);
// Supprimer l'enregistrement en base
$image->delete();
return back()->with('success', 'Image supprimée avec succès.');
}
Quand Utiliser <Form> vs useForm ?
| Critère | <Form> Component |
useForm Helper |
|---|---|---|
| Simplicité | ✅ Très simple, pas de v-model |
⚠️ Plus verbeux |
| Cas d'usage | CRUD standard, formulaires classiques | Logique complexe, transformations |
| Upload de fichiers | ✅ Supporte nativement | ✅ Avec gestion manuelle |
| Valeurs par défaut | defaultValue / selected |
Pré-remplir avec useForm(data) |
| Contrôle programmatique | ⚠️ Limité (via refs) | ✅ Total |
| Recommandation | Privilégier par défaut | Pour les cas avancés |
Règle générale : Utilisez <Form> sauf si vous avez une raison spécifique d'utiliser useForm.
Options Avancées
Avec <Form> Component
<Form
:action="update(house.id)"
:options="{
preserveScroll: true,
preserveState: true,
only: ['house', 'flash'],
}"
resetOnSuccess
#default="{ errors, processing, wasSuccessful }"
>
<!-- Vos champs -->
<div v-if="wasSuccessful" class="text-green-600">
Modifications enregistrées !
</div>
</Form>
Avec useForm Helper
form.post('/houses', {
preserveScroll: true,
onSuccess: () => {
form.reset();
alert('Maison créée avec succès !');
},
onError: (errors) => {
console.error('Erreurs:', errors);
},
});
Conclusion
Inertia.js propose deux approches puissantes pour gérer les formulaires :
- Le composant
<Form>: Simple, déclaratif, idéal pour la majorité des cas - Le helper
useForm: Contrôle total, parfait pour les besoins avancés
Dans les deux cas, Inertia gère automatiquement :
- La conversion en
FormDatapour les fichiers - La gestion des erreurs de validation Laravel
- Le tracking de progression pour les uploads
- Les états de chargement et de succès
En combinant ces outils avec Wayfinder et shadcn-vue, vous obtenez une expérience de développement moderne, type-safe, et productive. 🚀
Ressources
- Documentation Inertia.js - Forms
- Documentation Inertia.js - File Uploads
- Documentation Laravel - Validation
- Documentation Laravel - File Storage
- Projet Agence immobilière sur GitHub : https://github.com/opmvpc/immo
