Skip to content

FormField

Composant conteneur pour les champs de formulaire avec gestion du label, des messages d'aide, des états de validation et de l'accessibilité selon les normes W3C.

Description

SuFormField est un composant wrapper qui fournit une structure cohérente pour tous les champs de formulaire. Il gère le label, les messages d'aide, les états de validation (erreur, succès, avertissement) et assure l'accessibilité en associant correctement les labels et les messages aux champs via des props exposées dans le slot par défaut.

Exemples d'utilisation

Usage basique

Structure simple

vue
<template>
  <!-- Sans slot props explicites -->
  <SuFormField 
    label="Label de l'élément" 
    message="Texte d'aide pour guider l'utilisateur"
  >
    Default slot here
  </SuFormField>

  <!-- Avec récupération des slot props -->
  <SuFormField label="Nom d'utilisateur">
    <template #default="slotProps">
      <FormControlComponent v-bind="slotProps" />
    </template>
  </SuFormField>

  <!-- Avec destructuration des slot props -->
  <SuFormField 
    label="Email" 
    message="Utilisé pour la connexion"
  >
    <template #default="slotProps">
      <input 
        :id="slotProps?.fieldId"
        :aria-describedby="slotProps?.messageId"
        :aria-invalid="slotProps?.state === 'error'"
        :disabled="slotProps?.disabled"
        type="email"
      />
    </template>
  </SuFormField>
</template>

Props exposées dans le slot par défaut

Le slot par défaut expose automatiquement les props suivantes pour faciliter l'accessibilité :

vue
<template>
  <SuFormField label="Champ exemple" message="Message d'aide">
    <template #default="slotProps">
      <!-- fieldId : ID unique du champ (ex: "field-123") -->
      <!-- messageId : ID du message d'aide (ex: "message-123") -->
      <!-- state : État actuel ('default' | 'error' | 'success' | 'warning') -->
      <!-- size : Taille actuelle ('sm' | 'md' | 'lg') -->
      <!-- disabled : Booléen indiquant si le champ est désactivé -->
      
      <YourComponent 
        :id="slotProps?.fieldId"
        :aria-describedby="slotProps?.messageId"
        :state="slotProps?.state"
        :disabled="slotProps?.disabled"
      />
    </template>
  </SuFormField>
</template>

États de validation

États de validation




vue
<template>
  <SuFormField 
    label="État par défaut" 
    message="Texte d'aide pour guider l'utilisateur"
  >
    <template #default="{ fieldId, messageId }">
      <YourFormControlComponent 
        :id="fieldId"
        :aria-describedby="messageId" 
      />
    </template>
  </SuFormField>
  
  <SuFormField 
    state="error"
    label="État d'erreur" 
    message="Ce champ contient une erreur"
  >
    <template #default="{ fieldId, messageId, state }">
      <YourFormControlComponent 
        :id="fieldId"
        :aria-describedby="messageId"
        :aria-invalid="state === 'error'" 
      />
    </template>
  </SuFormField>
  
  <SuFormField 
    state="success"
    label="État de succès" 
    message="Valeur valide !"
  >
    <template #default="slotProps">
      <YourFormControlComponent 
        v-bind="slotProps"
      />
    </template>
  </SuFormField>
  
  <SuFormField 
    state="warning"
    label="État d'avertissement" 
    message="Attention à cette valeur"
  >
    <YourFormControlComponent 
      state="warning"
    />
  </SuFormField>
</template>

Champ requis

Champ obligatoire

vue
<template>
  <SuFormField 
    label="Champ requis"
    :required="true"
    message="Ce champ est obligatoire"
  >
    <template #default="{ fieldId, messageId }">
      <SuInput 
        :id="fieldId"
        :aria-describedby="messageId"
        :aria-required="true"
        type="text"
        placeholder="Entrez une valeur"
        required
      />
    </template>
  </SuFormField>
</template>

Tailles

Tailles disponibles

vue
<template>
  <SuFormField size="sm" label="Small">
    <input type="text" placeholder="Petit champ" />
  </SuFormField>
  
  <SuFormField size="md" label="Medium">
    <input type="text" placeholder="Champ moyen" />
  </SuFormField>
  
  <SuFormField size="lg" label="Large">
    <input type="text" placeholder="Grand champ" />
  </SuFormField>
