@msbci/form-renderer
v1.19.0
Published
React form renderer — themeable, data-source-aware, zero UI dependency
Readme
@msbci/form-renderer
React form renderer — themeable, data-source-aware, zero UI dependency. Renders forms defined by @msbci/form-core schemas.
Installation
npm install @msbci/form-renderer @msbci/form-coreMinimal Usage
import { FormRenderer } from '@msbci/form-renderer'
<FormRenderer
formSchema={myFormDefinition}
onSubmit={(responses) => api.saveSubmission(responses)}
mode="fill"
labels={{ submit: 'Submit', next: 'Next', previous: 'Previous' }}
/>Theme System
Inject your own colors, typography, and entire React components:
import type { IMosobiTheme } from '@msbci/form-renderer'
const myTheme: Partial<IMosobiTheme> = {
colors: {
primary: '#3B82F6',
secondary: '#8B5CF6',
background: '#FFFFFF',
surface: '#F9FAFB',
error: '#EF4444',
warning: '#F59E0B',
success: '#10B981',
text: '#111827',
textSecondary: '#6B7280',
border: '#E5E7EB',
},
// Override built-in components with your design system
components: {
Button: MyCustomButton,
Input: MyCustomInput,
Select: MyCustomSelect,
},
}
<FormRenderer formSchema={schema} theme={myTheme} />If theme.components.Select is provided, it replaces the default <select>. Otherwise, the built-in HTML component is used.
DataSource Connectors
Connect selection fields to live data with inter-field filtering:
import type { IDataSourceConnector } from '@msbci/form-core'
const dataSources: Record<string, IDataSourceConnector> = {
countries: {
id: 'countries',
name: 'Countries',
fetch: async ({ search }) => api.getCountries({ search }),
},
cities: {
id: 'cities',
name: 'Cities (filtered by country)',
fetch: async ({ dependsOn }) =>
api.getCities({ countryId: dependsOn?.countryId }),
},
}
<FormRenderer
formSchema={schema}
dataSources={dataSources}
theme={myTheme}
onSubmit={handleSubmit}
/>When a dependency value changes, the connected field automatically re-fetches with 150ms debounce and clears its current selection.
Modes
| Mode | Description |
|------|-------------|
| fill | Default — user can enter data and submit |
| readonly | All fields disabled, no submit button |
| review | Read-only with submission data pre-loaded |
<FormRenderer formSchema={schema} initialResponses={savedData} mode="readonly" />Apercu de mise en page (v1.15.0+)
designPreview est orthogonal a mode : il ne change pas la facon de saisir,
il suspend l'evaluation des conditions. Il s'adresse au concepteur qui verifie
une disposition, jamais a l'utilisateur qui remplit.
<FormRenderer formSchema={schema} designPreview />| | Rendu reel (defaut) | designPreview |
|---|---|---|
| Pages masquees par une condition | absentes | affichees |
| Champs masques par une condition | absents | affiches |
| Tableau sans ligne | aucune ligne | une ligne, pour montrer les colonnes |
| Champs calcules | calcules | calcules, a l'identique |
| « Suivant » | valide la page | ne valide rien |
| Depot | disponible | indisponible |
| Bandeau d'avertissement | absent | permanent |
Le bandeau n'est pas decoratif : sans lui, un concepteur croirait avoir teste
le comportement du formulaire alors qu'aucune condition n'a ete evaluee. Son
texte se remplace par labels.designPreview.notice.
@msbci/form-editor >= 1.12.0 offre la bascule entre les deux modes dans son
onglet d'apercu et transmet designPreview a son PreviewComponent.
Field Registry
MOSOBI Forms ships with built-in field components for all standard variable types. For custom or domain-specific fields, use the field registry to register your own components.
Built-in types
text · textarea · number · date · datetime · time · select · multiselect ·
checkbox · radio · file · image · email · gps · rating · calculated ·
hidden · label · panel · richtext · listradio · photoBehind the scenes 21 variable types are mapped to 13 components (e.g. text/textarea/email/calculated/hidden/label all share TextField; date/datetime/time share DateField; panel, richtext, listradio and photo each have a dedicated display-only or input component added in v1.1.0).
Registering a custom component
import { useState } from 'react'
import {
FormRenderer,
registerFieldComponent,
type FieldProps,
} from '@msbci/form-renderer'
import { useFormContext } from '@msbci/form-renderer'
// 1. Create a custom field component
function PhoneField({ variable, instanceNumber }: FieldProps) {
const { setValue, getValue, mode } = useFormContext()
const value = (getValue(variable.code, instanceNumber) as string) ?? ''
const readOnly = mode === 'readonly' || variable.isReadonly
const sanitize = (input: string) => input.replace(/[^0-9+\s-]/g, '')
return (
<input
type="tel"
value={value}
onChange={(e) => setValue(variable.code, sanitize(e.target.value), instanceNumber)}
placeholder={variable.placeholder ?? '+1 555 0100'}
readOnly={readOnly}
disabled={readOnly}
style={{ width: '100%', padding: 8, borderRadius: 4, border: '1px solid #ccc' }}
/>
)
}
// 2. Register it once at app initialization (e.g. in your entry file)
registerFieldComponent('phone', PhoneField)
// 3. Use it in a schema like any other type
const schema = {
id: 'F', code: 'CONTACT', name: 'Contact', version: '1.0.0', isPublished: true,
pages: [
{
id: 'P1', code: 'P1', name: 'Contact details', order: 0, isRepeatable: false,
variables: [
{
id: 'mobile', code: 'MOBILE', name: 'Mobile phone',
// The custom type registered above
type: 'phone' as const,
order: 0, isRequired: true, isReadonly: false, isHidden: false,
},
],
rosters: [],
},
],
}
function MyApp() {
return <FormRenderer formSchema={schema} onSubmit={(data) => console.log(data)} />
}Overriding a built-in type
registerFieldComponent overwrites whatever component is registered for the same key. Useful for replacing a built-in with a richer alternative:
import { registerFieldComponent, type FieldProps } from '@msbci/form-renderer'
function FancyDateField(props: FieldProps) {
// Wrap your favourite date picker library here
return <MyFavouriteDatePicker {...props} />
}
registerFieldComponent('date', FancyDateField)To restore a built-in, re-register the original component (re-imported from the package) or import it from @msbci/form-renderer directly.
FieldProps interface
export interface FieldProps {
/** The variable schema being rendered (code, type, options, validation, …). */
variable: IFormVariable
/** 1-based instance index when the variable lives in a repeatable page or roster row. */
instanceNumber?: number
}To access the surrounding form state inside a custom field, use the form context hook:
import { useFormContext } from '@msbci/form-renderer'
function MyField({ variable, instanceNumber }: FieldProps) {
const { getValue, setValue, mode, errors } = useFormContext()
const value = getValue(variable.code, instanceNumber)
const error = errors.find((e) => e.variableCode === variable.code)
// … render however you like
}TypeScript support
FieldProps is the canonical type for any field component:
import type { FieldProps } from '@msbci/form-renderer'
import type { ComponentType } from 'react'
const PhoneField: ComponentType<FieldProps> = ({ variable, instanceNumber }) => {
// …
return null
}If your custom type needs to be statically known to TypeScript (e.g. for autocomplete in schemas), you can broaden VariableType via module augmentation in your app:
// types/msbci-form-core.d.ts
declare module '@msbci/form-core' {
type VariableType = 'phone' | 'currency' // your additions
}Or simply use a built-in type (e.g. 'text') and discriminate in your component via variable.metadata.
Notes
- Registration is global. Call
registerFieldComponentonce at app startup; subsequent calls for the same key overwrite the previous binding. - Order of imports matters. Register before mounting
<FormRenderer>so the renderer sees your binding. - Built-in field components are also re-exported (
TextField,NumberField, etc.) and can be composed inside your custom one.
Impression et sommaire (v1.9.0+)
Trois options, toutes desactivees par defaut. Un hote qui n'en demande aucune obtient le rendu anterieur, sans un noeud ni un attribut de style de plus.
<FormRenderer
formSchema={schema}
enablePrint // feuille @media print + doublon textuel des valeurs
enableToc // sommaire des pages visibles
enableViewToggle // bascule « page par page » / « tout le formulaire »
view="paged" // vue initiale : 'paged' (defaut) ou 'full'
onViewChange={setView}
printOptions={{ pageMargin: '15mm', pageBreakBetweenPages: true }}
labels={{
toc: { title: 'Sommaire', errorCount: (n) => `${n} erreur(s)` },
view: { paged: 'Vue page par page', full: 'Tout le formulaire', print: 'Imprimer' },
summary: { title: 'Recapitulatif', rowLabel: 'Ligne', emptyValue: '—' },
}}
/>En vue 'full', le formulaire porte son propre depot : les boutons de page n'y ont pas d'objet,
mais le declarant doit pouvoir deposer sans repasser par la vue page par page. Le controle reste
celui de toutes les pages visibles, comme pour un depot ordinaire (v1.13.0+).
Pourquoi une feuille !important
Toute la mise en forme du rendu est portee par des styles en ligne, qu'aucune feuille de style
d'hote ne peut surcharger. Dans la cascade CSS, une seule chose passe devant un style en ligne
ordinaire : une declaration !important d'une feuille d'auteur. Chaque !important de la feuille
d'impression est donc la condition pour que la regle s'applique, et non une facilite d'ecriture.
Les regles sont toutes portees par @media print et par la classe racine du rendu : elles
n'existent qu'a l'impression, et seulement sous un formulaire.
La feuille retire les elements d'interface (navigation, progression, barres d'instances, aides de
saisie, boutons, fenetres modales), neutralise les couleurs de fond, empeche la coupure d'un champ
ou d'une ligne de tableau, et repete l'en-tete d'un tableau qui se poursuit sur la feuille
suivante (thead { display: table-header-group }).
Le doublon textuel des valeurs
Un champ de saisie imprime son cadre et tronque son contenu : aucune regle CSS ne fait grandir
un <input> pour reveler ce qu'il contient. Sous enablePrint, chaque champ rend donc, a cote de
son controle, un <span class="msbci-form-print-value"> portant la valeur mise en forme —
display: none a l'ecran, revele a l'impression, ou la feuille masque le controle lui-meme. Le
doublon est pose par FieldWrapper, donc il vaut aussi pour un composant de saisie injecte par
l'hote (theme.components.*) ou enregistre par registerFieldComponent.
Un hote qui remplace FieldWrapper lui-meme compose le meme resultat avec les exports
buildPrintStylesheet, PRINT_ROOT_CLASS, PRINT_UI_CLASS, PRINT_PAGE_CLASS,
PRINT_FIELD_CLASS, PRINT_VALUE_CLASS, formatResponseValue et formatVariableValue. Poser
PRINT_UI_CLASS sur un element propre a l'hote suffit a le retirer du document imprime.
Le sommaire
FormToc liste les pages visibles — une page masquee par une condition n'y figure pas —
permet d'en atteindre une directement et affiche le nombre d'erreurs qu'elle porte. Ces erreurs
sont celles du contexte, telles que la validation du formulaire complet les a produites
(validateAllPages, v1.7.0+) : aucun controle n'est refait, deux mecanismes de validation qui se
contredisent seraient pires qu'un seul. Le sommaire ne signale donc une page qu'apres une
tentative de depot ou de navigation.
Exported Components
FormRenderer · FormGroup · RosterRenderer · VariableRenderer · FieldWrapper · TextField · NumberField · DateField · CheckboxField · RadioField · SelectField · FormProgress · FormNavigation · FormSummary · FormToc · ValidationErrorsModal
All components are individually importable for advanced layouts.
hideLabel (optionnelle)
FieldWrapper et VariableRenderer acceptent hideLabel?: boolean. A true, le libelle du
champ est masque visuellement mais reste expose aux technologies d'assistance. A reserver aux
mises en page ou l'intitule est deja porte par un autre element — c'est ce que fait le rendu
collection_extend, dont l'en-tete de colonne tient lieu de libelle. Valeur par defaut : false.
Enregistrement automatique du brouillon (v1.19.0+)
onChange ne remonte qu'un code de variable et une valeur. Sur un tableau, deux lignes emettent
le meme code : un hote qui reconstruirait la carte des reponses a partir de ces notifications
ecraserait la premiere ligne par la seconde. Trois proprietes facultatives donnent acces a la
carte complete, sans passer par un clic simule sur le bouton de brouillon.
onResponsesChange — la carte complete a chaque changement
<FormRenderer formSchema={schema} onResponsesChange={(responses) => cache.current = responses} />Elle recoit la carte telle que le moteur la detient : clefs indexees (MONTANT_1, MONTANT_2),
compteurs de lignes, champs calcules et valeurs posees par les actions automatiques. Les valeurs
sont brutes : le contexte d'affichage (displayValue, intitules, page) n'est pas calcule ici,
parce qu'il le serait a chaque frappe.
Elle est emise a chaque changement. Un hote qui veut espacer son traitement conserve la derniere carte recue et n'agit qu'apres une pause de saisie : chaque emission portant l'etat complet, aucune valeur n'est perdue par un traitement differe.
autoSave — l'enregistrement tenu par le rendu
<FormRenderer
formSchema={schema}
onDraftSave={(responses) => api.saveDraft(responses)}
autoSave={{ idleMs: 5000, minIntervalMs: 30000 }}
/>L'hote ne fournit que onDraftSave : le rendu attend la fin de la saisie (idleMs, defaut
5000 ms), espace deux envois (minIntervalMs, defaut 30000 ms) et appelle onDraftSave avec la
meme carte enrichie que le bouton de brouillon. enabled: false suspend sans demonter le
rendu. Sans onDraftSave, la declaration reste sans effet.
onDraftSaveHandle — la commande, pour l'hote qui tient sa propre cadence
const commande = useRef<RequestDraftSave | null>(null)
<FormRenderer
formSchema={schema}
onDraftSave={(responses) => api.saveDraft(responses)}
onDraftSaveHandle={(c) => { commande.current = c }}
/>Le rendu publie sa commande d'enregistrement au montage, et null lorsqu'il n'y a rien a
enregistrer — pas de onDraftSave, ou mode autre que la saisie. L'appel rend 'started', ou
'busy' si un enregistrement est deja en vol ; la carte transmise a onDraftSave est celle du
bouton de brouillon. C'est le chemin des hotes dont la cadence depend d'autre chose que le temps
(bail entre onglets, passage en arriere-plan, retour du reseau, poids du dernier envoi).
data-testid="form-draft-button" reste pose sur le bouton, mais cesse d'etre un point d'accroche
officiel : le declenchement passe desormais par ces proprietes.
v1.19.0
Un hote qui voulait enregistrer un brouillon sans geste de l'utilisateur n'avait aucun moyen propre
d'obtenir la carte des reponses. onChange ne remonte qu'un code et une valeur, et sur un tableau
de deux lignes il emet deux fois le meme code : reconstruire la carte a partir de ces notifications
ecrasait la premiere ligne par la seconde — corruption silencieuse dans un formulaire a tableaux.
Le seul chemin restant etait de simuler un clic sur le bouton de brouillon, repere par son attribut
de test : l'hote dependait alors d'un detail d'implementation du rendu.
onResponsesChange(responses), nouvelle propriete facultative deFormRenderer(et option deuseFormEngine). Elle remonte la carte complete apres chaque changement, avec ses clefs indexees (MONTANT_1,MONTANT_2), les compteurs de lignes, les champs calcules et les valeurs posees par les actions automatiques. Elle est emise apres les actions automatiques, jamais sur un etat intermediaire, et pas du tout tant que rien n'a change. Les valeurs sont brutes : enrichir le contexte d'affichage a chaque frappe couterait un parcours complet des reponses pour une carte que l'hote n'enregistre pas a cette cadence.autoSave, nouvelle propriete facultative. L'hote ne fournit queonDraftSave; le rendu attend la fin de la saisie (idleMs, defaut 5000 ms), espace deux envois (minIntervalMs, defaut 30000 ms), reporte d'une seconde si un enregistrement est en vol, et transmet la meme carte enrichie que le bouton de brouillon.enabled: falsesuspend sans demonter le rendu.onDraftSaveHandle, nouvelle propriete facultative. Le rendu publie sa commande d'enregistrement —() => 'started' | 'busy'— etnullquand il n'y a rien a enregistrer. Elle s'adresse aux hotes dont la cadence ne depend pas que du temps : bail entre onglets, passage en arriere-plan, retour du reseau, poids du dernier envoi. La condition de publication est celle de l'affichage du bouton de brouillon : la commande existe exactement quand le bouton existe.data-testid="form-draft-button"est conserve. Le retirer serait une rupture pour qui s'y appuie ; il cesse simplement d'etre le chemin officiel.- Impact : additif. Les trois proprietes sont facultatives ; un hote qui n'en declare aucune
obtient un rendu et un comportement identiques. La carte transmise a
onDraftSaveest inchangee, par le bouton comme par les nouveaux chemins. 12 tests ajoutes, 363 passants.
v1.18.0
Un refus de navigation annoncait un nombre de champs a corriger sans jamais dire lesquels. Quand le champ refuse n'etait pas affichable, l'utilisateur n'avait plus rien pour avancer, et le support rien pour diagnostiquer.
- Le motif du refus sort enfin du moteur. Nouvelle propriete facultative
onValidationBlocked(errors)surFormRendereretFormNavigation: elle rend la liste des champs en defaut, chacun avec son code et la regle qui a refuse. En complement, chaque refus est trace en console — page, code de champ, regle, message — parce que l'information existait deja et n'atteignait personne. - Un refus qui ne peut pas etre montre le dit. Le deplacement vers le premier champ fautif rapporte desormais s'il a trouve sa cible. Quand il ne la trouve pas, le message nomme le champ et signale qu'il n'apparait pas sur la page, au lieu d'afficher un compte que rien ne permet de resoudre. Libelle surchargeable
navigation.blockedUnreachable. - Un champ de type
hiddenne retient plus l'enchainement. Il ne pose aucun controle a l'ecran : l'utilisateur n'a aucun moyen de lever le refus. Le depot continue de le valider — c'est la navigation qui ne doit jamais buter sur ce qu'on ne peut pas atteindre. - Republication d'alignement.
@msbci/form-corepasse en 1.16.0 (un conteneur ne peut plus etre rendu obligatoire, un panneau masque emporte ses enfants) ; la dependance etant epinglee a l'exacte version, ce paquet est republie pour rester installable avec elle. - Impact : additif. La propriete et le libelle sont facultatifs ; un hote qui ne les fournit pas retrouve le message et le comportement anterieurs, a la trace console pres. 7 tests ajoutes, 351 passants.
v1.17.0
Sur un formulaire administratif, chaque instance encadree d'un tableau portait un titre reprenant integralement l'intitule du tableau, suivi du numero. Un intitule de deux lignes doublait donc la hauteur de chaque cadre, sans rien apporter quand il n'y avait qu'une instance.
- Deux reglages de schema, honores ici.
roster.hideInstanceTitleretire cette ligne repetee ;roster.showRowNumberpose alors un numero de ligne a gauche de l'instance. Le numero n'a d'effet que si le titre est masque : ensemble ils feraient doublon. - Ou ils s'appliquent. Aux variantes rendues en cadres :
collection, et le rendu pilote parpilotVariableCode. Le tableau etendu numerote deja ses lignes dans une colonne dediee ; une variantecheck/lista options porte le libelle de l'option sur chaque ligne. Ni l'un ni l'autre ne change, et le reglage n'y est pas propose par l'editeur. - Masquer un titre ne prive pas du reperage. Le cadre prend alors
role="group"et unaria-labelportant le nom de l'instance : sans lui, deux instances deviendraient indiscernables a la lecture par synthese vocale. Quand le titre est visible, il joue deja ce role et rien n'est ajoute. - L'impression suit. Le numero est un bloc de texte, non un controle : il figure sur la sortie imprimee comme a l'ecran, et la regle qui empeche une instance d'etre coupee entre deux pages est inchangee.
- Republication d'alignement.
@msbci/form-corepasse en 1.15.0 ; la dependance etant epinglee a l'exacte version, ce paquet est republie pour rester installable avec elle. - Impact : additif, les deux reglages etant absents par defaut. Un tableau qui ne les porte pas est rendu a l'identique, y compris ses instances, son bouton de suppression et ses champs. 7 tests ajoutes, 344 passants.
v1.16.1
- Republication d'alignement.
@msbci/form-corepasse en 1.14.0 (duplication d'un sous-arbre de formulaire) ; la dependance etant epinglee a l'exacte version, ce paquet est republie pour rester installable avec elle. Aucun changement de code de rendu, aucun changement de comportement : 337 tests passants, inchanges.
v1.16.0
Dans un tableau, le contenu des cellules pouvait deriver au milieu de sa colonne sous un intitule reste, lui, aligne a gauche. Ce lot rend l'alignement du rendu explicite plutot qu'herite.
- Cause. Aucun element du rendu ne declarait son alignement horizontal, a l'exception des en-tetes de colonnes du tableau etendu. Le contenu des cellules heritait donc de l'alignement de l'application hote : une seule regle
text-alignposee sur un conteneur parent centrait le numero de ligne, les boutons, les groupes de cases et le texte saisi dans les champs, pendant que les intitules de colonnes, proteges par leur propre declaration, restaient a gauche. Cette asymetrie est ce que l'on voyait. Mesure, sous un hote centre : numero de ligne a 200,2 px pour un intitule a 174 px. - Correctif. La racine du rendu declare
text-align: left. Chaque cellule du tableau etendu — numero de ligne, champ, actions — le declare aussi, ainsi que l'en-tete de la colonne d'actions, que le navigateur centrait par defaut faute de style (regle propre ath). Un champ d'une cellule s'aligne donc sur l'intitule de sa colonne et sur un champ hors tableau, quelle que soit la feuille de style de l'hote. - Les lignes ne sont plus creusees. La cellule du numero de ligne s'aligne en haut comme les cellules de champ, au lieu d'etre centree verticalement et de flotter au milieu d'une ligne haute. Le champ d'une cellule perd la marge basse qui separe deux champs empiles sur une page : elle laissait un vide sous chaque controle. Hauteur de ligne mesuree : 72 px avant, 56 px apres pour une ligne d'un seul niveau ; 283 px avant, 267 px apres pour une ligne melant tous les types de champs.
- Nouvelle propriete
densedeFieldWrapperetVariableRenderer, facultative et fausse par defaut. Elle porte la suppression de cette marge. Un hote qui ne la passe pas obtient la marge d'avant. - Ce qui reste a l'hote. Un composant de saisie injecte par
theme.components.*decide seul de sa largeur et de la disposition de son contenu : le paquet garantit que la cellule qui l'accueille commence a gauche et fait toute la largeur de sa colonne, pas que le composant la remplisse. - Impact : additif (
dense). Les largeurs de colonnes declarees parcolSpanet la disposition fixe qui les accompagne sont inchangees, avec et sans largeurs declarees. L'impression est inchangee : les valeurs imprimees s'alignent sur l'intitule de leur colonne. Un formulaire sans tableau est rendu a l'identique. 10 tests ajoutes, 337 passants.
v1.15.0
Un concepteur qui transcrit un formulaire ne peut pas verifier la quatrieme page sans remplir les trois premieres, ni voir un champ que sa condition masque. Ce lot ajoute un apercu de mise en page, et rend audible un refus de navigation qui ne l'etait pas.
- Nouvelle propriete
designPreviewdeFormRenderer, facultative et fausse par defaut. Un hote qui ne la passe pas obtient exactement le rendu d'avant : memes conditions evaluees, meme navigation, meme validation. Passee atrue, aucune condition d'affichage n'est evaluee — toutes les pages et tous les champs sont rendus, un tableau montre au moins une ligne (sans quoi ses colonnes, qui sont justement ce qu'on vient verifier, n'apparaitraient pas), la navigation ne valide rien et le depot est indisponible. - L'ecart se voit a l'ecran. Un bandeau permanent rappelle que le rendu affiche n'est pas celui d'un utilisateur. Il ne suit pas le theme de l'hote : c'est un repere d'outil, pas un element du formulaire. Libelle remplacable par
labels.designPreview.notice, repere de testform-design-preview-banner. - Les champs calcules continuent de se calculer. Leur expression n'interdit d'atteindre aucune page ; les neutraliser afficherait une case qui n'existe pas dans le formulaire reel. Seules les conditions d'affichage sont suspendues.
- Un refus de « Suivant » dit desormais pourquoi. Le bouton refusait la page sans un mot des lors que l'hote n'avait pas active la fenetre de validation : le champ fautif portait bien son message, mais hors de l'ecran il etait introuvable et le bouton passait pour casse. Le refus annonce maintenant le nombre de champs a corriger a cote du bouton, et amene le premier d'entre eux a l'ecran avec le focus. Libelle remplacable par
labels.navigation.blocked(nombre), repere de testform-navigation-blocked. - Impact : additif (
designPreview,labels.navigation,labels.designPreview, typesFormNavigationLabelsetDesignPreviewLabelsexportes). Le seul changement visible sans rien demander est le message de refus, qui remplace un silence. 10 tests ajoutes, 327 passants.
v1.14.0
Le rendu sait desormais lire une piece deposee dans un stockage, et y deposer quand l'hote le lui permet. Par defaut, rien ne change : sans branchement, une piece est encodee en base64 dans la valeur et relue ainsi, exactement comme avant.
- Deux points d'entree, tous deux optionnels. Nouvelle propriete
attachmentsdeFormRenderer(typeIAttachmentSupport, exporte) :upload(file, scope)fait de la valeur une referenceIStoredFileau lieu d'une data URL,resolveUrl(ref, file)rend l'URL par laquelle une piece deposee s'affiche,maxBytesimpose la borne de taille de l'exploitation,scopeajoute au contexte de depot ce que l'hote seul connait. Le rendu ne parle jamais a un stockage : il appelle ces fonctions, que l'hote cable sur ses propres routes — aucun secret de stockage ne descend dans le navigateur. - Le nom d'origine survit enfin. Une data URL ne portait aucun nom : en mode fichier unique, le champ affichait litteralement « Fichier televerse », et le recapitulatif comme l'impression n'avaient rien de mieux a dire. Une piece deposee restitue son nom partout — champ, liste du mode multiple, recapitulatif avant depot, doublon d'impression.
- Tous les lecteurs d'une valeur de fichier sont devenus bivalents, pas un echantillon :
FileField(nom affiche, apercu d'image, galerie, boite lumineuse, liste du mode multiple, mesure de taille, chemintheme.components.FileUpload),PhotoCaptureField,formatResponseValue— donc le recapitulatif et l'impression, qui en dependent — et la mesure de taille du coeur. Une valeur en data URL, une reference, et une soumission qui porte les deux a la fois : les trois se lisent. - Un apercu ne s'invente pas a partir d'une reference. Une reference est opaque : sans
resolveUrl, la piece reste nommee et mesuree mais n'est pas previsualisee, plutot que d'afficher une image cassee. La resolution n'est demandee qu'une fois par reference et son resultat conserve, sans quoi chaque rendu la relancerait et ferait clignoter l'apercu. Nouveaux exportsuseAttachmentUrlsetattachmentDisplayUrl, pour qu'un hote qui remplace un composant de depot resolve comme le rendu par defaut. - Un depot qui echoue se voit. Sans message, le fichier selectionne disparaissait sans raison apparente. L'echec est annonce a cote de la zone de depot et journalise ; la valeur reste vide. Nouveaux libelles optionnels
labels.file.uploadFailedetlabels.image.uploadFailed. - Le plafond de taille peut venir de l'hote.
attachments.maxBytesporte la borne de l'exploitation ; unmaxBytesdeclare dans le formulaire ne peut que la resserrer, jamais l'elargir. La borne vaut au depot et a la validation — sans quoi un fichier accepte a la selection serait refuse a la soumission. SansmaxBytes, le defaut de 5 Mio s'applique comme avant. - Impact : additif sur l'API (
attachments, deux libelles optionnels, deux exports, les typesIAttachmentSupport,IStoredFileetIAttachmentScope). Un seul changement de rendu, correctif : en mode fichier unique, une valeur qui declare un nom l'affiche au lieu de « Fichier televerse » — une data URL nue, qui n'en declare aucun, est rendue mot pour mot comme avant. 22 tests ajoutes, 317 passants.
v1.13.1
- Republication de suivi. Aucun changement de code ni de comportement : le paquet est republie pour dependre de
@msbci/form-corev1.12.0, qui ajoute la propriete facultativeIFormDefinition.updatedAt. Le rendu n'en fait aucun usage. 295 tests passants, inchanges.
v1.13.0
Second passage en navigateur reel sur la meme notice administrative. Le lot precedent avait corrige ce que la construction du formulaire revelait ; celui-ci corrige ce que sa relecture avant depot revele — plus deux finitions d'impression et d'accessibilite.
- Le recapitulatif ignorait les tableaux.
FormSummaryne parcourait que les champs de premier niveau : aucune ligne de tableau n'y figurait, et une page dont tout le contenu est un tableau disparaissait de la relecture. Le declarant validait un dossier dont il ne voyait pas la moitie. Le recapitulatif parcourt desormais la page telle qu'elle est saisie — champs, panneaux, tableaux — et rend chaque tableau sous forme de tableau, une ligne par occurrence, avec ses en-tetes de colonnes. Nouveaux reperes de test :summary-roster-{CODE},summary-roster-row-{CODE}-{N},summary-cell-{COLONNE}-{N}. - Un panneau y apparaissait comme une ligne vide. Le panneau est un regroupement, il ne porte aucune valeur : il produisait une ligne « Exploitant — » que le lecteur prenait pour une reponse manquante. Un panneau est desormais un intitule de rubrique (
summary-panel-{CODE}) suivi de ses champs ; un panneau sans champ visible n'apparait plus du tout. Meme traitement pour les blocs de mise en page (label,richtext), qui n'ont pas de valeur a relire. - Le recapitulatif affichait la valeur interne d'une option. Un choix ferme s'y lisait
OUIla ou l'impression affichaitOui: le recapitulatif appelaitformatResponseValueau lieu deformatVariableValue, qui seul remplace une valeur d'option par son libelle. Les deux vues du meme dossier disent maintenant la meme chose. - Un champ calcule etait exclu de la relecture. Un total calcule est une donnee du dossier, affichee a la saisie et imprimee : une page qui n'en portait qu'un disparaissait entierement du recapitulatif. Les champs calcules y figurent desormais ; seuls les champs masques restent exclus.
- La vue « tout le formulaire » n'offrait aucun moyen de deposer. La bascule etant proposee en mode saisie, l'utilisateur pouvait tout remplir et se retrouver sans bouton de depot, sans autre issue que de revenir a la vue page par page. La vue complete porte maintenant le depot et l'enregistrement de brouillon, sans les boutons de page qui n'y ont pas d'objet — le controle reste celui de toutes les pages visibles, comme pour un depot ordinaire. Nouvelle propriete optionnelle
hidePageNavigationsurFormNavigation, pour un hote qui compose sa propre mise en page. - Les erreurs etaient effacees a chaque changement de page. Le sommaire designait une page en defaut ; en la rejoignant, les pastilles disparaissaient et la page atteinte ne signalait plus rien. Suivre une erreur menait donc a un ecran muet. Les erreurs sont desormais conservees d'une page a l'autre : chacune porte le code de sa page, la revalidation d'une page ne remplace que les siennes, et une erreur disparait quand son champ est corrige ou quand sa page est revalidee sans defaut.
- A l'impression, un choix ferme imprimait la liste des options et la valeur retenue (« Oui / Non / Oui »). La feuille d'impression masquait les
input, pas les libelles d'options qui les accompagnent. Les listes de boutons radio et de cases a cocher portent desormais la classe d'interface : seul le doublon textuel du champ, qui porte deja la valeur retenue, subsiste sur le papier. - Les
input type="radio"ettype="checkbox"d'une liste d'options portent leurvalue. L'etat etait porte par React seul : sans consequence fonctionnelle, mais un outil d'accessibilite, un test automatise ou un remplissage assiste n'avait aucun moyen de lire l'option designee par un bouton. - Le nom d'un fichier joint s'affiche lorsqu'il est disponible.
dataUrlFileName(exporte) lit un parametre;name=porte par une data URL, etformatResponseValuereconnait deja un objet{ name }. Rien dans ces paquets n'ecrit ce nom : la valeur d'une piece jointe reste la data URL nue, telle que tout hote existant la lit et la stocke. Un hote qui declare lui-meme le nom le voit desormais au recapitulatif, a l'impression et sur la vignette de depot.
Impact : additif sur l'API (hidePageNavigation, labels.summary.rowLabel, export dataUrlFileName). Trois changements de rendu, tous correctifs : le contenu du recapitulatif, la persistance des erreurs entre les pages, et le retrait des listes d'options du document imprime. 14 tests ajoutes, 295 passants — FormSummary n'en avait qu'un.
v1.12.0
Cinq defauts trouves a l'usage dans un navigateur reel, en construisant puis en remplissant une notice administrative complete. Aucun n'etait visible sous jsdom.
- Un champ place sous un tableau remontait au-dessus de lui. Le rendu affichait tous les champs d'une page, puis tous ses tableaux. Un total calcule pose sous le tableau qui l'alimente apparaissait donc avant lui, a la saisie comme a l'impression : le formulaire livre ne ressemblait pas a celui qu'on avait dessine. Le contenu d'une page suit desormais l'ordre voulu, en alternant grilles de champs et tableaux. Une page servie a l'ancienne forme (
variables[]etrosters[]separes) est recomposee par les rangs quand ils forment une seule suite ; s'ils se recouvrent — deux suites independantes, cas d'un schema ancien — la page garde sa presentation anterieure. - Une signature tracee avant la saisie du nom etait perdue sans un mot. La valeur n'etant enregistree qu'une fois l'engagement complet, le trait pose en premier etait rejete au relachement du pointeur. Le signataire voyait pourtant son trait a l'ecran ; il saisissait son nom ensuite, et le champ restait « non signe », sans explication ni moyen de s'en sortir autrement qu'en recommencant dans le bon ordre. Le trace est maintenant conserve jusqu'a ce que l'engagement soit complet, quel que soit l'ordre des gestes.
- Le recapitulatif n'affichait que la page ou l'on se trouvait.
FormSummaryevaluait la visibilite des champs sans nommer leur page : la page de navigation courante faisait foi, et les reponses de toutes les autres disparaissaient. Le dossier se relisait presque vide juste avant d'etre depose. - Une piece jointe imprimait son contenu encode. La valeur d'un champ fichier est une data URL : le recapitulatif et le doublon d'impression en rendaient les milliers de caracteres de base64 — plusieurs feuilles pour un seul document joint. Seuls la nature et le poids sont desormais rendus. La zone « glissez vos fichiers ici » porte enfin la classe d'interface et disparait a l'impression, comme les autres aides de saisie.
- Une ligne de tableau en disposition compacte pouvait etre coupee entre deux feuilles. Les regles d'impression ne protegeaient que les
<tr>d'un tableau etendu. Nouvelle classePRINT_ROW_CLASS(msbci-form-row), exportee, posee sur chaque ligne rendue en bloc.
Impact : additif sur l'API (une constante de classe exportee en plus). Trois changements de rendu, tous correctifs : l'ordre des elements d'une page, la description d'une piece jointe en toutes lettres, la zone de depot retiree du papier. 7 tests ajoutes, 281 passants.
v1.11.0
- Un fichier hors plafond etait encode et stocke sans un mot.
FileFieldne mesurait la taille que pour l'afficher a cote du nom du fichier : rien ne refusait le depot. Le champ refuse desormais chaque piece avant encodage, sur la taille du fichier d'origine. Encoder d'abord reviendrait a charger en memoire ce qu'on s'apprete a rejeter, et a le faire grossir d'un tiers au passage. Le plafond est celui defileConfig.maxBytes/imageConfig.maxBytes, ou le plafond par defaut de@msbci/form-corequand le formulaire n'en declare aucun. - Le refus est visible et nomme la limite. Un encadre
data-testid="file-field-{CODE}-too-large", portantrole="alert", liste les pieces refusees avec leur nom et la limite appliquee, en francais ou en anglais selon la langue de saisie. Sans lui, rien n'expliquerait pourquoi le fichier selectionne n'apparait pas dans la liste. En depot multiple, les pieces sous le plafond sont conservees et seules les autres sont refusees. - Un composant de depot injecte n'echappe pas au plafond. Quand l'hote fournit
theme.components.FileUpload, la valeur qu'il produit est mesuree a son tour avant d'etre ecrite : le plafond ne depend pas du composant de saisie. IFileFieldLabels.tooLarge/IImageFieldLabels.tooLarge(optionnels) recoivent le nom du fichier et la limite deja mise en forme, pour que l'hote formule le refus a sa maniere sans avoir a recalculer le plafond.- Impact : additif sur l'API (deux libelles optionnels de plus). Un seul changement de comportement, assume et voulu : une piece au-dela du plafond par defaut est refusee sur un formulaire qui n'en declarait aucun. Le rendu d'un formulaire existant est par ailleurs inchange. 5 tests ajoutes, 274 passants.
v1.10.0
- Le type
'signature'se saisit, se relit et s'imprime. NouveauSignatureField, inscrit au registre des champs. En modetyped(defaut), le signataire ecrit son nom et coche l'engagement ; la valeur n'est enregistree qu'une fois les deux poses, avec son horodatage. En modedrawn, une zone de trace a la souris ou au doigt (pointer events, donc tactile compris) s'y ajoute ; une fois le trait pose, c'est l'image enregistree qui est affichee, et non lecanvas— uncanvasrepart vide a chaque rendu, la ou l'image survit a un changement de page comme a la reprise d'un brouillon. - La signature figure sur l'exemplaire imprime. Le doublon textuel pose sous chaque champ rend « nom — horodatage », jamais la data URL :
formatResponseValuereconnait desormais la valeur de signature. La trace manuscrite, elle, est une<img>que la feuille d'impression ne masque pas ; la zone de saisie et la case d'acceptation portent la classe d'interface et disparaissent. - Le champ ne duplique pas le message d'erreur deja porte par l'enveloppe de champ, et son libelle est associe a la zone de nom (
signaturerejoint les types etiquetables parhtmlFor). - Ce que le champ etablit : le nom declare, la date et l'heure de la saisie, le texte d'engagement en vigueur. Ce n'est pas une signature electronique qualifiee — rien n'y certifie l'identite du signataire ni ne scelle le document.
- Composable par l'hote.
SignatureFieldest exporte, ainsi que le type de libellesISignatureFieldLabels(labels.signaturedeFormRenderer, relaye parfieldLabels.signature). Un hote qui injecte son propre composant de saisie compose la meme valeur avecbuildSignatureValuede@msbci/form-core. - Impact : strictement additif — un composant de plus au registre, un bloc de libelles optionnel. Un formulaire sans champ signature est rendu a l'identique. 11 tests ajoutes, 269 passants.
v1.9.0
- Aucune preparation a l'impression. Pas une seule regle
@media print, et toute la mise en forme en styles en ligne qu'aucune feuille d'impression ne peut surcharger. Un formulaire rempli est pourtant une piece a produire : l'agent instructeur, le signataire et le declarant doivent pouvoir l'imprimer. Nouvelle optionenablePrint: elle injecte une feuille@media printdont chaque!importantest la condition pour passer devant les styles en ligne — c'est la seule declaration que la cascade CSS place au-dessus d'eux. La feuille retire les elements d'interface, neutralise les couleurs de fond, empeche la coupure d'un champ ou d'une ligne, et repete l'en-tete d'un tableau qui se poursuit sur la feuille suivante. - Une valeur saisie ne s'imprimait pas lisiblement. Un champ de saisie imprime son cadre et tronque son contenu, et aucune regle CSS ne fait grandir un
<input>. SousenablePrint, chaque champ rend donc un doublon textuel de sa valeur (span.msbci-form-print-value), masque a l'ecran et revele a l'impression, ou la feuille masque le controle. Le doublon est pose parFieldWrapper: il vaut aussi pour un composant de saisie injecte par l'hote (theme.components.*) ou enregistre parregisterFieldComponent.buildPrintStylesheet, les cinq constantes de classe,formatResponseValueetformatVariableValuesont exportes pour un hote qui remplaceFieldWrapperlui-meme. - Aucun mode « tout le formulaire ». Le rendu n'affichait qu'une page a la fois : rien a imprimer d'un seul tenant, et aucune vue d'ensemble. Nouvelle prop
view('paged'par defaut,'full'pour rendre toutes les pages visibles a la suite, sans navigation), aveconViewChangeet une bascule integree optionnelle (enableViewToggle). Une page masquee par une condition n'y figure pas, et elle y apparait des que sa condition devient vraie. - La visibilite d'un champ se lit maintenant par la page qui le rend. Tant qu'une seule page etait affichee, la page courante de la navigation suffisait ; plusieurs pages rendues a la suite devaient chacune declarer la sienne.
isVariableVisibleaccepte un troisieme argument optionnelpageCode, etFormGrouppublie le code de sa page par un contexte (PageScopeProvider/usePageScope, exportes). Omis, le comportement est celui d'avant. - Aucune vue d'ensemble du parcours. Sur un formulaire de six rubriques, on ne savait ni ou l'on en etait ni ce qu'il restait. Nouveau
FormToc(enableToc) : il liste les pages visibles, en atteint une directement et signale celles qui portent une erreur. Il s'appuie sur la validation du formulaire complet introduite en v1.7.0 — les erreurs du contexte portent deja le code de leur page — et n'en cree pas un second mecanisme.IFormContextValuegagne un membre optionnelvisiblePages; un hote qui fournit son propre contexte sans l'implementer retombe surform.pages. - Impact : strictement additif et opt-in. Toutes les nouvelles props sont desactivees par defaut, aucune signature existante n'est modifiee et le recapitulatif reprend la mise en forme des valeurs telle quelle, desormais partagee avec l'impression. Un hote qui ne demande ni impression ni sommaire obtient le rendu anterieur. 11 tests ajoutes, 258 passants.
v1.8.0
- Les largeurs de colonnes etaient ignorees dans le tableau. La mise en page sur 12 colonnes (
colSpan) etait honoree partout — page, panneau, lignes de tableau en presentation compacte — sauf dans la presentation etendue (collection_extend), ou toutes les colonnes recevaient la meme largeur. Un tableau melant un libelle long, une quantite et une unite courte y devenait illisible. Chaque en-tete porte desormais la largeur derivee de soncolSpan, par la formule deja utilisee partout ailleurs (colSpan / 12), et la disposition fixe n'est imposee que si au moins une largeur est declaree — sans quoi le navigateur ignorerait une colonne plus etroite que son contenu. Un tableau sans aucuncolSpanest rendu a l'identique. - Aucun champ de saisie ne portait d'identifiant. Le libelle designait, par
htmlFor, un identifiant que personne ne portait : un lecteur d'ecran n'annoncait pas le libelle, et aucun outillage de test ou d'assistance ne pouvait viser un champ. L'identifiant ne pouvait pas etre le code seul — dans un tableau de N lignes il aurait ete duplique N fois, ce qui est invalide en HTML et rompt justement l'association qu'il porte. Il integre donc le numero de ligne, sous la formeCODE_Ndeja utilisee par les clefs de reponses, et reste stable d'un rendu a l'autre. Le libelle s'y rattache parhtmlForquand le controle est nativement etiquetable, pararia-labelledbypour les composites (liste deroulante, groupe de boutons radio). Le message d'erreur porte le meme identifiant suffixe et est designe pararia-describedby.fieldElementId,fieldLabelId,fieldErrorIdetisLabelableTypesont exportes pour un hote qui remplace un composant de saisie. - Les boutons radio d'une colonne de tableau partageaient le meme groupe HTML. L'attribut
namevalait le code de la variable, identique sur toutes les lignes : cocher « Oui » ligne 2 decochait la ligne 1. Le groupe integre desormais le numero de ligne. Le champ « oui/non par option » n'etait pas concerne, son groupe portait deja le numero de ligne. - Les regles de validation n'etaient pas posees sur les champs. Elles etaient evaluees en JavaScript sans qu'aucun attribut correspondant n'atteigne le controle : bornes numeriques et de date, longueur, motif, mode de saisie. Elles le sont maintenant, avec une exception assumee : le caractere obligatoire est expose par
aria-requiredet jamais par l'attributrequired. C'est le seul attribut qui rend invalide un champ vide, donc l'etat normal de toute page pas encore atteinte : pose sur le controle, il donnerait au navigateur de quoi refuser une soumission avec ses propres bulles, dans sa langue, en doublon des messages de l'application et sans connaitre sa validation par pages. Les autres attributs ne contraignent qu'une valeur effectivement saisie et hors bornes — un cas que l'application refuse deja — et apportent en echange une aide reelle a la saisie. Le message de la regle est repris danstitle, de sorte qu'un signalement du navigateur affiche le texte de l'application. - Impact : additif.
FieldWrapperaccepte uninstanceNumberoptionnel, les champs gagnent des attributs, aucun n'en perd. Un formulaire sans largeur de colonne, sans regle de validation et hors tableau est rendu exactement comme avant. 17 tests ajoutes, 247 passants.
v1.7.0
- Le champ calcule restait vide. Le rendu affichait le type
calculateden lecture seule avec une pastille de formule, mais rien ne l'evaluait : aucun total de colonne, aucun sous-total, aucun report de montant.useFormEngineresout desormais les champs calcules parCalculationEngine(@msbci/form-core1.8.0+) a l'initialisation — un brouillon rouvert affiche ses totaux sans attendre une saisie — et a chaque changement d'une dependance. La valeur est rangee dans les reponses (metadata.source: 'auto', comme toute valeur produite par le moteur) et part donc avec la soumission. Un champ calcule n'est pas saisissable : il l'etait deja, en lecture seule et desactive, et le reste. - La soumission ne validait que la derniere page.
handleSubmitn'appelait que la validation de la page courante : un formulaire de six pages partait en n'ayant controle que la sixieme, alors que le serveur valide toutes les pages visibles (@msbci/form-server1.5.0+). L'investisseur decouvrait au depot un refus portant sur une page qu'il croyait finie. La soumission passe maintenant parvalidateAllPages, qui delegue au parcours unique du coeur — le meme que celui du serveur. - L'erreur nomme sa page.
ValidationErrorsModalacceptepageLabels(optionnel, code de variable vers libelle de page) et affiche le nom de la page sous chaque erreur ; cliquer une erreur ramene sur la page concernee, la ou le clic ne faisait que fermer la fenetre. Le passage d'une page a l'autre reste libre : c'est la soumission qui controle, pas la navigation. - Le navigateur n'est plus plus severe que le serveur.
validateCurrentPageecarte les erreurs portant sur un champ masque par une condition : elles bloquaient la navigation sur un champ que le serveur, lui, accepte. - Nouveau membre optionnel de
IFormContextValue—validateAllPages. Optionnel : un hote qui fournit son propre contexte de formulaire n'a rien a implementer, la soumission retombe alors sur la validation de la page courante. - Impact : aucune prop de
FormRenderermodifiee, aucune signature existante changee. Un formulaire sans champ calcule est rendu a l'identique — le hook rend l'objet de reponses recu tel quel, sans nouvelle clef ni nouvelle reference. 11 tests ajoutes, 230 passants.
v1.6.0
- Les champs d'un panneau sont rendus a l'interieur de l'encadre. Jusqu'ici
PanelFieldn'affichait qu'un titre et une description : un panneau porteur de champs (IFormVariable.children,@msbci/form-core1.7.0+) les rend maintenant dans son cadre, par la memeLayoutGridque la page — la mise en page sur 12 colonnes (colSpan,startWithNewLine) y est donc honoree a l'identique. Un panneau masque par une condition emporte ses champs avec lui, et la condition d'un champ regroupe s'applique comme ailleurs. - Les champs regroupes comptent partout ou les champs comptent. Le recapitulatif, la recherche d'une variable par son code et le decalage des valeurs a la suppression d'une instance de page repetable parcourent desormais tous les champs de la page, panneaux aplatis.
- Impact : strictement additif. Un panneau sans enfants rend exactement ce qu'il rendait, et un formulaire enregistre avant ce lot est affiche a l'identique. 4 tests ajoutes, 219 passants.
v1.5.0
- Les conditions de tableau n'etaient evaluees nulle part — ni par le moteur, ni par le rendu. Rendre ces conditions editables sans les evaluer aurait ete pire que rien : un concepteur aurait cru avoir conditionne son tableau.
IFormContextValuegagne unisRosterVisible?(rosterCode, instanceNumber?)optionnel, implemente paruseFormEngineau-dessus deFormTree.isRosterVisible;FormGroupn'affiche plus que les tableaux visibles, sur une page simple comme sur une page repetable. - Validation : un tableau masque par une condition ne valide plus aucune ligne. Sans cela, ses colonnes obligatoires auraient bloque une page ou le tableau n'apparait pas.
- Compatibilite :
isRosterVisibleest optionnel. Un hote qui fournit son propre contexte de formulaire sans l'implementer conserve le comportement anterieur — tous les tableaux affiches. - Impact : aucune signature existante modifiee. 8 tests ajoutes, 215 passants.
v1.4.0
- Les lignes de tableau n'etaient jamais validees. Cause :
useFormEngine.validateCurrentPage, unique appelant deValidationEngine.validatePage, ne transmettait ni numero de ligne ni nombre de lignes. Les valeurs de tableau, stockees sous des clefs suffixees, etaient lues comme absentes : un tableau a colonnes obligatoires passait la soumission entierement vide. Correctif : le hook resout le nombre de lignes de chaque tableau de la page et le transmet, avec l'etat « obligatoire » resolu par les conditionsrequire. - Le nombre de lignes d'un tableau n'etait pas conserve. Cause :
CollectionRostertenait son compteur dans un etat local initialise au minimum, jamais ecrit dans les reponses. Changer de page et revenir remettait le tableau au minimum ; rouvrir un brouillon masquait les lignes saisies, qui restaient pourtant dans la soumission — invisibles, non modifiables, et transmises. Correctif : le compteur est persiste dans les reponses sous la clef reserveeCODE__rowCount. Une soumission anterieure sans compteur reste lisible : le nombre de lignes est deduit des valeurs presentes. - Le bouton de suppression retirait toujours la derniere ligne. Cause :
handleRemovedecrementait un compteur sans recevoir le numero de ligne, et etait cable a l'identique sur chaque ligne. Supprimer la ligne 2 sur 5 retirait visuellement la ligne 5 ; les valeurs de la ligne 2 restaient, celles de la ligne 5 devenaient orphelines. Meme defaut sur les pages repetables, ouhandleRemove(num)recevait le numero sans l'utiliser. Correctif : la suppression retire la ligne designee et remonte les suivantes d'un rang, valeurs comprises. - Nouveaux membres optionnels de
IFormContextValue—isVariableRequired,isVariableReadOnly,getRosterRowCount,addRosterRow,removeRosterRow,removeInstanceValues. Tous optionnels : un hote qui construit son propre contexte de formulaire n'a rien a implementer, les composants retombent sur le comportement anterieur (etat local). - Impact : aucune prop de
FormRenderermodifiee, aucune signature existante changee. Le test qui verifiait que la suppression retirait « la derniere » ligne decrivait le defaut : il verifie desormais que la ligne designee est bien celle qui part. 11 tests ajoutes, 207 passants.
v1.3.4
- Correctif — libelles dupliques dans le tableau
collection_extend. Cause : chaque cellule du tableau appelaitVariableRenderer, qui delegue aFieldWrapperle rendu du libelle ; l'intitule etait donc affiche une fois en en-tete de colonne (<th>) et une fois au-dessus du champ dans chaque ligne. Sur un tableau de six colonnes et dix lignes, soixante libelles redondants rendaient le formulaire illisible. - Correctif : nouvelle prop optionnelle
hideLabelsurFieldWrapperetVariableRenderer. Lorsqu'elle est active, le libelle n'est pas supprime mais rendu visuellement masque (techniqueclip-path: inset(50%)), donc toujours present dans l'arbre d'accessibilite.RosterRendererla positionne uniquement pour la mise en pagecollection_extend, ou l'en-tete de colonne porte deja l'intitule. - Accessibilite : chaque
<th>recoit unidstable (roster-<CODE>-col-<VARIABLE>), chaque<td>un attributheaderscorrespondant, et le contenu de la cellule est enveloppe dans unrole="group"avecaria-labelledbypointant sur cet en-tete. Le champ reste donc nommable par les technologies d'assistance, en navigation tableau comme en navigation formulaire. - Impact : aucun changement pour les autres types de roster (
check,list,collection) ni pour les champs hors tableau —hideLabelest optionnelle et vautfalsepar defaut. Aucune signature existante modifiee. - Nouveaux tests de non-regression (
rosterExtendLabels.test.tsx+ tests ajoutes arosterCollection.test.tsx) : un tableau de 6 colonnes x 10 lignes n'expose plus qu'un seul libelle visible par colonne, et 10 libelles accessibles masques. 196 tests passants.
v1.3.3
- Republication corrigeant le protocole
workspace:*dans le tarball publié (les dépendances internes@msbci/*étaient résolues enworkspace:*, ce qui cassait l'installation autonome). Publié viapnpm publishqui remplace correctement ces références. Aucun changement de code applicatif.
v1.3.2
FileFieldmulti-upload — un seul composant pourtype: 'file'ettype: 'image'. Mode single intact (maxFiles === 1ou absent → valeurstringlegacy), mode multi (maxFiles > 1) → valeurstring[]. Upgrade transparent : une valeurstringhéritée en mode multi est affichée comme[value].- UX multi-fichiers : drop-zone native (drag & drop sans dépendance externe), liste cliquable des fichiers avec nom + taille estimée + bouton suppression, masquage automatique de la drop-zone à
maxFilesatteint. - Galerie image : grille de miniatures (
auto-fill, minmax(120px, 1fr)), lightbox au clic (overlay sombre +<img>centré, fermeture par × ou Escape), bouton suppression sur chaque miniature. - Camera capture : si
imageConfig.allowCamera, un bouton dédié « 📷 Prendre une photo » déclenche un input séparé aveccapture="environment"pour l'accès direct à la caméra mobile. - Encodage parallèle :
Promise.all(files.map(fileToBase64))— sélectionner 10 fichiers ne séquentialise pas les FileReader. - Override d'i18n : nouvelles props
labels.file/labels.imagesurFormRendererProps, propagées via leFormContext(fieldLabels). Défauts FR + EN conservés. - Nouveaux types publics :
IFieldLabels,IFileFieldLabels,IImageFieldLabels. - 14 nouveaux tests RTL (
fileFieldMulti.test.tsx) — 189 tests passants.
v1.3.0
- Compatible
IFormPage.items[](variables et rosters unifiés) FormGroup/FormSummarylisent désormais viagetPageVariables/getPageRosters— tolérant aux schémas legacy- Aucun changement cassant — l'API publique de
FormRendererreste identique - Bundle inchangé, 169 tests toujours verts
v1.2.0
- Prop
lang+onLangChangesurFormRenderer LangProvider,useLang(),LangSelector(exportés publiquement)- Rendu multilingue sur tous les composants (champs, options, pages, rosters)
- Stratégie de détection :
prop/browser/selector/auto - Sélecteur de langue intégré (
strategy='selector') - Defaults UI en français (Soumettre, Suivant, Précédent, ...)
- 12 nouveaux tests i18n (169 total)
What's new in v1.1.0
- 4 new field components added to the registry:
PanelField(display-only bordered section),RichTextField(interpolated HTML with regex-based sanitization),ListRadioField(Yes/No radios per option) andPhotoCaptureField(mobile rear-camera via nativecapture="environment"). RosterRenderernow supports two dynamic row layouts:rosterType: 'collection'(compact card stack) and'collection_extend'(extended table with column headers), both with add/remove based onIFormRoster.collectionConfig.- Responses passed to
onSubmitandonDraftSaveare automatically enriched with display labels, page/roster context and interpolated variable labels (via the new@msbci/form-core/enrichResponsesWithContext). FieldWrappernow bypasses label/error chrome for display-only types (panel,richtext).FieldPropstype is now exported publicly.- Built-in type count: 17 → 21 with the 4 additions above. The Field Registry section was expanded with a complete custom-component example.
License
Copyright (c) 2026 MOSOBI — All rights reserved. Commercial license required. Contact: [email protected]
