@msbci/form-renderer
v1.3.2
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" />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.
Exported Components
FormRenderer · FormGroup · RosterRenderer · VariableRenderer · FieldWrapper · TextField · NumberField · DateField · CheckboxField · RadioField · SelectField · FormProgress · FormNavigation · FormSummary · ValidationErrorsModal
All components are individually importable for advanced layouts.
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]