</template>

Personnalisation avec slots nommés

Personnalisation du label et du message

vue
<script setup>
import { UserIcon, InformationCircleIcon } from '@heroicons/vue/24/outline'
</script>

<template>
  <!-- Label personnalisé avec icône -->
  <SuFormField 
    label="Nom d'utilisateur" 
    :required="true"
    message="Entrez votre nom d'utilisateur"
  >
    <template #label="{ label, required, fieldId }">
      <label 
        :for="fieldId" 
        class="flex items-center gap-2 font-bold text-gray-900"
      >
        <UserIcon class="w-4 h-4" /> 
        {{ label }} 
        <span v-if="required" class="text-red-500">*</span>
      </label>
    </template>
    
    <template #message>
      <div class="flex items-center gap-2 text-sm text-gray-600">
        <InformationCircleIcon class="w-4 h-4" />
        <span>Entrez votre nom d'utilisateur <em>(minimum 4 caractères)</em></span>
      </div>
    </template>
    
    <SuInput id="fieldId" type="text" placeholder="Entrez votre nom d'utilisateur" />
  </SuFormField>

  <!-- Message personnalisé avec état conditionnel -->
  <SuFormField 
    label="Email" 
    :state="emailState"
    message="Message par défaut"
  >
    <template #message="{ state }">
      <div :class="['text-sm', getMessageClass(state)]">
        <span v-if="state === 'error'">⚠️ Format d'email invalide</span>
        <span v-else-if="state === 'success'">✓ Email valide</span>
        <span v-else>Entrez votre adresse email</span>
      </div>
    </template>
    
    <input type="email" placeholder="nom@exemple.com" />
  </SuFormField>
</template>

Champ désactivé

Champ désactivé

vue
<template>
  <SuFormField 
    label="Champ désactivé"
    :disabled="true"
    message="Ce champ est désactivé"
  >
    <template #default="{ fieldId, messageId, disabled }">
      <SuInput 
        :id="fieldId"
        :aria-describedby="messageId"
        :disabled="disabled"
        type="text"
        value="Valeur désactivée"
      />
    </template>
  </SuFormField>
</template>

Support RTL (droite à gauche)

Support des langues RTL

vue
<template>
  <div dir="rtl">
    <SuFormField 
      label="حقل النص"
      message="رسالة المساعدة"
    >
      <SuInput type="text" placeholder="أدخل النص هنا" />
    </SuFormField>
  </div>
</template>

API

Props

PropTypeDefaultDescription
labelstringundefinedLabel du champ de formulaire
messagestringundefinedMessage d'aide ou de validation
state'default' | 'error' | 'success' | 'warning''default'État visuel du champ
size'sm' | 'md' | 'lg''md'Taille du champ
requiredbooleanfalseIndique si le champ est obligatoire
disabledbooleanfalseDésactive le champ

Slots

Slot par défaut

Le slot par défaut reçoit automatiquement les props suivantes :

PropTypeDescription
fieldIdstringID unique généré pour le champ
messageIdstringID unique généré pour le message
state'default' | 'error' | 'success' | 'warning'État actuel du champ
size'sm' | 'md' | 'lg'Taille du champ
disabledbooleanIndique si le champ est désactivé
vue
<SuFormField label="Exemple">
  <template #default="{ fieldId, messageId, state, disabled }">
    <!-- Votre contrôle de formulaire ici -->
  </template>
</SuFormField>

Slot label

Permet de personnaliser complètement le rendu du label.

PropTypeDescription
labelstringTexte du label
requiredbooleanIndique si le champ est requis
fieldIdstringID du champ associé (même valeur que fieldId)
size'sm' | 'md' | 'lg'Taille du champ
vue
<SuFormField label="Mon label" :required="true">
  <template #label="{ label, required, fieldId }">
    <label :for="fieldId">
      {{ label }} <span v-if="required">*</span>
    </label>
  </template>
</SuFormField>

Slot message

Permet de personnaliser complètement le rendu du message.

