Save France
Composables

useForm

Composable pour la gestion du formulaire multi-étapes de contrat

Description

Composable pour la gestion d'un formulaire multi-étapes avec navigation, validation et persistance des données. Il synchronise automatiquement l'état avec les routes de l'application.

Vue d'ensemble

Le composable useForm gère un formulaire complexe en plusieurs étapes :

  1. Localisation (code postal)
  2. Équipements
  3. Tarifs
  4. Contact
  5. Contrat
  6. Paiement

Il synchronise automatiquement l'étape courante avec la route active et persiste les données dans le state Nuxt.

flowchart TD
    A[useForm] --> B[État formulaire]
    A --> C[Navigation]
    A --> D[Validation]
    B --> E[Address]
    B --> F[Equipments]
    B --> G[Contact]
    B --> H[Offer]
    C --> I[currentStep]
    C --> J[Routes]
    D --> K[isStepCompleted]

Structure des données

CompleteForm

interface CompleteForm {
  address: AddressForm;
  equipments: EquipmentForm[];
  contact: ContactForm;
  offer: OfferForm;
}

AddressForm

interface AddressForm {
  street: string;
  postalCode: string;
  city: string;
  lessThanTwoYears: boolean;
}

EquipmentForm

interface EquipmentForm {
  equipmentType: string;
  brand: string;
  installedAt: number | null;
  uiDuctable: number;
  uiNotDuctable: number;
  isMultiSplit: boolean;
  internUnits: Array<{ value: string; label: string }>;
}

ContactForm

interface ContactForm {
  civility: string;
  name: string;
  lastname: string;
  email: string;
  phone: string;
  street: string;
  additionalInfo: string;
  postalCode: string;
  city: string;
  isPro: boolean;
  companyName: string;
  siret: string;
  vatNumber: string;
  billingDifferent: boolean;
  billingCivility: string;
  billingName: string;
  billingLastName: string;
  billingStreet: string;
  billingAdditionalInfo: string;
  billingPostalCode: string;
  billingCity: string;
}

OfferForm

interface OfferForm {
  maintenanceOffer: string;
  price: number;
  showPunctual: boolean;
  paymentType: "subscription" | "one_shot";
}

API

État

currentStep: Ref<number>

Étape courante du formulaire (0-indexé).

const { currentStep } = useForm();
console.log(currentStep.value); // 0, 1, 2, ...

totalSteps: number

Nombre total d'étapes (5).

formData: Ref<CompleteForm>

Données du formulaire (réactif, persisté dans le state).

const { formData } = useForm();
formData.value.address.postalCode = '75001';

errors: Ref<Record<string, string>>

Erreurs de validation par champ.

const { errors } = useForm();
errors.value['address.postalCode'] = 'Code postal invalide';

nextStep(): void

Passe à l'étape suivante. Navigation automatique vers la route correspondante.

const { nextStep } = useForm();
nextStep(); // Étape 0 → 1

prevStep(): void

Revient à l'étape précédente. Émet un événement Matomo de tracking.

const { prevStep } = useForm();
prevStep(); // Étape 1 → 0

goToStep(step: number): void

Va directement à une étape spécifique.

const { goToStep } = useForm();
goToStep(3); // Aller à l'étape 3

Paramètres :

  • step : Numéro de l'étape (0 à totalSteps)

Mise à jour des données

updatePostalCode(data: Partial<AddressForm>): void

Met à jour les données d'adresse et synchronise avec les champs de contact.

const { updatePostalCode } = useForm();
updatePostalCode({
  postalCode: '75001',
  city: 'Paris',
  street: '123 Rue Example',
});

updateEquipment(data: EquipmentForm[]): void

Met à jour la liste complète des équipements.

const { updateEquipment } = useForm();
updateEquipment([
  {
    equipmentType: '/api/types/1',
    brand: '/api/brands/1',
    installedAt: 2020,
    // ...
  },
]);

updateEquipmentItem(index: number, data: Partial<EquipmentForm>): void

Met à jour un équipement spécifique.

