Skip to content

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 ​

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 :

  1. FormRow injecte le modèle réactif racine du formulaire (rootModel).
  2. Une computed property isConditionMet compile et évalue l'expression JavaScript fournie via new Function('model', 'return ' + condition).
  3. Le paramètre disponible dans l'expression est model, qui correspond directement à l'objet de données réactif v-model du formulaire.
  4. Si la condition est évaluée à true (ou truthy), le champ est affiché avec v-if.
  5. 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...catch par FormRow.
  • 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-if gè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 :

  1. 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).

  2. 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 (frontErrors et frontWarnings) sont immédiatement effacées, et les indicateurs visuels d'erreur (is-invalid) sont retirés.

  3. 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.

  4. Accès à la visibilité dans les contraintes Callback : Dans une contrainte Callback, le paramètre context fournit la propriété booléenne context.isVisible, permettant si besoin de vérifier programmatiquement si le champ est actuellement visible :

    javascript
    if (!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>