@msbci/form-core
@msbci/form-core
Pure TypeScript form engine — zero external dependencies. The logic core shared by all @msbci/form-* packages.
Installation
npm install @msbci/form-core
Concepts
The form hierarchy follows an explicit composition model:
IFormDefinition
└── pages: IFormPage[] ← Navigation units
├── variables: IFormVariable[] ← Input fields
└── rosters: IFormRoster[] ← Pilot-driven sub-sections
└── variables: IFormVariable[]
- IFormPage — A navigation step containing fields and rosters
- IFormVariable — A single input field (text, number, date, select, etc.)
- IFormRoster — A repeating section driven by
pilotVariableCode(no add/remove — row count comes from a numeric variable) - IFormDefinition — The top-level form schema containing pages
Rosters are sub-components of pages, never at the same level. The relationship is modeled via composition (IFormPage.rosters), not parentId.
Engines
| Engine | Purpose |
|---|---|
ConditionEngine |
Evaluate ${VAR} expressions with 20+ built-in functions, caching, iteration support |
ValidationEngine |
Rule-based validation (required, min, max, pattern, email, url) + ConditionEval |
NavigationEngine |
Page ordering, visibility filtering, instance management, progress tracking |
FormTree |
Centralized visibility and jump management — evaluates all conditions in one pass |
InstanceManager |
Add/remove instances for repeatable pages with min/max constraints |
DependencyResolver |
Inter-variable dependency graph with transitive resolution |
CalculationEngine |
Champs calculated — ordre de calcul, portee (page, instance, ligne), delegation a ConditionEngine |
SchemaValidator |
Design-time structural validation of form definitions |
Usage
import {
ConditionEngine,
FormTree,
NavigationEngine,
type IFormDefinition,
type IFormPage,
type IFormVariable,
} from '@msbci/form-core'
// Define a form
const form: IFormDefinition = {
id: 'demo',
code: 'REGISTRATION',
name: 'Registration Form',
version: '1.0.0',
isPublished: true,
pages: [
{
id: 'p1',
code: 'INFO',
name: 'Personal Information',
order: 0,
isRepeatable: false,
variables: [
{
id: 'v1', code: 'NAME', name: 'Full Name', type: 'text',
order: 0, isRequired: true, isReadonly: false, isHidden: false,
},
{
id: 'v2', code: 'AGE', name: 'Age', type: 'number',
order: 1, isRequired: true, isReadonly: false, isHidden: false,
},
],
rosters: [
{
id: 'r1', code: 'CHILDREN', name: 'Children',
rosterType: 'collection', order: 2,
pilotVariableCode: 'NB_CHILDREN',
variables: [
{
id: 'rv1', code: 'CHILD_NAME', name: 'Child Name', type: 'text',
order: 0, isRequired: true, isReadonly: false, isHidden: false,
},
],
},
],
},
],
}
// Evaluate conditions
const engine = new ConditionEngine({
AGE: { variableCode: 'AGE', value: 25 },
})
engine.evaluate('${AGE} >= 18') // true
engine.evaluate('show(${AGE} < 65)') // true
// Build form tree for visibility
const tree = new FormTree()
tree.build(form, { AGE: { variableCode: 'AGE', value: 25 } })
const visible = tree.getVisibleVariables('INFO')
Variable Types
text · textarea · number · date · datetime · time · select · multiselect · checkbox · radio · file · gps · calculated · hidden · label · signature
v1.15.0
Un tableau ne pouvait pas changer de page. Un champ et un panneau le pouvaient, un tableau non — et les trois arrivaient a destination avec des positions incoherentes. Ce lot pose le deplacement d'un element de page, contenu compris, et ajoute deux reglages d'affichage sur les instances d'un tableau.
- Nouveau module
utils/move:movePageItemInForm(form, fromPageCode, toPageCode, itemCode, options?). Il deplace un element de premier niveau — tableau, panneau ou champ simple — d'une page vers une autre, avec tout son contenu : un panneau emmene ses champs, un tableau emmene ses colonnes. Fonction pure, le formulaire recu n'est pas modifie ; rendnullquand une page ou l'element est introuvable, ou quand rien ne bougerait. - Un deplacement n'est pas une duplication : aucun code n'est reecrit. C'est ce qui le rend sur — une condition qui interrogeait le champ deplace continue de le designer, puisqu'elle le designe par son code et que ce code ne change pas. Le validateur ne signale donc rien, et c'est correct.
- Mais l'ordre de saisie, lui, peut basculer. Une condition de page 2 qui interroge un champ desormais en page 7 ne peut plus se declencher : la valeur n'est pas encore saisie quand la page 2 s'affiche. Le validateur n'a rien a redire — le code existe.
movePageItemInFormrend donc, dansforwardReferences, les seules references que ce deplacement fait basculer en avant : celles qui l'etaient deja ne lui sont pas imputees.findForwardReferences(form)est exposee separement pour un controle global. Les emplacements qui citent un code sont couverts comme ils le sont pour le renommage : conditions de page, de tableau et de champ, expression d'un champ calcule, variable pilote, variable de comptage d'instances, dependances de source de donnees, contenu interpole d'unrichtextet gabarits de libelle. - Les deux pages sont renumerotees de bout en bout. Une renumerotation partielle laisse des trous a l'origine et des doublons a l'arrivee ; deux elements de meme
orderse rangent alors selon l'ordre d'insertion, c'est-a-dire au hasard d'une relecture a l'autre. Position d'arrivee reglable paroptions.index, fin de page par defaut. - Un champ regroupe dans un panneau peut lui aussi changer de page : il est sorti du panneau et arrive au premier niveau de la page choisie. Une colonne de tableau, elle, n'est pas un element de page — elle appartient au gabarit de ligne et ne voyage qu'avec son tableau.
- Deux reglages d'affichage sur
IFormRoster:hideInstanceTitlemasque le titre porte par chaque instance encadree — le libelle du tableau repete, suivi du numero — etshowRowNumberpose alors un numero de ligne a sa place. Sur une notice administrative, un intitule de deux lignes repete au-dessus de chaque cadre double la hauteur de la rubrique sans rien apporter quand il n'y a qu'une instance. - Impact : strictement additif — un module nouveau, deux proprietes facultatives, aucune signature existante modifiee. Les deux reglages sont absents par defaut : un formulaire existant decrit exactement le meme rendu. 19 tests ajoutes, 491 passants.
v1.14.0
Recreer a la main quatre rubriques de meme profil sur un formulaire de huit pages etait le geste le plus couteux du concepteur. Ce lot apporte la duplication d'un sous-arbre — page, tableau, panneau, champ — et traite ce qui la rend delicate : les references croisees.
- Generer des codes uniques n'aurait pas suffi. Une condition, une expression de champ calcule, une variable pilote ou une dependance de source de donnees designe un champ par son code. Une copie qui aurait garde les references de l'original aurait pilote l'original au lieu d'elle-meme : le formulaire aurait paru correct dans le concepteur et se serait comporte de travers au remplissage. Nouveau module
utils/duplicate:duplicatePageInForm,duplicatePageItemInForm, et les constantesDEFAULT_COPY_NAME_SUFFIX/DEFAULT_COPY_CODE_SUFFIX. - La regle tenue, a toutes les profondeurs. Une reference vers un element interne au sous-arbre duplique est reecrite vers la copie ; une reference vers un element exterieur reste intacte. Sur une page dupliquee, un « si oui, remplir ci-dessous » interne suit la copie, tandis qu'une condition qui depend d'une reponse d'une autre page continue de lire cette autre page.
- Un seul mecanisme de reecriture, pas deux. Le sous-arbre clone est isole dans un formulaire porteur reduit a lui, sur lequel on applique
renameVariableCodeInForm— celui-la meme qui sert au renommage d'un code — une fois par code interne. Ce qui n'est pas dans le porteur ne peut pas etre atteint : la regle ci-dessus est obtenue par construction, et non par une seconde implementation qui aurait derive de la premiere. - Les codes generes tiennent la grammaire.
ORIGINE_COPIE, puisORIGINE_COPIE2,ORIGINE_COPIE3; jamais un suffixe purement numerique, quecheckCodeFormatrefuse a juste titre puisqueMONTANT_2designerait la ligne 2 deMONTANT. Dupliquer une copie n'empile pas les suffixes. L'unicite est verifiee sur tout le formulaire, pas sur la seule page, et un code herite que la grammaire refuse est assaini avant d'etre derive. - Le nom de la copie dit qu'elle en est une, dans chaque langue renseignee :
{ fr: 'Equipements', en: 'Equipment' }devient{ fr: 'Equipements (copie)', en: 'Equipment (copy)' }. La marque est reglable parcopyNameSuffix; les identifiants le sont pargenerateId, ce qui rend la duplication reproductible en test. - Une copie liee a un modele de variable en est detachee. Un champ portant un
templateIdtient son code et son nom du modele, qui les reimpose a la relecture : conservee liee, la copie aurait repris le code de l'original et la collision serait revenue apres enregistrement. Les valeurs etant deja denormalisees sur le champ, la copie les garde toutes et devient un champ autonome. renameVariableCodeInFormreecrit un emplacement de plus. Le contenu d'un champrichtextest interpole a l'affichage : il cite des codes au meme titre qu'une expression, et etait le seul emplacement que le renommage laissait derriere lui. Corrige pour le renommage comme pour la duplication.- Impact : strictement additif — un module nouveau, aucune signature existante modifiee. La seule evolution de comportement porte sur le renommage d'un code, qui met desormais a jour le contenu d'un champ
richtextau lieu de le laisser pointer vers l'ancien code. 25 tests ajoutes, 472 passants.
v1.13.0
- La valeur d'une piece jointe ne pouvait etre qu'une data URL, donc le fichier lui-meme. Un formulaire portant trois plans numerises voyait le contenu de ces trois fichiers voyager dans la ligne de soumission, et le nom d'origine se perdre en route : la valeur ne le portait nulle part. Nouveau type
IStoredFile(kind: 'stored-file',refopaque,fileName,contentType,size,checksum,uploadedAt,adapter) et nouveau type de porteeIAttachmentScope: une reponse peut desormais designer un fichier depose ailleurs au lieu de le transporter. - Le type vit dans le coeur, pas dans le paquet de stockage, et c'est un choix. Le coeur est sans dependance et tourne dans un navigateur ; le stockage est du code serveur, avec ses flux et ses connecteurs. Faire dependre le coeur du stockage embarquerait ce dernier dans tout paquet de rendu, donc chez l'utilisateur final. Le type est de forme identique des deux cotes : une reference produite par un connecteur satisfait celle que le coeur declare, sans qu'aucun des deux paquets n'importe l'autre.
- Nouveau module
utils/stored-file: les lecteurs bivalents.isStoredFile,attachmentFileName,attachmentContentType,attachmentByteLength,attachmentEntries, etdataUrlFileName(jusqu'ici loge dans le rendu, desormais partage — signature et comportement inchanges). L'ordre de reconnaissance est fixe : reference d'abord, reconnue au discriminantkindet jamais a la forme, data URL ensuite, chaine nue en dernier. Un objet{ name, url }produit par un hote tiers n'est donc pas pris pour une reference. - La mesure de taille accepte les deux formes.
findOversizedAttachmentsmesurait une chaine, et ignorait silencieusement toute entree qui n'en etait pas une : une reference serait passee sans jamais etre pesee. Elle lit maintenant la taille inscrite dans la reference, et la charge decodee d'une data URL comme auparavant. - Le plafond de taille peut venir de l'hote, et c'est lui qui fait autorite.
attachmentMaxBytes(variable, hostMaxBytes?)accepte une borne d'exploitation. UnmaxBytesdeclare dans un formulaire ne peut alors que la resserrer, jamais l'elargir : sans cette regle, un exploitant qui releve sa limite a 20 Mio verrait quand meme ses formulaires refuser a 5 Mio, sans explication. Meme parametre optionnel surfindOversizedAttachments, surValidationEngine.validateVariableetvalidateByType, viaIValidatePageOptions.attachmentMaxByteset le nouveauIValidateFormOptionsdevalidateForm. Absente, la borne du formulaire — ou le defaut de 5 Mio — s'applique exactement comme avant. - Impact : strictement additif — deux types nouveaux, un module nouveau, des parametres tous optionnels ajoutes en fin de signature. Aucune signature existante modifiee, aucun comportement change en l'absence des nouveaux arguments. Une soumission portant des data URL est lue, mesuree et validee a l'identique, et les deux formes cohabitent indefiniment dans une meme soumission : aucune reprise n'est un prealable. 16 tests ajoutes, 447 passants.
v1.12.0
- Rien dans le schema ne disait de quand datait un formulaire. Deux concepteurs ouvrant le meme formulaire dans deux onglets l'enregistraient l'un apres l'autre, et le second effacait le travail du premier sans qu'aucun des deux ne le sache : l'enregistrement remplace l'arbre en bloc, et aucune information ne permettait de detecter que la version modifiee n'etait plus celle qui se trouvait en base. Nouvelle propriete facultative
IFormDefinition.updatedAt(horodatage ISO 8601 de la derniere ecriture, tel que le serveur le renvoie au chargement). - C'est la version du formulaire, pas une donnee d'affichage. Un client la recoit au chargement et la renvoie telle quelle a l'enregistrement ; le serveur la confronte a l'etat en base et refuse l'ecriture si elle a change entre-temps. Elle n'a de sens que pour ce dialogue : ni le moteur, ni la validation, ni la navigation ne la lisent.
- Impact : strictement additif — une propriete facultative de plus sur un type existant, aucune signature modifiee. Un client qui l'ignore de bout en bout garde exactement le comportement anterieur, sans controle de concurrence. Aucun test ajoute a ce paquet, 431 passants.
v1.11.0
- Aucun plafond de taille ne protegeait les pieces jointes. Une piece deposee est stockee en base64 dans la soumission : le fichier entier voyage dans la ligne enregistree, et l'encodage lui ajoute environ un tiers de son poids. Rien n'empechait un plan numerise de quarante mega-octets. Nouveau module
utils/attachments:ATTACHMENT_DEFAULT_MAX_BYTES,attachmentMaxBytes,findOversizedAttachments,isAttachmentVariable,attachmentTooLargeMessage, et les nouvelles proprietesIFileConfig.maxBytes/IImageConfig.maxBytes(plafond par fichier, en octets decodes). - Le plafond s'applique meme sans declaration, et c'est le but. Aucun formulaire concu avant cette version ne porte
maxBytes: ce sont precisement ceux-la qui n'ont aucune protection. En l'absence de valeur declaree, le plafond par defaut de 5 Mio s'applique. Le choix couvre l'usage administratif courant (une page A4 numerisee a 200 ppp pese quelques centaines de kilooctets, un dossier de dix pages quelques mega-octets) tout en gardant la ligne manipulable : encodee, une piece de 5 Mio en occupe environ 6,7. Un concepteur qui a besoin de davantage releve la valeur explicitement ; personne ne se retrouve sans limite par omission. Une valeur declaree nulle ou negative est ignoree plutot que d'interdire tout depot. ValidationEnginerefuse une piece hors plafond avec la reglefileSize, surfile,imageetphoto, en mode fichier unique comme en mode multiple, chaque piece etant mesuree separement. Le message nomme la variable, le poids recu et la limite, en francais et en anglais. Comme le serveur reutilise ce meme moteur, le refus vaut des deux cotes sans qu'aucune regle soit ecrite deux fois.utils/data-url: la mesure d'une data URL, jusqu'ici logee dans le module signature, vit dans son propre module et se partage avec les pieces jointes.dataUrlByteLengthest inchange et toujours exporte par la racine du paquet ;formatByteSizes'y ajoute et rend un poids lisible dans la langue demandee.- Impact : additif sur les types et les exports (deux proprietes optionnelles, un module nouveau, aucune signature modifiee). Un seul changement de comportement, assume et voulu : une piece jointe au-dela de 5 Mio est desormais refusee sur un formulaire qui ne declarait aucun plafond. Un formulaire dont les pieces restent sous cette limite est rendu et evalue exactement comme avant. 10 tests ajoutes, 431 passants.
v1.10.0
- Aucun type de signature n'existait. Un dossier administratif se conclut par un engagement du declarant, et rien ne permettait de le porter : la seule approximation possible etait une case a cocher doublee d'un champ de texte, sans horodatage ni trace. Nouveau type de variable
'signature'et nouveau moduleutils/signature:ISignatureConfig(reglages, portes parIFormVariable.signatureConfig),ISignatureValue(valeur enregistree),buildSignatureValue,isSignatureValue,isSignatureFilled,formatSignatureValue,dataUrlByteLength,signatureMaxImageBytes, et les constantesSIGNATURE_DEFAULT_MAX_IMAGE_BYTES/SIGNATURE_DEFAULT_HEIGHT. - Ce que le champ etablit, et ce qu'il n'etablit pas. Il enregistre le nom declare par le signataire, l'horodatage de la saisie et le texte d'engagement en vigueur a ce moment. Ce n'est pas une signature electronique qualifiee : aucun certificat, aucune autorite, aucun horodatage opposable, aucun scellement du document. Un tiers ne peut prouver par cette seule valeur ni l'identite du signataire, ni que le formulaire n'a pas ete modifie ensuite. La valeur juridique releve d'un dispositif de signature, hors perimetre de ces composants — le vocabulaire du module s'en tient donc a « engagement », jamais a « certification ».
- Deux formes, un seul contrat de valeur.
mode: 'typed'(defaut) est une acceptation nominative horodatee, qui pese quelques centaines d'octets.mode: 'drawn'y ajoute une trace manuscrite, transportee dans la valeur en data URL PNG. Les deux produisent la meme formeISignatureValue, de sorte qu'un hote qui injecte son propre composant de saisie compose exactement ce que le rendu par defaut compose, viabuildSignatureValue. - Le poids de la trace est plafonne. Une image dans la soumission fait gonfler la ligne enregistree, comme le font deja les pieces jointes.
ValidationEnginerefuse une trace au-dela designatureConfig.maxImageBytes(defaut 120 Ko decodes) avec la reglesignatureSize, plutot que de laisser passer une valeur qui grossira la base sans que personne ne le voie. - Une signature vide n'etait pas detectable.
isEmptyrendfalsesur tout objet : un champ signature obligatoire portant{}aurait passe la validation. Le controle « obligatoire » d'une variable de typesignatures'appuie desormais surisSignatureFilled— un nom non vide, et un trait en mode manuscrit. Le comportement deisEmptylui-meme est inchange. - Impact : strictement additif — un type de variable de plus, un champ optionnel sur
IFormVariable, un module nouveau. Aucune signature existante modifiee. Un formulaire qui ne porte pas de champ signature est valide et evalue exactement comme avant. 16 tests ajoutes, 421 passants.
v1.9.0
- Un message de validation integre affichait
[object Object]. Le nom d'une variable est unLocalizedString: interpole dans un gabarit (`${variable.name} is required`), un nom multilingue etait converti en chaine par le moteur JavaScript et devenait[object Object] is required. Trois messages etaient concernes — obligatoire, valeur numerique attendue, date invalide — et tous les trois etaient rediges en anglais, dans une suite dont le francais est la langue par defaut. Ils sont desormais composes langue par langue, a partir du nom resolu dans chacune, et restent desLocalizedStringque le rendu resout au moment de l'affichage : « Raison sociale » est obligatoire en francais,"Company name" is requireden anglais. Le message implicite de nombre minimal de fichiers et le message de repli d'une condition de validation sont localises de la meme facon. - Impact : aucune signature modifiee, aucune regle d'evaluation changee — seul le texte des messages par defaut evolue, et il devient lisible la ou il ne l'etait pas. Un message defini par l'auteur du formulaire (
IValidationRule.message) n'est jamais touche. 4 tests ajoutes, 405 passants.
v1.8.0
- Le champ calcule n'etait jamais calcule. Le type
calculatedexistait,IFormVariable.expressionetait stocke et son absence refusee par le validateur — mais aucun moteur ne l'evaluait. Le champ s'affichait en lecture seule avec une pastille de formule et restait definitivement vide : pas de total de colonne, pas de sous-total, pas de report de montant. Nouveau moteurCalculationEngine(compute/computeEntries), qui n'evalue rien lui-meme : il resout l'ordre de calcul, la portee et le cas de la dependance vide, puis delegue aConditionEngine. Champ calcule et condition parlent donc le meme langage, avec les memes fonctions, et ne peuvent pas diverger. - Trois decisions portees par le moteur. Quand : la valeur est recalculee des qu'une dependance change, et a l'ouverture d'un brouillon. Quoi tant qu'une dependance manque : la valeur est
nullet le champ reste vide —${A} + ${B}sansBne vaut pasA; les arguments des fonctions d'agregation sont exclus de cette regle, une somme de colonne devant se calculer sur les lignes remplies. Portee : un champ calcule place dans un tableau produit une valeur par ligne (CODE_N), evaluee dans la portee de sa ligne ; sur une page repetable, une valeur par instance. Un champ calcule qui en reference un autre est evalue apres lui ; un cycle laisse les valeurs concernees vides plutot que de boucler. - Les fonctions d'agregation s'arretaient au premier trou.
sum(),count(),avg(),any(),all()parcouraient les lignes tant qu'une valeur existait et s'arretaient a la premiere ligne vide. Sur un tableau de cinq lignes dont la deuxieme etait incomplete, la somme s'arretait a la premiere : un total etait faux des qu'une ligne etait incomplete, c'est-a-dire la norme en cours de saisie. Le parcours va desormais jusqu'au plus grand numero de ligne present et ecarte les lignes vides, ce qui preserve la semantique de chaque fonction.sum()etavg()acceptent en outre un nombre revenu sous forme de chaine d'un brouillon, jusqu'ici silencieusement ignore. ConditionEngine.evaluateExpression(expression, iteration?)— rend la valeur d'une expression (nombre, chaine, date), la ouevaluatene rend qu'un booleen. Meme pipeline, memes fonctions integrees.undefinedquand l'expression est vide ou ne s'evalue pas.ValidationEngine.validateForm(form, responses, tree?)— valide toutes les pages visibles en un parcours, chaque erreur portantpageCodeetpageName. Ce parcours devient celui du navigateur et du serveur : jusqu'ici chacun avait le sien, et ils divergeaient sur un tableau masque par une condition — le serveur en exigeait les lignes, le rendu n'en exigeait aucune. Trois regles y sont tenues ensemble : une page masquee n'est pas validee, un champ masque par une condition non plus, un tableau masque n'exige aucune ligne.IFieldValidationErrorgagnepageCode?etpageName?, renseignes parvalidateFormseul.- Conditions ligne a ligne — decision :
RosterConditionEnginen'est pas branche. Son evaluation d'execution ferait double emploi avecFormTree, qui evalue deja les conditions d'une ligne de tableau dans la portee de sa ligne (getVisibleVariablesForInstance), et serait plus restrictive : elle isole la ligne, donc une condition referencant un champ de page cesserait de fonctionner. Sa verification de conception, elle, interdit qu'une condition de tableau reference un champ hors du tableau, ce queSchemaValidatorautorise explicitement. Deux moteurs qui se contredisent seraient pires qu'un seul. Le module reste disponible pour un outil de conception qui voudrait ces controles, sans etre appele en production. - Impact : strictement additif — un moteur nouveau, deux methodes nouvelles, deux champs optionnels. Aucune signature existante modifiee. Un formulaire sans champ calcule est evalue exactement comme avant. Seul changement de comportement assume : une agregation portant sur un tableau a lignes incompletes rend desormais le bon resultat, la ou elle en rendait un faux. 18 tests ajoutes, 401 passants.
v1.7.0
- Une page n'avait aucune subdivision : le panneau ne contenait rien. Le type
panelrendait un encadre porteur d'un titre et d'une description, pose a cote des champs et non autour d'eux. La seule granularite de section disponible etait donc la page, c'est-a-dire une etape de navigation : un document administratif en six rubriques devenait six etapes, ou une page plate sans structure.IFormVariableporte desormaischildren?: IFormVariable[], renseigne uniquement sur une variable de typepanel. Etendre la variable plutot qu'introduire un nouveau type d'element de page est un choix delibere : un panneau reste un element deIFormPage.items[], il se deplace, se reordonne et se conditionne exactement comme avant, et un schema anterieur reste lisible sans migration. - La contrainte est tenue par le modele, pas par l'interface. Un panneau contient des champs simples, et rien d'autre : ni tableau, ni panneau imbrique, ni enfant portant lui-meme des enfants.
SchemaValidatorrefuse chacun de ces trois cas, ainsi que deschildrenportes par une variable qui n'est pas un panneau. La profondeur de l'arbre reste donc fixe : aucun consommateur n'a de recursion a gerer, et un arbre invalide est rejete quelle que soit sa provenance. - Deux parcours, deux usages.
getPageVariables(page)continue de rendre la structure telle qu'elle est saisie — un panneau y compte pour un seul element — et reste le parcours des composants de mise en page. Le nouveaugetPageFields(page)aplatit les panneaux et rend tous les champs de la page, chaque panneau suivi de ses enfants : c'est le parcours des moteurs, qui travaillent sur des champs et non sur une mise en page.getPanelChildren(variable)rend les enfants tries parorder(liste vide pour toute autre variable),isPanelVariablecomplete les discriminants existants. - Les moteurs voient les champs regroupes. Condition, validation, dependances, comptage d'instances d'une page repetable, enrichissement des reponses et verification des references d'expression parcourent desormais
getPageFields: un champ place dans un panneau se valide, se conditionne et se refere comme un champ de page.renameVariableCodeInFormreecrit egalement les enfants d'un panneau. - Impact : strictement additif — un champ optionnel, trois fonctions nouvelles, aucune signature existante modifiee. Un panneau sans enfants se comporte exactement comme avant, et un formulaire enregistre avant ce lot est inchange a la lecture comme au rendu. 15 tests ajoutes, 383 passants.
v1.6.0
- Le code d'une variable n'avait ni regle de forme partagee ni renommage sur l'arbre. La grammaire
${CODE}du moteur de conditions et de l'analyseur d'expressions n'accepte que[A-Z_][A-Z0-9_]*, et un code hors de ce sous-ensemble n'est pas rejete : il est ignore, ce qui produit une condition qui ne se declenche jamais. Cette regle n'etait ecrite nulle part hors des expressions regulieres des moteurs, donc inexploitable par un outil de conception. Nouveau moduleutils/variable-codeexportantcheckCodeFormat(code, options?),isValidCode, le typeCodeFormatIssueet la constanteVARIABLE_CODE_PATTERN. Le constat est rendu, pas affiche : l'appelant recoit'empty','invalidCharacters','reserved'ou'rowSuffix'et choisit son libelle et sa langue. - Deux refus meritent une explication.
'reserved'couvre_INDEX_, que le moteur reserve au numero d'instance d'une page repetable.'rowSuffix'couvre un code termine par_suivi de chiffres :${MONTANT_3}designe deja la ligne 3 du tableauMONTANT, un code ainsi forme rendrait la resolution ambigue. L'optionallowRowSuffixleve ce dernier refus pour les codes qu'aucune expression ne designe — celui d'une page, celui d'un tableau — dont les valeurs generees (PAGE_1) l'utilisent deja. - Renommage propage sur l'arbre en memoire.
renameVariableCodeInForm(form, oldCode, newCode)reecrit les sept emplacements qui referencent un code de variable : conditions de page, de tableau et de variable, expression d'un champ calcule, variable pilote d'un tableau, variable de comptage d'instances d'une page repetable, dependances de source de donnees, et gabarits de libelle (${CODE}dans un nom de variable ou un libelle d'instance). Fonction pure, tolerante au schema historique (variables[]/rosters[]) comme au schema unifie (items[]). Elle repond au cas ou l'hote enregistre l'arbre complet en un seul appel : ce chemin remplace les pages telles qu'elles arrivent, sans rien reecrire, la ou une modification granulaire de code cote serveur les reecrit.rewriteCodeInExpressionetexpressionReferencesCodesont exportes pour les usages unitaires. - Impact : strictement additif — un nouveau module, aucune signature existante modifiee, aucun comportement change. 10 tests ajoutes, 368 passants.
v1.5.0
- Rouvrir une condition en mode visuel la detruisait. Cause : aucun convertisseur d'expression vers forme structuree n'existait ; l'editeur repartait toujours d'un groupe vide. Correctif cote coeur : nouveau module
utils/condition-expressionexportantparseConditionExpression(expression)et le typeIParsedConditionExpression. Il analyse exactement la grammaire produite par le constructeur visuel (enveloppesshow/hide/require/readonly/ConditionEval/setValue, operateurs infixes,isEmpty/isNotEmpty/contains/startsWith/endsWith, litteraux texte, nombre et booleen) et refuse en bloc tout ce qui n'en releve pas (saut de champ, melange&&/||, agregation, fonction inconnue) plutot que de rendre une expression approximative. L'appelant peut ainsi verrouiller son mode visuel au lieu d'ecraser la condition. - Les conditions de tableau n'etaient evaluees nulle part.
FormTreeindexe desormais les tableaux du formulaire et exposeisRosterVisible(rosterCode, instanceNumber?), qui combine les conditionsshow/hidedu tableau et tient compte de la visibilite de sa page. Un code inconnu est considere visible : le rendu reste seul juge de ce qu'il connait. - Une seule condition etait prise en compte. Sur une page, la premiere condition l'emportait et les suivantes n'etaient jamais lues ; sur une variable, la derniere ecrasait les precedentes. Les conditions d'affichage d'un meme conteneur ou d'une meme variable se combinent maintenant par un ET, et les conditions
require/readonlyd'une meme variable par un OU (il suffit qu'un motif soit rempli). Changement de comportement assume : identique tant qu'il n'y a qu'une condition, ce qui est le cas de tout ce que l'editeur produisait jusqu'ici, la deuxieme condition n'ayant jamais eu d'effet observable. - Bornes de date inertes. Les regles
min/maxne comparaient que des nombres : posees sur un champdateoudatetime, elles ne declenchaient jamais.ValidationEngine.applyRulecompare desormais les dates quand la variable est de type date et que la borne est une date analysable. Ajout strictement additif : les bornes numeriques sont inchangees, et une borne non analysable reste sans effet comme auparavant. SchemaValidatoretendu — verification des references : expression de page, de tableau ou de variable designant un code inexistant, variable pilote ou variable de controle absente, tableaucheck/listsans options ni pilote (le rendu retombait silencieusement sur une ligne unique). Les controles existants (unicite des codes, champ calcule sans expression, liste sans options) sont inchanges.- Impact : aucune signature existante modifiee, tous les ajouts sont optionnels ou nouveaux. 27 tests ajoutes, 358 passants.
v1.4.0
- Les lignes de tableau n'etaient jamais validees. Cause :
ValidationEngine.validatePageacceptait uninstanceNumberet construisait sa clef suffixee, mais parcourait les variables d'un roster une seule fois, sur la clef non suffixee — le moteur ne connaissait structurellement pas le nombre de lignes. Consequence : un tableau dont toutes les colonnes sont obligatoires passait la soumission entierement vide, et le message « obligatoire » se declenchait a tort sur une ligne remplie. C'est le seul defaut du lot qui rendait une soumission faussement valide. - Correctif :
validatePageaccepte un cinquieme parametre optionnelIValidatePageOptions(rosterRowCounts,isRequiredOverride) et boucle sur les N lignes de chaque tableau, en lisantCODE_Net en reportant le numero de ligne dans l'instanceNumberde chaque erreur. Sans options, le nombre de lignes est deduit des reponses parresolveRosterRowCount. - Nouveau module
utils/roster-rows—rosterRowCountKey,isRosterRowCountKey,getRosterRowBounds,deduceRosterRowCount,resolveRosterRowCount,ROSTER_ROW_COUNT_SUFFIX,DEFAULT_ROSTER_MIN_ROWS. Le nombre de lignes est persistable sous une clef reservee (CODE__rowCount), hors de l'espace des codes de variables. Compatibilite des donnees : une soumission anterieure qui ne porte pas cette clef reste lisible — le nombre de lignes est deduit du plus grandCODE_Nportant une valeur, borne parcollectionConfig. - « Obligatoire si » et « lecture seule si » se comportaient comme « Afficher si ». Cause : les actions
requireetreadonlyn'etaient implementees nulle part, etFormTree.evaluateVariableConditionsretombait sur la branche de visibilite — le champ disparaissait quand la condition etait fausse, au lieu de devenir facultatif. Meme defaut poursetValue, dont l'expression nue etait relue comme une condition de visibilite. - Correctif :
FormTreetraite l'action avant la visibilite.require/readonlyalimentent deux nouveaux champs optionnels deIVariableNodeState(isRequiredOverride,isReadOnlyOverride), lisibles via les nouvelles methodesgetRequiredOverride()/getReadOnlyOverride()(instance comprise) ;validateetsetValuene touchent plus aisConditionMet.ConditionEnginereconnait les enveloppesrequire(condition)etreadonly(condition)au meme titre queshow/hide/setValue. Les expressions nues enregistrees par une version anterieure sont traitees correctement : c'est l'action declaree qui decide, pas la forme de l'expression. validateVariableaccepte un quatrieme parametre optionnelisRequiredOverride, qui prime survariable.isRequiredet neutralise aussi une reglerequireddeclaree en dur.- Impact : aucune signature existante modifiee, tous les ajouts sont optionnels. Un appelant qui n'a pas mis a jour son code obtient le comportement anterieur pour les variables de page, et beneficie desormais de la validation ligne a ligne des tableaux (les lignes etant deduites des reponses). 19 tests ajoutes, 331 passants.
v1.3.5
- Report de
fileConfig/imageConfiga la resolution des modeles —resolveVariableTemplate()reconstruit integralement la variable resolue champ par champ, et ne listait nifileConfigniimageConfig. Consequence : sur le chemin resolu (une variable liee a un modele de variable), les contraintes d'upload etaient perdues — celles du modele comme celles portees par la variable — et le champ retombait en upload simple sans filtre, alors meme qu'elles etaient correctement persistees. Les deux objets sont desormais reportes avec la meme semantique que les autres champs optionnels : substitution simple (l'objet est remplace en entier, jamais fusionne en profondeur), la surcharge locale (templateOverrides) primant sur la valeur du modele, la valeur propre de la variable servant de dernier recours. Quatre tests ajoutes (heritage, surcharge, valeur propre, absence). - Correction de bug additive et retrocompatible : aucune signature modifiee, aucun objet vide introduit quand ni le modele ni la variable ne configurent d'upload.
v1.3.4
- Documentation : retrait d'une référence à un projet spécifique dans le changelog au profit d'une formulation générique. Aucun changement de code ni d'API.
v1.3.3
- Multi-upload
file/image— nouveaux typesIFileConfig(maxFiles,minFiles,accept) etIImageConfig(maxImages,minImages,allowCamera) exposés surIFormVariable.fileConfig/imageConfigET surIVariableTemplate(donc surchargeable viaIVariableTemplateOverrides). - Contrat de valeur rétrocompatible :
maxFiles/maxImages === 1(ou absent) → la valeur reste unstringsimple. Au-delà de 1 →string[]. Aucune migration de schéma requise. - Nouveau
ValidationRuleType: 'minFiles'— règle explicite testant la longueur dustring[]. Une valeurstringlegacy compte comme 1 item (pas de cassure). - Garde implicite —
validateVariablelitfileConfig.minFiles/imageConfig.minImagesautomatiquement, sans avoir à wirer unevalidationRulesentry. - 9 nouveaux tests (
file-validation.test.ts) — 308 tests passants.
v1.3.2
- DataSourceConfig dynamique — nouveau type
IDataSourceConfigqui décrit une source de données stockée en base (code,url,method,queryParam,valueField,labelField,headersOverride,dependencies). Permet à l'admin de gérer ses connecteurs sans recompiler le host. IScopeHeaders— type de transport pour la réponse agrégéeGET /forms/:id/datasources({ scopeId, headers }). Les headers arrivent déjà décryptés côté renderer (le serveur fait le travail).IScope.headers?: Record<string, string>— surface optionnelle pour les GET de scope. Le champ reflète les headers décryptés (ou{}si la clé d'encryption manque).- Pas de breaking change — purement additif.
- 0 nouveau test côté core (la logique vit dans server/renderer/editor), 299 tests inchangés.
v1.3.1
IFormDefinition.scopeIds?: string[]— un formulaire peut désormais appartenir à plusieurs scopes, ordonnés par priorité (le premier scope l'emporte en cas de conflit decodelors de la résolution des templates).migrateFormScopeId(form)— migration transparente du champ legacyscopeId: stringversscopeIds: [string]. Câblée dansdeserializeForm: tout payload v1.3.0 est upgradé à la lecture.- Si
scopeIdetscopeIdssont tous deux présents,scopeIdsest conservé tel quel et le champ legacy est dropé. IFormDefinition.scopeIdest supprimé du type — utiliserscopeIds[].- 5 nouveaux tests (299 total)
v1.3.0
IScope,IVariableTemplate,IVariableTemplateOverrides— référentiel de variables réutilisables par scoperesolveVariableTemplate,resolveFormTemplates,collectTemplateIds— résolution Option B (in-memory)IFormPage.items[]— variables et rosters unifiés dans une liste ordonnée commune- Helpers
getPageVariables,getPageRosters,isPageVariable,isPageRoster,migratePageToItems,migrateFormToItems IFormVariable.templateId,templateOverrides— liaison optionnelle vers le référentielIFormDefinition.scopeId— appartenance d'un formulaire à un scope (remplacé parscopeIds[]en v1.3.1)- Compatibilité ascendante : les schémas legacy avec
variables[]+rosters[]sont migrés automatiquement à la désérialisation - 30 nouveaux tests (294 total)
v1.2.0
LocalizedString: support multilingue natif (string | Record<string, string>)resolveLabel,hasTranslation,getAvailableLangsIFormLangConfig+DEFAULT_LANG_CONFIGIFormDefinition.langConfig(stratégies :prop/browser/selector/auto)- 13 champs de schéma passés en
LocalizedString(name,description,placeholder,option.label,errorMessage,instanceLabel, ...) - Compatibilité ascendante : les schémas string simples existants fonctionnent sans modification
- 20 nouveaux tests (264 total)
What's new in v1.1.0
- New
RosterConditionEnginefor row-scoped condition evaluation with backward-jump, scope, self-reference and circular-dependency detection (DFS tricolor — beyond the original baseline). - New
enrichResponsesWithContextutility resolves option labels, interpolates${VAR}placeholders in variable labels, and attaches page/roster context metadata to each response. FormTree.executeAutoActionsnow cascades until stable (max 10 iterations, guards against circular dependencies).VariableTypeextended with 4 new values:panel,richtext,listradio,photo.IFormRoster.collectionConfig?: { min?, max? }added for dynamic row counts.IFieldResponseMetadataextended with 8 optional fields (displayValue,variableLabel, page / roster context, ...).- All changes are backwards-compatible — optional types and new exports only.
License
Copyright (c) 2026 MOSOBI — All rights reserved. Commercial license required. Contact: dev@mosobi.com