Retour au cours

frontend / vuejs

Formulaires & v-model avancé

Leçon 101 exercice

Explication

Ce que vous allez apprendre

  • Utiliser v-model sur tous les types de champs de formulaire (texte, select, checkbox, radio)
  • Appliquer les modificateurs .trim, .number et .lazy selon le besoin
  • Lier des checkboxes multiples à un même tableau réactif
  • Construire une validation dérivée avec computed et gérer la soumission de façon robuste
  • Séparer clairement la responsabilité de validation de celle de soumission

Dans quel contexte ?

Un formulaire d'inscription doit valider un email, un mot de passe de longueur minimale et une confirmation identique, afficher les erreurs au fur et à mesure, désactiver le bouton de soumission tant que le formulaire n'est pas valide, et gérer un état de chargement pendant l'appel réseau. C'est un cas extrêmement courant, et Vue fournit tous les outils nécessaires sans bibliothèque externe pour un formulaire de cette complexité.

D'abord, une liaison bidirectionnelle qui s'adapte au type de champ

v-model fonctionne différemment selon le type d'élément HTML : sur un <input type="text">, il lie la valeur textuelle ; sur une checkbox, il lie un booléen (ou un tableau pour des checkboxes multiples partageant le même v-model) ; sur des radios, il lie la valeur de l'option sélectionnée. Cette adaptation automatique évite d'écrire soi-même la logique de synchronisation pour chaque type de champ.

Prérequis

Il faut avoir compris le v-model personnalisé sur un composant (leçon 5) : cette leçon l'applique concrètement aux champs de formulaire natifs.

Ensuite, trois modificateurs qui évitent du code répétitif

.trim retire automatiquement les espaces en début/fin de saisie, .number convertit la valeur en nombre (rappel du piège de typage vu implicitement dès qu'on manipule des formulaires), et .lazy synchronise la valeur seulement à l'événement change plutôt qu'à chaque frappe — utile pour éviter une validation trop agressive pendant la saisie.

ModificateurEffet
.trimRetire les espaces en début/fin
.numberConvertit la saisie en nombre
.lazySynchronise au change, pas à chaque frappe

Il reste un comportement à bien comprendre sur les checkboxes multiples

Plusieurs checkboxes partageant le même v-model="langues" (un tableau réactif) s'ajoutent ou se retirent automatiquement de ce tableau selon leur état coché ou non — sans qu'il faille écrire manuellement la logique de push/splice. C'est un raccourci puissant, mais qui suppose de bien initialiser langues comme un tableau (ref([])) dès le départ.

Piège fréquent

Initialiser une checkbox multiple avec un ref(null) au lieu d'un tableau vide provoque une erreur silencieuse ou un comportement inattendu, Vue s'attendant à pouvoir manipuler un tableau pour cocher/décocher plusieurs valeurs.

Maintenant, construire une validation qui reste synchronisée avec la saisie

Une computed qui recalcule un objet d'erreurs à chaque changement des champs du formulaire reste toujours à jour automatiquement, sans qu'il faille déclencher manuellement une revalidation à chaque frappe. estValide dérive ensuite simplement de l'absence d'erreurs dans cet objet.

Astuce

Sépare toujours la logique de VALIDATION (une computed pure, sans effet de bord) de la logique de SOUMISSION (une fonction async avec gestion d'erreur réseau et état de chargement). Ce découplage rend chaque partie testable indépendamment et évite qu'un bug de validation n'affecte la gestion réseau, ou inversement.

Pour des formulaires vraiment complexes en production, des bibliothèques comme VeeValidate évitent de réinventer la gestion d'erreurs par champ, le focus automatique sur le premier champ invalide, et l'intégration avec des schémas de validation typés. Maintenant que les formulaires n'ont plus de secret, la prochaine leçon explore comment étendre le comportement du DOM lui-même avec des directives personnalisées.

Commandes & code

Formulaires & v-model avancé

v-model unifie la liaison bidirectionnelle sur tous les types de champs, avec des modificateurs utiles.

vue
<script setup>
import { ref, reactive } from 'vue'

const texte = ref('')
const description = ref('')
const paysChoisi = ref('fr')
const accepteConditions = ref(false)
const langues = ref([])          // checkboxes multiples -> tableau
const genre = ref('')            // radio -> valeur unique

const formulaire = reactive({
  email: '',
  age: null,
})
</script>

<template>
  <!-- Modificateurs de v-model -->
  <input v-model.trim="texte" placeholder="espaces automatiquement retirés" />
  <input v-model.number="formulaire.age" type="number" placeholder="converti en Number" />
  <input v-model.lazy="description" placeholder="sync au 'change', pas à chaque frappe" />

  <select v-model="paysChoisi">
    <option value="fr">France</option>
    <option value="be">Belgique</option>
    <option value="ch">Suisse</option>
  </select>

  <label><input type="checkbox" v-model="accepteConditions" /> J'accepte les conditions</label>

  <!-- checkboxes multiples sur le même tableau réactif -->
  <label><input type="checkbox" value="fr" v-model="langues" /> Français</label>
  <label><input type="checkbox" value="en" v-model="langues" /> Anglais</label>
  <label><input type="checkbox" value="es" v-model="langues" /> Espagnol</label>

  <!-- radios -->
  <label><input type="radio" value="h" v-model="genre" /> Homme</label>
  <label><input type="radio" value="f" v-model="genre" /> Femme</label>

  <textarea v-model="formulaire.email" />
</template>
vue
<!-- Formulaire complet avec validation manuelle et gestion de soumission -->
<script setup>
import { reactive, computed } from 'vue'

const form = reactive({
  email: '',
  motDePasse: '',
  confirmation: '',
})

const erreurs = computed(() => {
  const e = {}
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
    e.email = 'Adresse email invalide'
  }
  if (form.motDePasse.length < 8) {
    e.motDePasse = 'Minimum 8 caractères'
  }
  if (form.confirmation !== form.motDePasse) {
    e.confirmation = 'Les mots de passe ne correspondent pas'
  }
  return e
})

const estValide = computed(() => Object.keys(erreurs.value).length === 0)
const enEnvoi = ref(false)

async function soumettre() {
  if (!estValide.value) return
  enEnvoi.value = true
  try {
    const reponse = await fetch('/api/inscription', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: form.email, motDePasse: form.motDePasse }),
    })
    if (!reponse.ok) throw new Error(`Erreur serveur: ${reponse.status}`)
    console.log('inscription réussie')
  } catch (erreur) {
    console.error(erreur)
  } finally {
    enEnvoi.value = false
  }
}
</script>

