Thème
Affichage Conditionnel de Champs (data-condition)
La librairie @codifyo/form-vue permet d'afficher ou de masquer dynamiquement des champs de formulaire en fonction de l'état réactif des autres champs (v-model), grâce à l'attribut HTML data-condition.
Sommaire
- 1. Principe de Fonctionnement
- 2. Configuration Backend (Symfony)
- 3. Collections et Prototypes (
CollectionType) - 4. Bonnes Pratiques & Précautions
- 5. Exemple Complet
1. Principe de Fonctionnement
Chaque champ est encapsulé dans un composant FormRow.vue qui surveille l'attribut data-condition transmis par Symfony dans form.vars.attr['data-condition'].
Sous le capot :
FormRowinjecte le modèle réactif racine du formulaire (rootModel).- Une computed property
isConditionMetcompile et évalue l'expression JavaScript fournie vianew Function('model', 'return ' + condition). - Le paramètre disponible dans l'expression est
model, qui correspond directement à l'objet de données réactifv-modeldu formulaire. - Si la condition est évaluée à
true(ou truthy), le champ est affiché avecv-if. - Dès qu'une valeur du formulaire change, Vue réévalue immédiatement la condition et met à jour l'affichage sans rechargement.
2. Configuration Backend (Symfony)
Pour rendre un champ conditionnel, ajoutez simplement l'attribut data-condition dans le tableau d'options 'attr' du champ dans votre FormBuilder Symfony.
a) Dépendance à une Checkbox (Booléen)
Afficher un champ uniquement si une case à cocher est cochée :
php
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
// Checkbox déclencheuse
$builder->add('hasDifferentDeliveryAddress', CheckboxType::class, [
'label' => 'Adresse de livraison différente',
'required' => false,
]);
// Champ conditionnel
$builder->add('deliveryAddress', TextareaType::class, [
'label' => 'Adresse de livraison',
'attr' => [
'data-condition' => 'model.hasDifferentDeliveryAddress === true',
],
]);b) Dépendance à un Choix (Select / Radio)
Afficher des champs spécifiques selon l'option sélectionnée dans un ChoiceType :
php
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
$builder->add('clientType', ChoiceType::class, [
'label' => 'Type de client',
'choices' => [
'Particulier' => 'individual',
'Entreprise' => 'company',
],
]);
// Affiché uniquement pour les entreprises
$builder->add('companyName', TextType::class, [
'label' => 'Raison sociale',
'attr' => [
'data-condition' => "model.clientType === 'company'",
],
]);
$builder->add('siret', TextType::class, [
'label' => 'Numéro SIRET',
'attr' => [
'data-condition' => "model.clientType === 'company'",
],
]);c) Conditions Multiples et Logique Avancée
Vous pouvez utiliser tous les opérateurs logiques JavaScript standards (&&, ||, !, >, <, ternaires, etc.) :
php
// Affiché si client professionnel ET non exonéré de TVA
$builder->add('vatNumber', TextType::class, [
'label' => 'Numéro de TVA intracommunautaire',
'attr' => [
'data-condition' => "model.clientType === 'company' && !model.isVatExempt",
],
]);
// Condition numérique
$builder->add('parentalConsent', CheckboxType::class, [
'label' => 'Autorisation parentale obligatoire',
'attr' => [
'data-condition' => "model.age && model.age < 18",
],
]);3. Collections et Prototypes (CollectionType)
La librairie prend en charge les conditions à l'intérieur des collections Symfony dynamiques (CollectionType avec 'prototype' => true).
Lors de l'instanciation ou de l'ajout d'une ligne, le placeholder __name__ contenu dans l'attribut data-condition est automatiquement remplacé par l'index de la ligne courante grâce à FormHelper.
Exemple dans un sous-formulaire de collection
php
// ContactType.php (FormType enfant d'une Collection)
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
$builder->add('type', ChoiceType::class, [
'choices' => [
'Email' => 'email',
'Téléphone' => 'phone',
],
]);
$builder->add('emailValue', TextType::class, [
'label' => 'Adresse email',
'attr' => [
// __name__ sera remplacé par l'index réel (0, 1, 2...)
'data-condition' => "model.contacts?.[__name__]?.type === 'email'",
],
]);
$builder->add('phoneValue', TextType::class, [
'label' => 'Numéro de téléphone',
'attr' => [
'data-condition' => "model.contacts?.[__name__]?.type === 'phone'",
],
]);TIP
Un champ dans une collection peut également dépendre d'une variable globale du formulaire parent (ex: model.globalSetting === true) tout en utilisant __name__ pour cibler ses propres valeurs.
4. Bonnes Pratiques & Précautions
a) Utiliser l'Optional Chaining (?.)
Au chargement initial, certaines propriétés de votre modèle peuvent être null ou undefined. Pour éviter les erreurs JavaScript lors de l'accès à des sous-propriétés, utilisez systématiquement l'optional chaining :
javascript
// Recommandé
"model.shipping?.address?.country === 'FR'"
// Déconseillé (peut générer une exception si model.shipping est vide)
"model.shipping.address.country === 'FR'"b) Tolérance aux Pannes
Si une expression contient une erreur de syntaxe ou produit une exception à l'exécution :
- L'erreur est capturée dans un
try...catchparFormRow. - Un avertissement est loggué dans la console (
Failed to evaluate form condition: ...). - Le champ est simplement masqué (
false), évitant ainsi de faire planter toute l'application Vue.
c) Valeurs dans le Modèle (v-model)
Lorsqu'un champ est masqué par v-if, son composant est démonté du DOM. Cependant, sa valeur reste intacte dans l'objet réactif model (le v-model).
Pourquoi ce comportement ?
- Expérience utilisateur (UX) : Si l'utilisateur décoche une case par inadvertance et la recoche aussitôt, ses données ne sont pas perdues et il n'a pas à ressaisir son texte.
- Principe Vue.js : La directive
v-ifgère le rendu DOM, elle ne modifie ni ne supprime les propriétés des objets de données sous-jacents.
Comment réinitialiser la valeur si vous ne souhaitez pas la conserver ?
Si vos règles métier exigent que la valeur soit effacée dès que le champ est masqué, vous disposez de deux approches :
Approche Frontend (Vue 3) : Ajoutez un watch sur la variable conditionnelle dans votre composant pour remettre à zéro le champ dépendant lorsqu'elle repasse à false :
typescript
watch(() => formData.value.hasDifferentDeliveryAddress, (isActive) => {
if (!isActive) {
formData.value.deliveryAddress = ''; // ou null
}
});Approche Backend (Symfony - Recommandée) : Pour des raisons de sécurité et d'intégrité métier, il est toujours recommandé de valider et nettoyer les données côté serveur via un écouteur d'événement Symfony Form (PRE_SUBMIT) :
php
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
if (empty($data['hasDifferentDeliveryAddress'])) {
$data['deliveryAddress'] = null;
$event->setData($data);
}
});d) Validation Frontend & Gestion des Erreurs
La librairie synchronise automatiquement le moteur de validation frontend (useValidation) avec la visibilité conditionnelle des champs :
Ignorer les validations des champs masqués : Tant qu'un champ est masqué par sa condition (ou parce qu'un conteneur parent est masqué), aucune contrainte frontend (comme
NotBlank,Regex, etc.) n'est exécutée, et le champ est automatiquement considéré comme valide. Il ne bloquera jamais la soumission du formulaire (isValid: true).Nettoyage automatique des erreurs : Si un champ affiché comportait des erreurs de validation (ex: laissé vide alors qu'il est obligatoire) et que l'utilisateur modifie une valeur qui entraîne le masquage de ce champ, toutes ses erreurs (
frontErrorsetfrontWarnings) sont immédiatement effacées, et les indicateurs visuels d'erreur (is-invalid) sont retirés.Visibilité héritée (Champs imbriqués) : Si un conteneur ou un groupe de champs conditionnel (
fieldset,FormRow) est masqué, tous ses champs enfants héritent automatiquement de cet état masqué et leurs validations sont également ignorées.Accès à la visibilité dans les contraintes
Callback: Dans une contrainteCallback, le paramètrecontextfournit la propriété booléennecontext.isVisible, permettant si besoin de vérifier programmatiquement si le champ est actuellement visible :javascriptif (!context.isVisible) { return true; // Le champ est masqué }
5. Exemple Complet
Backend : OrderType.php
php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class OrderType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('deliveryMethod', ChoiceType::class, [
'label' => 'Mode de livraison',
'choices' => [
'Retrait en magasin' => 'store',
'Livraison à domicile' => 'home',
],
])
->add('deliveryAddress', TextType::class, [
'label' => 'Adresse de livraison',
'attr' => [
'data-condition' => "model.deliveryMethod === 'home'",
],
])
->add('hasElevator', CheckboxType::class, [
'label' => 'Présence d\'un ascenseur ?',
'required' => false,
'attr' => [
'data-condition' => "model.deliveryMethod === 'home'",
],
])
->add('floorNumber', TextType::class, [
'label' => 'Étage',
'attr' => [
'data-condition' => "model.deliveryMethod === 'home' && model.hasElevator === false",
],
]);
}
}Frontend : Utilisation Standard
vue
<template>
<symfony-form :form="orderForm" v-model="formData" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { FormField } from '@codifyo/form-vue';
// Données initiales
const formData = ref({
deliveryMethod: 'store',
deliveryAddress: '',
hasElevator: false,
floorNumber: '',
});
// JSON récupéré depuis l'API Symfony
const orderForm = FormField.fromJson(apiResponseData);
</script>