PropTypeDescription
messagestringTexte du message
state'default' | 'error' | 'success' | 'warning'État actuel du champ
size'sm' | 'md' | 'lg'Taille du champ
messageIdstringID du message (pour aria-describedby)
vue
<SuFormField message="Mon message" state="error">
  <template #message="{ message, state, messageId }">
    <div :id="messageId" :class="getMessageClass(state)">
      {{ message }}
    </div>
  </template>
</SuFormField>

Classes CSS générées

Le composant génère automatiquement des IDs et des classes CSS pour faciliter le styling et l'accessibilité :

  • su-form-field : Classe racine du composant
  • su-form-field--{size} : Modificateur de taille (sm, md, lg)
  • su-form-field--{state} : Modificateur d'état (default, error, success, warning)
  • su-form-field--required : Appliqué quand required est true
  • su-form-field--disabled : Appliqué quand disabled est true
  • su-form-field__label : Classe du label
  • su-form-field__message : Classe du message
  • su-form-field__content : Classe du conteneur du contenu

Accessibilité

Le composant SuFormField respecte les normes WCAG 2.1 AA et les bonnes pratiques W3C :

✅ Fonctionnalités d'accessibilité

  • Association label/champ : Le label est automatiquement associé au champ via for/id (fourni via fieldId)
  • Messages descriptifs : Les messages sont liés au champ via aria-describedby (fourni via messageId)
  • Indicateur de champ requis : Affichage visuel et sémantique avec aria-required
  • États de validation : Communication claire des états via couleurs, icônes et ARIA
  • Support RTL : Gestion complète des langues de droite à gauche
  • Contraste des couleurs : Ratios conformes WCAG AA (4.5:1 minimum)
  • Messages live : Annonce des changements d'état avec aria-live="polite"
  • Focus visible : Indicateur de focus clair sur le label et le champ

🎯 Bonnes pratiques d'utilisation

vue
<!-- ✅ BON : Utilisation complète des props d'accessibilité -->
<SuFormField 
  label="Adresse email"
  :required="true"
  state="error"
  message="L'email est invalide"
>
  <template #default="{ fieldId, messageId, state }">
    <input 
      :id="fieldId"
      :aria-describedby="messageId"
      :aria-invalid="state === 'error'"
      :aria-required="true"
      type="email"
      autocomplete="email"
    />
  </template>
</SuFormField>

<!-- ❌ MAUVAIS : Ne pas utiliser les props d'accessibilité -->
<SuFormField label="Email" state="error" message="Invalide">
  <input type="email" />
  <!-- Manque: id, aria-describedby, aria-invalid -->
</SuFormField>

<!-- ✅ BON : Label personnalisé avec for correct -->
<SuFormField label="Mot de passe" :required="true">
  <template #label="{ label, required, fieldId }">
    <label :for="fieldId" class="custom-label">
      {{ label }} <span v-if="required">*</span>
    </label>
  </template>
  <template #default="{ fieldId }">
    <input :id="fieldId" type="password" />
  </template>
</SuFormField>

Exemples d'usage avancés

Validation de formulaire avec états dynamiques

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

const username = ref('')
const usernameError = ref('')

const validateUsername = () => {
  if (!username.value) {
    usernameError.value = 'Le nom d\'utilisateur est requis'
  } else if (username.value.length < 4) {
    usernameError.value = 'Minimum 4 caractères'
  } else {
    usernameError.value = ''
  }
}

const usernameState = computed(() => {
  if (!username.value) return 'default'
  return usernameError.value ? 'error' : 'success'
})
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <SuFormField 
      label="Nom d'utilisateur"
      :required="true"
      :state="usernameState"
      :message="usernameError || 'Minimum 4 caractères'"
    >
      <template #default="{ fieldId, messageId, state }">
        <input 
          :id="fieldId"
          :aria-describedby="messageId"
          :aria-invalid="state === 'error'"
          type="text"
          v-model="username"
          @blur="validateUsername"
        />
      </template>
    </SuFormField>
  </form>
</template>

Formulaire complexe avec plusieurs champs

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

const formData = ref({
  email: '',
  password: '',
  confirmPassword: ''
})