<template>
  <form @submit.prevent="soumettre" novalidate>
    <div>
      <input v-model.trim="form.email" type="email" :aria-invalid="!!erreurs.email" />
      <p v-if="erreurs.email" role="alert">{{ erreurs.email }}</p>
    </div>
    <div>
      <input v-model="form.motDePasse" type="password" />
      <p v-if="erreurs.motDePasse" role="alert">{{ erreurs.motDePasse }}</p>
    </div>
    <div>
      <input v-model="form.confirmation" type="password" />
      <p v-if="erreurs.confirmation" role="alert">{{ erreurs.confirmation }}</p>
    </div>
    <button type="submit" :disabled="!estValide || enEnvoi">
      {{ enEnvoi ? 'Envoi...' : "S'inscrire" }}
    </button>
  </form>
</template>

Résumé

  • .trim, .number, .lazy sont les trois modificateurs natifs de v-model sur un input.
  • Sur des checkboxes multiples, v-model lie automatiquement à un tableau des valeurs cochées.
  • Séparer la validation (computed dérivé) de la soumission (fonction async avec gestion d'erreur/loading).
  • Pour des formulaires complexes en production, des libs comme VeeValidate ou Zod+VeeValidate évitent de réinventer la validation.

Exercices pratiques

1 disponible
1

Mission : un bouton d'inscription qui reste actif à tort

Objectif : Diagnostiquer une checkbox mal initialisée et une validation défaillante, puis livrer un formulaire d'inscription conforme.

Contexte

Un formulaire d'inscription possède un champ langues initialisé avec const langues = ref(null) relié à trois checkboxes (français, anglais, espagnol) via le même v-model="langues". Par ailleurs, le bouton "S'inscrire" reste cliquable même quand le mot de passe fait moins de 8 caractères, car la validation actuelle ne vérifie que l'email. Les deux bugs doivent être corrigés avant la mise en production.

Résoudre l’exercice →