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.
Le composable useForm gère un formulaire complexe en plusieurs étapes :
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]
interface CompleteForm {
address: AddressForm;
equipments: EquipmentForm[];
contact: ContactForm;
offer: OfferForm;
}
interface AddressForm {
street: string;
postalCode: string;
city: string;
lessThanTwoYears: boolean;
}
interface EquipmentForm {
equipmentType: string;
brand: string;
installedAt: number | null;
uiDuctable: number;
uiNotDuctable: number;
isMultiSplit: boolean;
internUnits: Array<{ value: string; label: string }>;
}
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;
}
interface OfferForm {
maintenanceOffer: string;
price: number;
showPunctual: boolean;
paymentType: "subscription" | "one_shot";
}
currentStep: Ref<number>Étape courante du formulaire (0-indexé).
const { currentStep } = useForm();
console.log(currentStep.value); // 0, 1, 2, ...
totalSteps: numberNombre 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(): voidPasse à l'étape suivante. Navigation automatique vers la route correspondante.
const { nextStep } = useForm();
nextStep(); // Étape 0 → 1
prevStep(): voidRevient à l'étape précédente. Émet un événement Matomo de tracking.
const { prevStep } = useForm();
prevStep(); // Étape 1 → 0
goToStep(step: number): voidVa directement à une étape spécifique.
const { goToStep } = useForm();
goToStep(3); // Aller à l'étape 3
Paramètres :
step : Numéro de l'étape (0 à totalSteps)updatePostalCode(data: Partial<AddressForm>): voidMet à 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[]): voidMet à 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>): voidMet à jour un équipement spécifique.
const { updateEquipmentItem } = useForm();
updateEquipmentItem(0, {
brand: '/api/brands/2',
});
addEquipment(): voidAjoute un nouvel équipement vide à la liste.
const { addEquipment } = useForm();
addEquipment();
removeEquipment(index: number): voidSupprime un équipement de la liste (minimum 1 équipement requis).
const { removeEquipment } = useForm();
removeEquipment(0);
updateContact(data: Partial<ContactForm>): voidMet à jour les données de contact.
const { updateContact } = useForm();
updateContact({
name: 'John',
lastname: 'Doe',
email: 'john@example.com',
});
updateOffer(data: Partial<OfferForm>): voidMet à jour l'offre sélectionnée.
const { updateOffer } = useForm();
updateOffer({
maintenanceOffer: '/api/offers/1',
paymentType: 'subscription',
});
isStepCompleted(step: number): booleanVé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 :
postalCode et city requisbrand, equipmentType et installedAtmaintenanceOffer requisname, lastname, email, phone requistruegetStepProgress(): numberCalcule le pourcentage de progression (0-100).
const { getStepProgress } = useForm();
const progress = getStepProgress(); // 0-100
setError(field: string, message: string): voidDéfinit un message d'erreur pour un champ.
const { setError } = useForm();
setError('address.postalCode', 'Code postal invalide');
clearError(field: string): voidSupprime l'erreur d'un champ.
const { clearError } = useForm();
clearError('address.postalCode');
clearAllErrors(): voidSupprime toutes les erreurs.
const { clearAllErrors } = useForm();
clearAllErrors();
resetForm(): voidRéinitialise le formulaire à l'état initial.
const { resetForm } = useForm();
resetForm();
setValueFromApi(apiRequest: ApiRequest): voidInitialise 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 :
@id) en stringsuiDuctable et uiNotDuctableLe 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.
<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>
<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>
<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>
<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>
currentStep directement, utilisez les fonctions de navigationisStepCompleted() pour désactiver le bouton "Suivant"setError() pour afficher les erreurs de validationclearError() lors de la correctionstepRoutesreqID est valide dans les paramètres de routeuseState fonctionne correctementisStepCompleted()<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>