const errors = ref({})

const validateForm = () => {
  errors.value = {}
  
  if (!formData.value.email) {
    errors.value.email = 'Email requis'
  } else if (!/\S+@\S+\.\S+/.test(formData.value.email)) {
    errors.value.email = 'Email invalide'
  }
  
  if (!formData.value.password) {
    errors.value.password = 'Mot de passe requis'
  } else if (formData.value.password.length < 8) {
    errors.value.password = 'Minimum 8 caractères'
  }
  
  if (formData.value.password !== formData.value.confirmPassword) {
    errors.value.confirmPassword = 'Les mots de passe ne correspondent pas'
  }
  
  return Object.keys(errors.value).length === 0
}
</script>

<template>
  <form @submit.prevent="validateForm">
    <SuFormField 
      label="Email"
      :required="true"
      :state="errors.email ? 'error' : 'default'"
      :message="errors.email || 'Votre adresse email'"
    >
      <template #default="slotProps">
        <input 
          type="email"
          v-model="formData.email"
          v-bind="slotProps"
        />
      </template>
    </SuFormField>
    
    <SuFormField 
      label="Mot de passe"
      :required="true"
      :state="errors.password ? 'error' : 'default'"
      :message="errors.password || 'Minimum 8 caractères'"
    >
      <template #default="slotProps">
        <input 
          type="password"
          v-model="formData.password"
          v-bind="slotProps"
        />
      </template>
    </SuFormField>
    
    <SuFormField 
      label="Confirmation"
      :required="true"
      :state="errors.confirmPassword ? 'error' : 'default'"
      :message="errors.confirmPassword || 'Confirmez votre mot de passe'"
    >
      <template #default="slotProps">
        <input 
          type="password"
          v-model="formData.confirmPassword"
          v-bind="slotProps"
        />
      </template>
    </SuFormField>
  </form>
</template>

Intégration avec différents types de contrôles

vue
<template>
  <!-- Input standard -->
  <SuFormField label="Nom">
    <template #default="slotProps">
      <input type="text" v-bind="slotProps" />
    </template>
  </SuFormField>
  
  <!-- Select -->
  <SuFormField label="Pays">
    <template #default="{ fieldId, messageId }">
      <select :id="fieldId" :aria-describedby="messageId">
        <option value="">Sélectionner...</option>
        <option value="fr">France</option>
        <option value="be">Belgique</option>
      </select>
    </template>
  </SuFormField>
  
  <!-- Textarea -->
  <SuFormField label="Description">
    <template #default="{ fieldId, messageId }">
      <textarea 
        :id="fieldId" 
        :aria-describedby="messageId"
        rows="4"
      ></textarea>
    </template>
  </SuFormField>
  
  <!-- Checkbox -->
  <SuFormField label="J'accepte les conditions">
    <template #default="{ fieldId }">
      <input :id="fieldId" type="checkbox" />
    </template>
  </SuFormField>
  
  <!-- Radio group -->
  <SuFormField label="Genre">
    <template #default="{ messageId }">
      <div :aria-describedby="messageId">
        <label><input type="radio" name="gender" value="m" /> Homme</label>
        <label><input type="radio" name="gender" value="f" /> Femme</label>
        <label><input type="radio" name="gender" value="o" /> Autre</label>
      </div>
    </template>
  </SuFormField>
</template>

Intégration avec le design system

Le composant SuFormField est conçu pour être utilisé avec tous les contrôles de formulaire de votre design system :

vue
<template>
  <!-- Avec SuInput -->
  <SuFormField label="Email">
    <template #default="slotProps">
      <SuInput type="email" v-bind="slotProps" />
    </template>
  </SuFormField>
  
  <!-- Avec SuSelect (composant futur) -->
  <SuFormField label="Pays">
    <template #default="slotProps">
      <SuSelect :options="countries" v-bind="slotProps" />
    </template>
  </SuFormField>
  
  <!-- Avec SuTextarea (composant futur) -->
  <SuFormField label="Message">
    <template #default="slotProps">
      <SuTextarea v-bind="slotProps" />
    </template>
  </SuFormField>
</template>

Publié sous licence MIT.