const { updateEquipmentItem } = useForm();
updateEquipmentItem(0, {
  brand: '/api/brands/2',
});

addEquipment(): void

Ajoute un nouvel équipement vide à la liste.

const { addEquipment } = useForm();
addEquipment();

removeEquipment(index: number): void

Supprime un équipement de la liste (minimum 1 équipement requis).

const { removeEquipment } = useForm();
removeEquipment(0);

updateContact(data: Partial<ContactForm>): void

Met à jour les données de contact.

const { updateContact } = useForm();
updateContact({
  name: 'John',
  lastname: 'Doe',
  email: 'john@example.com',
});

updateOffer(data: Partial<OfferForm>): void

Met à jour l'offre sélectionnée.

const { updateOffer } = useForm();
updateOffer({
  maintenanceOffer: '/api/offers/1',
  paymentType: 'subscription',
});

Validation

isStepCompleted(step: number): boolean

Vérifie si une étape est complétée selon des critères spécifiques.

const { isStepCompleted } = useForm();
if (isStepCompleted(1)) {
  // Étape 1 complétée
}

Critères de validation :

  • Étape 1 : postalCode et city requis
  • Étape 2 : Au moins un équipement avec brand, equipmentType et installedAt
  • Étape 3 : maintenanceOffer requis
  • Étape 4 : name, lastname, email, phone requis
  • Étape 5-6 : Toujours true

getStepProgress(): number

Calcule le pourcentage de progression (0-100).

const { getStepProgress } = useForm();
const progress = getStepProgress(); // 0-100

Gestion des erreurs

setError(field: string, message: string): void

Définit un message d'erreur pour un champ.

const { setError } = useForm();
setError('address.postalCode', 'Code postal invalide');

clearError(field: string): void

Supprime l'erreur d'un champ.

const { clearError } = useForm();
clearError('address.postalCode');

clearAllErrors(): void

Supprime toutes les erreurs.

const { clearAllErrors } = useForm();
clearAllErrors();

Utilitaires

resetForm(): void

Réinitialise le formulaire à l'état initial.

const { resetForm } = useForm();
resetForm();

setValueFromApi(apiRequest: ApiRequest): void

Initialise le formulaire depuis les données de l'API. Ne fonctionne que si le formulaire est à l'état initial.

const { setValueFromApi } = useForm();
const { data } = await $fetch(`/api/requests/${requestId}`);
setValueFromApi(data);

Transformation des données :

  • Convertit les objets API (@id) en strings
  • Transforme les dates en années
  • Crée les unités internes depuis uiDuctable et uiNotDuctable
  • Transforme les numéros de téléphone (+33 → 0)

Routes

Le composable mappe automatiquement les étapes aux routes :

const stepRoutes = [
  '/postal-code',
  '/[reqID]/equipment',
  '/[reqID]/pricing',
  '/[reqID]/contact',
  '/[reqID]/contract',
  '/[reqID]/payment',
];

La navigation est synchronisée automatiquement via un watch sur currentStep.

Utilisation

Exemple de base

<script setup lang="ts">
const {
  currentStep,
  formData,
  nextStep,
  prevStep,
  isStepCompleted,
} = useForm();
</script>

<template>
  <div>
    <p>Étape {{ currentStep + 1 }} sur {{ totalSteps }}</p>
    <UButton @click="prevStep" :disabled="currentStep === 0">
      Précédent
    </UButton>
    <UButton 
      @click="nextStep" 
      :disabled="!isStepCompleted(currentStep)"
    >
      Suivant
    </UButton>
  </div>
</template>

Formulaire d'adresse

<template>
  <UForm @submit="handleSubmit">
    <UFormGroup label="Code postal" name="postalCode">
      <UInput 
        v-model="formData.address.postalCode"
        @update:model-value="updatePostalCode({ postalCode: $event })"
      />
    </UFormGroup>
    
    <UFormGroup label="Ville" name="city">
      <UInput v-model="formData.address.city" />
    </UFormGroup>
    
    <UButton type="submit">Continuer</UButton>
  </UForm>
</template>

<script setup lang="ts">
const { formData, updatePostalCode, nextStep } = useForm();

