Aller au contenu
PROJET WEB DYNAMIQUE

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>

  1. Pas de v-model : Chaque input a simplement un attribut name. Le composant gère automatiquement la collecte des données.

  2. Intégration Wayfinder : L'action :action="store()" utilise Wayfinder pour générer automatiquement l'URL et la méthode HTTP.

  3. Slot Props : #default="{ errors, processing }" expose les erreurs de validation et l'état de traitement du formulaire.

  4. Affichage des Erreurs : Les erreurs sont accessibles via errors.nomDuChamp et s'affichent automatiquement après validation côté serveur.

  5. État de Chargement : La propriété processing permet 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

  1. Initialisation : const imageForm = useForm({ images: null })

  2. Liaison Manuelle : Avec useForm, vous gérez manuellement les changements via v-model ou des handlers d'événements.

  3. Méthodes de Soumission :

    • imageForm.post(url, options)
    • imageForm.put(url, options)
    • imageForm.patch(url, options)
    • imageForm.delete(url, options)
  4. Options de Soumission :

    • preserveScroll: true : Maintient la position du scroll
    • onSuccess: () => {} : Callback après succès
    • onError: () => {} : Callback en cas d'erreur
  5. Propriétés Disponibles :

    • imageForm.processing : Indique si le formulaire est en cours de soumission
    • imageForm.progress : Objet contenant percentage pour l'upload de fichiers
    • imageForm.errors : Objet contenant les erreurs de validation
    • imageForm.wasSuccessful : true après une soumission réussie
  6. Méthodes Utiles :

    • imageForm.reset() : Réinitialise le formulaire
    • imageForm.clearErrors() : Efface toutes les erreurs
    • imageForm.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 :

  1. Le composant <Form> : Simple, déclaratif, idéal pour la majorité des cas
  2. Le helper useForm : Contrôle total, parfait pour les besoins avancés

Dans les deux cas, Inertia gère automatiquement :

  • La conversion en FormData pour 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