Composants personnalisés CRUD
QForm Builder peut maintenant gérer un registre local de composants métier sérialisables. Le registre est disponible dans l’onglet Personnalisés du panneau gauche. Il permet de créer, modifier, dupliquer, importer, exporter et supprimer une définition, puis d’ajouter une instance au formulaire.
Une définition contient un identifiant stable, un titre, une icône, une description et un schéma de champ. Le schéma reste du JSON QForm Builder : il ne contient ni fonction ni composant Vue arbitraire.
Exemple titres-restaurant
import type { QFormBuilderCustomComponentRegistry } from '@vevedh/qform-builder-layer/types'
const customComponents: QFormBuilderCustomComponentRegistry = [
{
key: 'dsi-meal-voucher-information',
title: 'Nouvelle offre de titres-restaurant',
description: 'Bloc d’information métier configurable.',
icon: 'restaurant',
schema: {
$el: 'div',
name: 'tickets_restaurant_information',
label: 'Nouvelle offre de titres-restaurant',
qformKind: 'dsi-meal-voucher-information',
dsiComponent: 'dsi-meal-voucher-information',
ignore: true,
informationTitle: 'Évolution de l’offre déjeuner',
informationParagraphs: [
'La valeur faciale du titre-restaurant passe de 100,00 € à 100,25 €.',
'Un titre supplémentaire est attribué dans chaque formule : 21 titres pour la formule 1 et 26 titres pour la formule 2.',
'La répartition reste inchangée : 60 % pris en charge par la CACEM et 40 % à la charge de l’agent.',
'Cette nouvelle offre remplacera définitivement l’offre actuelle.',
],
informationWarning: 'Sans consentement transmis au plus tard le 30 octobre 2026, l’offre déjeuner actuelle sera interrompue et restera suspendue jusqu’à la régularisation de la situation.',
deadline: '2026-10-30T23:59:59-04:00',
faceValue: 100.25,
employerRatePercent: 60,
employeeRatePercent: 40,
formulas: [
{
key: 'formula_1',
label: 'Formule 1',
titleCount: 21,
totalValue: 2105.25,
employerContribution: 1263.15,
employeeContribution: 842.1,
},
{
key: 'formula_2',
label: 'Formule 2',
titleCount: 26,
totalValue: 2606.5,
employerContribution: 1563.9,
employeeContribution: 1042.6,
},
],
attrs: {
class: 'dsi-meal-voucher-information-placeholder q-pa-md rounded-borders',
},
},
},
]<FormBuilder
v-model:schema="schema"
v-model:custom-components="customComponents"
/>Les propriétés métier de premier niveau sont détectées automatiquement :
- chaînes courtes : champ texte ;
- textes longs : zone de texte ;
- nombres : champ numérique ;
- booléens : interrupteur ;
- tableaux de chaînes : éditeur de liste ;
- objets et tableaux structurés : éditeur JSON validé ;
- images Base64 JPEG, PNG ou WebP : miniature avec taille, sans exposition de la chaîne complète.
Une instance ajoutée au formulaire conserve qformCustomComponentKey, qformCustomComponentVersion et qformCustomProperties. Le panneau droit utilise ces métadonnées pour afficher les propriétés modifiables.
Persistance
Par défaut, le registre est sauvegardé dans le stockage local avec une clé dérivée du builderId :
qform-builder:<builderId>:custom-componentsLa persistance peut être désactivée :
<FormBuilder :custom-components-autosave="false" />Une clé explicite peut être fournie :
<FormBuilder custom-components-storage-key="dsi:components:forms" />Pour une persistance serveur, utilisez v-model:custom-components et enregistrez le registre via votre service métier NFZ/Feathers.
API publique
const builder = ref<InstanceType<typeof FormBuilder> | null>(null)
builder.value?.getCustomComponents()
builder.value?.createCustomComponent(definition)
builder.value?.updateCustomComponent('ancienne-cle', definition)
builder.value?.removeCustomComponent('ma-cle')
builder.value?.importCustomComponents(json)
const exported = builder.value?.exportCustomComponents()Événements disponibles :
update:customComponents
custom-component-create
custom-component-update
custom-component-remove
custom-component-import
custom-component-exportSécurité et limites
Le registre réutilise le nettoyeur de schéma public. Les clés de pollution de prototype, les gestionnaires on*, $cmp, les balises HTML dangereuses, les URL actives et les styles CSS actifs sont rejetés. Le registre est limité à 200 définitions, 100 propriétés par définition et 2 Mo pour un import JSON.
La suppression d’une définition retire uniquement l’entrée du catalogue. Les instances déjà placées dans les formulaires restent intactes afin d’éviter une perte de données métier.
Validation et automatisation
Le rendu accepte les codes de locale personnalisés exposés par l’application hôte. Le formateur générique retombe explicitement sur le français, sauf pour les codes commençant par en, qui utilisent le format anglais.
Pour les tests navigateur, les dialogues CRUD exposent des sélecteurs stables : data-qform-custom-component-editor-dialog, data-qform-custom-component-delete-dialog et data-qform-custom-component-transfer-dialog. Les assertions de contenu doivent privilégier les rôles sémantiques exacts, par exemple le titre de formule, afin d’éviter les correspondances partielles dans les paragraphes métier.
Designer graphique par glisser-déposer
Depuis Personnalisés > Créer un composant, l’onglet Designer visuel permet de composer le rendu sans écrire le schéma à la main. La palette propose les blocs suivants : titre, texte stylisé, paragraphe, alerte, carte valeur, grille de formules, section, colonnes, liste, badge, bouton, image embarquée, séparateur et espacement.
Un bloc peut être ajouté par clic ou glisser-déposer, déplacé dans le canvas, dupliqué ou supprimé. Son panneau de propriétés associe le bloc à une propriété métier telle que informationTitle, informationParagraphs.0 ou formulas. Les propriétés absentes sont créées avec une valeur sérialisable adaptée.
Le designer enregistre un objet qformCustomLayout versionné dans la définition et dans chaque instance. L’onglet JSON avancé reste disponible pour les structures métier qui ne sont pas encore couvertes graphiquement.
Icône du composant, QColor et texte stylisé
L’icône du composant se choisit dans un catalogue visuel filtrable fondé sur quasar-ui-qiconpicker. Seuls les identifiants Material composés de lettres, chiffres et underscores sont conservés lors de la normalisation. Le registre reste donc sérialisable et ne peut pas injecter un composant d’icône arbitraire.
Les couleurs d’arrière-plan, de texte et de bordure utilisent désormais un sélecteur QColor. Une valeur peut être choisie dans la palette ou saisie sous forme HEX, RGB(A), jeton Quasar autorisé ou variable var(--q-...). Toute valeur hors contrat est rejetée avant la sauvegarde.
Le bloc Texte stylisé conserve du texte brut, jamais du HTML. Son inspecteur permet de régler la police, la taille de 10 à 96 px, la graisse, la hauteur de ligne, l’espacement des lettres, l’italique, le soulignement, la casse, l’ombre et un dégradé linéaire ou radial. Les familles de police, ombres et transformations proviennent de listes sûres ; les couleurs du dégradé passent par le même normaliseur que les autres styles.
{
id: 'text-hero',
type: 'text',
propertyPath: 'heroText',
text: 'Texte de présentation',
textOptions: {
fontFamily: 'georgia',
fontSize: 34,
fontWeight: 700,
lineHeight: 1.2,
letterSpacing: 0.5,
shadow: 'medium',
gradientType: 'linear',
gradientFrom: '#b45309',
gradientTo: '#7c3aed',
gradientAngle: 90,
},
}Styles Quasar et UnoCSS
Le composant complet et chaque bloc disposent de réglages d’apparence : couleurs, bordure, espacement, arrondi, ombre, alignement et pleine largeur. Les classes avancées sont choisies dans une liste sûre de classes Quasar et UnoCSS ; aucune classe libre n’est exécutée directement. Le sélecteur de classes est recherchable : saisissez par exemple q-pa-lg, bg-primary ou grid-cols-2 pour filtrer la liste avant sélection.
layout: {
version: 1,
rootStyle: {
backgroundColor: 'blue-grey-1',
padding: 'lg',
radius: 'lg',
classes: ['w-full', 'shadow-sm'],
},
blocks: [
{
id: 'heading-1',
type: 'heading',
propertyPath: 'informationTitle',
style: { textColor: 'primary', alignment: 'left' },
},
],
}Les chemins contenant __proto__, prototype ou constructor, les couleurs CSS actives et les classes hors allowlist sont rejetés. Le registre reste portable entre le builder, FormViewer, l’import/export et la persistance serveur.
Canvas WYSIWYG et édition directe
Le canvas du designer est un éditeur direct : le contenu affiché n’est plus une simple prévisualisation. Cliquez dans un titre, un paragraphe, une alerte ou une carte de valeur pour modifier la propriété métier associée sans quitter le canvas. Les nombres conservent leur type numérique et les chemins indexés tels que informationParagraphs.0 sont mis à jour dans une copie sécurisée du schéma.
Les grilles de formules exposent les propriétés primitives de chaque entrée sous forme de champs. Les chaînes et nombres utilisent des champs de saisie ; les booléens utilisent un interrupteur afin de conserver leur type. Une formule peut être ajoutée ou supprimée graphiquement. Les objets plus complexes restent modifiables depuis l’onglet JSON avancé.
Le déplacement d’un bloc se fait depuis la poignée dédiée. La carte du bloc est l’unique source native du glisser-déposer ; la poignée l’arme au pointerdown, tandis qu’un départ depuis un champ ou un bouton est refusé. Cette séparation évite qu’un déplacement démarre pendant l’édition. Les zones de destination avant et après un bloc deviennent visibles pendant le déplacement. Les boutons Monter et Descendre, ainsi que Alt + Flèche haut et Alt + Flèche bas, offrent une alternative clavier déterministe.
Chaque modification directe synchronise immédiatement :
- la valeur métier du schéma ;
- le rendu du canvas ;
- le JSON avancé ;
- la définition enregistrée dans le registre.
Les chemins contenant des segments de pollution de prototype et les index de tableau déraisonnables sont refusés par l’écrivain interne du designer.
Mise en page sur 12 colonnes
Le canvas utilise une grille responsive de 12 colonnes. Chaque bloc possède une largeur comprise entre 1/12 et 12/12. La largeur peut être modifiée directement depuis la carte avec les boutons − et +, en tirant la poignée située dans l’angle inférieur droit, ou depuis l’inspecteur avec le curseur et les préréglages 3/12, 4/12, 6/12, 8/12 et 12/12.
Les blocs adjacents occupent la même ligne lorsque la somme de leurs largeurs ne dépasse pas 12. Sur un écran étroit, ils repassent automatiquement sur une seule colonne afin de préserver la lisibilité.
Le bloc Colonnes fournit sa propre grille interne de 12 colonnes. Chaque carte interne contient un titre, un texte, une icône Quasar et une largeur réglable. Jusqu’à six colonnes peuvent être ajoutées graphiquement. Le bloc Section structure une zone visuelle avec un titre, une description, une icône et les styles habituels.
Les blocs complémentaires servent aux compositions UI courantes :
- Liste : collection de chaînes modifiable ligne par ligne ;
- Badge : statut ou valeur courte fortement mise en évidence ;
- Bouton : libellé, icône et URL sûre (
https,mailto,tel, chemin relatif ou ancre) ; - Section : séparation sémantique et visuelle d’un groupe d’informations ;
- Colonnes : cartes éditables alignées sur une grille interne.
La largeur est sérialisée dans qformCustomLayout.blocks[].span. Les anciennes définitions qui ne contiennent pas span restent compatibles et utilisent 12/12 par défaut.
Image embarquée Base64
Le bloc Image embarquée accepte uniquement les formats raster JPEG, PNG et WebP. Le fichier source peut atteindre 10 Mo, mais il est redimensionné et réencodé dans le navigateur avant d’être stocké dans la propriété métier du bloc sous forme de data:image/...;base64,.... Les formats SVG et GIF ne sont pas acceptés afin d’éviter les contenus actifs et les animations non maîtrisées.
L’inspecteur permet de régler :
- la taille cible entre 16 et 128 Ko ;
- le format de sortie WebP, JPEG ou PNG ;
- la qualité maximale de l’encodeur ;
- la dimension maximale de la source ;
- le texte alternatif, la légende et le mode d’ajustement
contain,cover,fillouscale-down; - la largeur interne de l’image et sa hauteur en pixels.
Après chaque encodage, le designer affiche la taille source et la taille optimisée afin de rendre le gain mesurable. L’optimiseur recherche d’abord la meilleure qualité JPEG ou WebP compatible avec la taille cible. Si cela ne suffit pas, il réduit progressivement les dimensions jusqu’à atteindre la cible ou le meilleur résultat sûr. La sortie embarquée reste plafonnée à 128 Ko afin de rester compatible avec les limites des documents versionnés et des imports JSON.
L’image se redimensionne directement dans le canvas avec la poignée placée dans son angle inférieur droit. Les flèches gauche et droite modifient sa largeur par pas de 5 %, tandis que les flèches haut et bas modifient sa hauteur. Cette taille interne est indépendante de la largeur du bloc dans la grille principale : le bloc conserve son propre span de 1 à 12 colonnes.
Le runtime vérifie à nouveau le type MIME, l’encodage Base64 et la taille avant de produire la balise <img>. Le rendu utilise loading="lazy" et decoding="async". Dans le panneau de propriétés d’une instance, la donnée Base64 est présentée comme une miniature avec sa taille, et non comme une longue zone de texte.
{
id: 'image-hero',
type: 'image',
propertyPath: 'embeddedImage',
span: 12,
text: 'Présentation du service',
image: {
alt: 'Équipe réunie dans la salle de projet',
widthPercent: 85,
height: 320,
fit: 'cover',
targetKb: 96,
quality: 0.82,
format: 'webp',
maxDimension: 1600,
},
}