function handleSubmit() {
  updatePostalCode({
    postalCode: formData.value.address.postalCode,
    city: formData.value.address.city,
  });
  nextStep();
}
</script>

Gestion des équipements

<template>
  <div>
    <div v-for="(equipment, index) in formData.equipments" :key="index">
      <EquipmentForm
        :equipment="equipment"
        @update="updateEquipmentItem(index, $event)"
      />
      <UButton 
        v-if="formData.equipments.length > 1"
        @click="removeEquipment(index)"
      >
        Supprimer
      </UButton>
    </div>
    
    <UButton @click="addEquipment">Ajouter un équipement</UButton>
  </div>
</template>

<script setup lang="ts">
const {
  formData,
  addEquipment,
  removeEquipment,
  updateEquipmentItem,
} = useForm();
</script>

Initialisation depuis l'API

<script setup lang="ts">
const route = useRoute();
const { reqID } = route.params;
const { setValueFromApi } = useForm();

onMounted(async () => {
  if (reqID) {
    const { data } = await $fetch(`/api/requests/${reqID}`);
    setValueFromApi(data);
  }
});
</script>

Bonnes pratiques

  1. Synchronisation
    • Le composable synchronise automatiquement avec les routes
    • Ne modifiez pas currentStep directement, utilisez les fonctions de navigation
  2. Validation
    • Utilisez isStepCompleted() pour désactiver le bouton "Suivant"
    • Validez les données avant de passer à l'étape suivante
  3. Erreurs
    • Utilisez setError() pour afficher les erreurs de validation
    • Nettoyez les erreurs avec clearError() lors de la correction
  4. Performance
    • Les données sont persistées dans le state Nuxt
    • Évitez de créer plusieurs instances du composable

Dépannage

La navigation ne fonctionne pas

  1. Vérifiez que les routes correspondent aux stepRoutes
  2. Vérifiez que reqID est valide dans les paramètres de route
  3. Vérifiez les erreurs dans la console

Les données ne persistent pas

  1. Vérifiez que useState fonctionne correctement
  2. Vérifiez que les données ne sont pas réinitialisées ailleurs
  3. Vérifiez la console pour les erreurs

La validation ne fonctionne pas

  1. Vérifiez que les champs requis sont bien remplis
  2. Vérifiez la logique dans isStepCompleted()
  3. Vérifiez les erreurs dans la console

Exemple complet

<template>
  <div class="multi-step-form">
    <StepNavigation />
    
    <div class="form-content">
      <!-- Étape 1 : Localisation -->
      <div v-if="currentStep === 0">
        <AddressForm />
      </div>
      
      <!-- Étape 2 : Équipements -->
      <div v-if="currentStep === 1">
        <EquipmentForm />
      </div>
      
      <!-- Étape 3 : Tarifs -->
      <div v-if="currentStep === 2">
        <PricingForm />
      </div>
      
      <!-- Étape 4 : Contact -->
      <div v-if="currentStep === 3">
        <ContactForm />
      </div>
      
      <!-- Étape 5 : Contrat -->
      <div v-if="currentStep === 4">
        <ContractForm />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
const {
  currentStep,
  formData,
  nextStep,
  prevStep,
  isStepCompleted,
  setValueFromApi,
} = useForm();

const route = useRoute();
const { reqID } = route.params;

// Initialiser depuis l'API si reqID existe
onMounted(async () => {
  if (reqID && reqID !== 'undefined') {
    try {
      const { data } = await $fetch(`/api/requests/${reqID}`);
      setValueFromApi(data);
    } catch (error) {
      console.error('Erreur de chargement:', error);
    }
  }
});
</script>

Résumé

  • Gestion complète d'un formulaire multi-étapes
  • Synchronisation automatique avec les routes
  • Persistance des données dans le state Nuxt
  • Validation des étapes avec critères spécifiques
  • Gestion des erreurs par champ
  • Initialisation depuis l'API avec transformation automatique
  • Navigation intuitive avec fonctions dédiées
  • Support de plusieurs équipements avec ajout/suppression