Thème
Vue 3 Symfony Form
Une librairie Vue 3 robuste permettant de générer automatiquement un formulaire dynamique basé sur le retour JSON du composant createView() de Symfony Form.
Sommaire
- Installation & Configuration
- Initialisation des Données (Important)
- Documentation Détaillée
- Chargement Dynamique des Choix (Choice Providers)
- Configuration Globale : Éditeur Riche (TinyMCE)
- Créer un Custom Widget (Widget Personnalisé)
- Personnalisation du Thème (UI Classes)
- Composants Avancés : Checkboxes et Switches
- Support de l'option
inherited_data - Affichage Conditionnel de Champs (
data-condition) - Gestion des Conflits
- Accessibilité & Traductions
- Tests & Assurance Qualité
Documentation Détaillée
TIP
Des guides exhaustifs sont disponibles pour approfondir les fonctionnalités avancées :
- 📄 Validation Frontend & Contraintes Symfony : moteur de validation réactif, messages d'erreurs, règles de dates,
propertyPathet parité PHP. - 📄 Affichage Conditionnel de Champs : syntaxe
data-condition, collections et tolérance aux erreurs. - 📄 Système de Grille et Colonnes Dynamique : gestion des colonnes via
colset intégration Bootstrap / Tailwind.
Installation & Configuration
- Installez le package (selon votre gestionnaire) :bash
npm install @codifyo/form-vue - Dans votre point d'entrée (
main.tsoumain.js), installez le plugin :typescriptimport { createApp } from 'vue'; import App from './App.vue'; import SymfonyFormPlugin from '@codifyo/form-vue'; const app = createApp(App); app.use(SymfonyFormPlugin); app.mount('#app');
Initialisation des Données (Important)
Les données JSON de configuration renvoyées par Symfony ne doivent pas être passées telles quelles au composant racine <symfony-form>. Elles doivent impérativement être converties en une arborescence d'instances de la classe FormField pour que la validation stricte des props de Vue fonctionne de bout en bout.
Exemple :
typescript
import { FormField } from '@codifyo/form-vue';
// 1. On parse le retour textuel JSON fourni par l'API Symfony
const rawJson = JSON.parse(jsonStringFromSymfony);
// 2. On instancie récursivement tous les objets en classes FormField
const parsedFormJson = FormField.fromJson(rawJson);Chargement Dynamique des Choix (Choice Providers)
Le champ ChoiceType (block_prefixes: ["form", "choice"]) de Symfony permet nativement d'utiliser des choix statiques envoyés dans le JSON (dans la clé choices). Toutefois, pour de longues listes de données ou pour des options dépendantes du contexte, il est souvent préférable de charger ces données asynchrone depuis une API (Lazy Loading).
La librairie permet cela via le mécanisme des Choice Providers.
1. Configurer Symfony (Backend)
Dans le builder Symfony, vous pouvez définir les options choices_provider (le nom identifiant le provider sur le front) et choices_provider_options (les paramètres optionnels envoyés au front). Note: Cette configuration se fait via les attr ou une extension custom du form type Symfony pour injecter ces variables dans la vue vars.
JSON final généré par Symfony :
json
{
"vars": {
"name": "users",
"full_name": "form[users]",
"block_prefixes": ["form", "choice"],
"multiple": true,
"choices_provider": "api_users",
"choices_provider_options": { "role": "admin" }
}
}2. Définir le Provider (Frontend Vue 3)
Un Choice Provider est une simple fonction qui reçoit les options et doit retourner un objet contenant :
choices: Unerefcontenant le tableau des choix.load(optionnel) : Une fonction appelée pour recharger les données (utile pour faire de la recherche asynchrone).
typescript
import { ref } from 'vue';
const myChoiceProviders = {
// Clé correspondant à "choices_provider" du JSON
api_users: (options) => {
const choices = ref([]);
// La méthode load sera passée au composant select final (ex: vue-select)
const load = async (query = '') => {
// Options contient ici { role: "admin" }
const response = await fetch(`/api/users?q=${query}&role=${options.role}`);
choices.value = await response.json();
};
// Chargement initial au montage du composant
load();
return { choices, load };
}
};3. Passer les Providers au Formulaire
Passez l'objet contenant tous vos providers via la prop choiceProviders du composant <symfony-form>. La librairie utilisera le système de provide / inject natif de Vue 3 pour le distribuer automatiquement à tous les sous-composants, sans aucun prop-drilling !
vue
<template>
<symfony-form
:form="parsedFormJson"
v-model="formData"
:choiceProviders="myChoiceProviders"
/>
</template>
<script setup>
import SymfonyForm from '@codifyo/form-vue';
import { ref } from 'vue';
const formData = ref({});
const parsedFormJson = { /* ... */ };
const myChoiceProviders = {
// ... définitions de vos providers ...
};
</script>Bonus : Remplacement du <select> par un composant riche
Par défaut, la librairie utilise la balise native <select>. Celle-ci ne gère pas de barre de recherche. Si vous souhaitez bénéficier pleinement du Lazy Loading dynamique asynchrone (via la méthode load), vous pouvez surcharger le widget standard en enregistrant votre propre composant compatible (comme vue-select).
Votre composant de Select recevra alors automatiquement la prop load, que vous pourrez lier nativement !
Configuration Globale : Editeur Riche (TinyMCE)
La librairie utilise @tinymce/tinymce-vue pour le rendu des champs texte riches (WisiwygWidget). Par défaut, ce composant tente de charger le moteur TinyMCE depuis leur Cloud payant, ce qui affichera un avertissement "A valid API key is required" si vous n'avez pas de clé API valide.
Pour résoudre ce problème et rendre l'éditeur totalement agnostique, vous pouvez (et devez) injecter une configuration globale via le système de provide de Vue.
Dans votre composant racine ou main.ts, fournissez un objet tinymceOptions :
typescript
import { provide } from 'vue';
// Configuration de TinyMCE pour toute l'application
provide('tinymceOptions', {
// L'URL de votre script TinyMCE (auto-hébergé ou via CDN public comme cdnjs)
// Cela empêchera l'appel au Cloud et retirera l'avertissement de clé API !
scriptSrc: 'https://cdnjs.cloudflare.com/ajax/libs/tinymce/7.1.1/tinymce.min.js',
// (Optionnel) Base URL si vous auto-hébergez vos propres thèmes/skins
// base_url: '/mes-assets/tinymce',
// (Optionnel) N'importe quelle option d'initialisation native de TinyMCE
initOptions: {
branding: false,
menubar: false,
// language: 'fr_FR', // Nécessite de configurer 'language_url' si le CDN ne l'inclut pas
},
// (Optionnel) Configuration avancée des plugins (natifs ou personnalisés)
custom_plugins: {
// Exemple 1: Configurer un plugin natif (ex: 'table')
// TinyMCE chargera le plugin, la librairie ajoutera automatiquement les boutons et options.
'table': {
toolbar_append: ' | table tabledelete | tableprops tablerowprops tablecellprops',
init_options: { table_default_attributes: { border: '1' } }
},
// Exemple 2: Créer un plugin totalement sur mesure (Plugin Factory)
// Vous pouvez passer une fonction qui lit la configuration spécifique du champ (ex: `form.vars`).
'dynamic_expression': (form) => ({
// La fonction/classe qui sera passée à tinymce.PluginManager.add()
setup: MyCustomExpressionPluginClass,
toolbar_append: ' | dynamic_expression',
init_options: {
// Lecture dynamique d'une option métier propre à ce champ précis !
expression_config: form.vars.expression_builder_options,
content_style: '.tinymce-expression { background-color: #02faf2; }'
}
})
}
});Note : Pour activer un plugin, il suffit d'envoyer son nom dans la propriété plugins du champ depuis votre backend, ou via le FormBuilder JS (form.add('champ', 'wysiwyg', { plugins: ['table', 'dynamic_expression'] })). La librairie s'occupera d'appeler PluginManager.add, d'ajouter les boutons à la toolbar et de fusionner les options sans la moindre plomberie de votre côté !
Créer un Custom Widget (Widget Personnalisé)
Vous pouvez très facilement remplacer n'importe quel type de champ Symfony par votre propre composant Vue (ou un composant issu d'une librairie tierce comme Vuetify ou ElementPlus).
1. Créer le composant Vue
Votre composant doit accepter la propriété form (définie via FormHelper.getProps()) et utiliser defineModel() pour lier la valeur.
vue
<!-- components/MyCustomDatePicker.vue -->
<script setup lang="ts">
import { defineModel } from 'vue';
import { FormHelper } from '@codifyo/form-vue';
// 1. Déclarer le modèle bidirectionnel (Vue 3.4+)
const model = defineModel();
// 2. Hériter des propriétés standard de la librairie
const props = defineProps(FormHelper.getProps());
// Vous pouvez accéder à la configuration Symfony via props.form.vars
// ex: props.form.vars.id, props.form.vars.required, etc.
</script>
<template>
<div class="my-custom-wrapper">
<!-- Remplacez ceci par votre composant métier (ex: VueDatePicker) -->
<input
type="date"
v-model="model"
class="super-custom-input"
:id="form.vars.id"
:required="form.vars.required"
:disabled="form.vars.disabled"
/>
</div>
</template>2. Enregistrer le Widget
Lors de l'initialisation de votre application, utilisez la fonction registerFormWidget pour associer un block_prefix Symfony à votre composant.
typescript
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import SymfonyFormPlugin, { registerFormWidget } from '@codifyo/form-vue';
import MyCustomDatePicker from './components/MyCustomDatePicker.vue';
const app = createApp(App);
app.use(SymfonyFormPlugin);
// Dès que la librairie rencontrera un champ Symfony avec le prefix "date",
// elle utilisera automatiquement MyCustomDatePicker !
registerFormWidget('date', MyCustomDatePicker);
app.mount('#app');TIP
Le premier argument de registerFormWidget correspond au block_prefix Symfony. Vous pouvez écraser des types natifs (text, number, choice) ou cibler vos propres Custom FormTypes (ex: si vous avez créé un MyAppDateRangeType en PHP, utilisez my_app_date_range).
Personnalisation du Thème (UI Classes)
La librairie génère un balisage agnostique qui s'appuie sur un dictionnaire de classes CSS. Par défaut, ce dictionnaire est configuré pour Bootstrap 4.6.
Vous pouvez surcharger n'importe quelle classe CSS (ou fournir votre propre thème, ex: Tailwind CSS ou Bootstrap 5) lors de l'initialisation du plugin :
typescript
app.use(SymfonyFormPlugin, {
themes: {
default: {
input: 'form-control', // Champ texte classique
inputError: 'is-invalid', // Classe ajoutée en cas d'erreur
label: 'form-label', // Label standard
checkInput: 'form-check-input',// Input pour Checkbox/Radio
checkLabel: 'form-check-label',// Label pour Checkbox/Radio
checkContainer: 'form-check', // Conteneur global Checkbox/Radio
checkContainerSwitch: 'custom-control custom-switch', // Conteneur Switch BS4
buttonDefault: 'btn btn-primary',
// ...
}
}
});Composants Avancés : Checkboxes et Switches
La librairie génère automatiquement la structure HTML complexe requise par les frameworks CSS modernes (l'input avant le label, enveloppés dans un conteneur dédié).
Activer le mode "Switch" (Interrupteur)
Vous pouvez transformer visuellement une simple Checkbox en interrupteur (Switch) très facilement depuis votre backend Symfony.
Option 1 : Via les attributs (attr) Passez simplement switch => true dans le tableau d'attributs de votre champ :
php
$builder->add('active', CheckboxType::class, [
'attr' => ['switch' => true]
]);Option 2 : Via un FormType personnalisé Si vous avez créé un type Symfony dédié (ex: SwitchType), assurez-vous qu'il déclare le block prefix switch :
php
public function getBlockPrefix(): string
{
return 'switch';
}La librairie l'identifiera automatiquement et appliquera la classe checkContainerSwitch ainsi que l'attribut ARIA role="switch" pour l'accessibilité.
Compatibilité Bootstrap 5 (Switches)
Par défaut, les interrupteurs utilisent le balisage natif de Bootstrap 4.6 (custom-control custom-switch). Si vous migrez vers Bootstrap 5, la structure HTML requise pour un switch est différente.
Vous pouvez facilement adapter la librairie pour Bootstrap 5 en surchargeant le thème par défaut :
typescript
app.use(SymfonyFormPlugin, {
themes: {
default: {
checkContainerSwitch: 'form-check form-switch',
checkInputSwitch: 'form-check-input',
checkLabelSwitch: 'form-check-label',
}
}
});Support de l'option inherited_data
La librairie prend en charge nativement l'option inherited_data de Symfony Form. Lorsque vous configurez un sous-formulaire ou un groupe de champs avec 'inherited_data' => true côté Symfony, la librairie :
- Ne crée pas de sous-objet intermédiaire dans votre modèle réactif (
v-model). - Map directement les champs enfants sur l'objet modèle du formulaire parent.
- Gère de manière intelligente le nettoyage (
removeExtra), en évitant de supprimer les propriétés héritées ou partagées entre le formulaire parent et ses enfants.
Exemple Symfony :
php
$builder->add('personnal_info', PersonnalInfoType::class, [
'inherited_data' => true,
]);Les champs définis dans PersonnalInfoType (ex: first_name, last_name) modifieront directement et réactivement les propriétés à la racine de votre modèle Vue 3.
Affichage Conditionnel de Champs (data-condition)
Vous pouvez afficher ou masquer dynamiquement n'importe quel champ en fonction de l'état réactif d'autres champs (v-model) en renseignant l'attribut HTML data-condition côté Symfony :
php
$builder->add('deliveryAddress', TextareaType::class, [
'label' => 'Adresse de livraison',
'attr' => [
'data-condition' => 'model.hasDifferentAddress === true',
],
]);L'expression JavaScript passée à data-condition reçoit l'objet réactif model représentant l'ensemble des données du formulaire.
TIP
Consultez la documentation détaillée sur l'Affichage Conditionnel de Champs pour des exemples avancés (conditions composées, optional chaining, et gestion dynamique des collections avec __name__).
Gestion des Conflits
La librairie intègre nativement un système de détection de conflits, particulièrement utile pour afficher à l'utilisateur les modifications par rapport à une valeur de base (par exemple, afficher la valeur d'origine d'un champ avant de la sauvegarder).
Lorsqu'une différence est détectée, une alerte s'affiche sous le champ concerné indiquant la valeur de référence avec un bouton permettant de la restaurer.
Activer la gestion des conflits
Utilisez les props referenceData et showConflicts sur le composant racine <symfony-form> :
vue
<symfony-form
:form="parsedForm"
v-model="formData"
:referenceData="originalData"
:showConflicts="true"
/>L'objet referenceData doit avoir la même structure que l'objet v-model. Le système gère intelligemment les champs complexes (tableaux, choix imbriqués, etc.) pour afficher le libellé texte correspondant à la valeur.
Traduction et personnalisation
Vous pouvez personnaliser les libellés de l'alerte de conflit via la prop optionnelle conflictTranslations :
vue
<symfony-form
...
:conflictTranslations="{
referenceValue: 'Ancienne valeur :',
empty: 'Non renseigné',
restore: 'Annuler la modif'
}"
/>Accessibilité & Traductions
Les composants visent la conformité WCAG 2.2 AA : champs reliés à leur label, aide, erreurs et avertissements via aria-describedby, aria-required / aria-invalid, régions live pour les erreurs et l'affichage des champs conditionnels.
Focus à la soumission
Si le formulaire est invalide à la soumission, le focus est placé sur le premier champ en erreur. Désactivable avec :focusInvalid="false".
Traduire les libellés
Tous les textes affichés ou annoncés par la librairie sont personnalisables via la prop translations (voir l'interface exportée FormTranslations et defaultTranslations). Les paramètres {name}, {count}… sont remplacés à l'affichage.
vue
<symfony-form
...
:translations="{
fileDropPrompt: 'Drop a file here or click to browse',
fileRemove: 'Remove {name}',
collectionAdd: 'Add an item',
fieldShown: 'Field “{label}” shown'
}"
/>Les options Symfony add_label, delete_label, empty_label, sort_up_label et sort_down_label d'une collection restent prioritaires.
Options utiles
- Préfixe / suffixe en icône : ajoutez un
labelpour qu'il soit lu par les lecteurs d'écran, ex.'prepend_input' => ['icon' => 'fas fa-euro-sign', 'label' => 'euros']. - Bloc repliable :
heading_level(1 à 6, défaut 2) fixe le niveau du titre. - Thème : la classe
visuallyHidden(défautsr-only, Bootstrap 4) masque visuellement les labels destinés aux seuls lecteurs d'écran. Pour Bootstrap 5, définissezvisuallyHidden: 'visually-hidden'.
Tests & Assurance Qualité
La librairie est couverte par une suite complète de 199 tests unitaires, de composants et fonctionnels sous Vitest et @vue/test-utils avec des seuils de couverture stricts :
- Lignes : > 84%
- Instructions (Statements) : > 81%
- Branches : > 73%
- Fonctions : > 77%
Commandes disponibles
bash
# Exécution de l'ensemble de la suite de tests
npm test -- --run
# Rapport détaillé de couverture de code (V8)
npx vitest run --coverage
# Vérification du typage TypeScript strict et build de distribution
npm run build