forme-engine
v1.0.6
Published
React form generation library — conditional display, multi-step pagination, OTP, file upload, repeat groups
Downloads
142
Maintainers
Readme
forme-engine
A powerful, JSON-driven React form engine — conditional logic, multi-step pagination, OTP, file uploads, repeat groups, multilingual labels, and more. Build complex forms without writing form code.
Why forme-engine?
Most React form libraries give you primitives — you still wire validation, conditionals, multi-step logic, file uploads, and OTP flows yourself. forme-engine is the only library in the React ecosystem that combines all of the following out of the box, with zero boilerplate:
| Feature | react-hook-form | Formik | RJSF | form-engine | |---|:---:|:---:|:---:|:---:| | JSON-driven rendering | ❌ | ❌ | ✅ | ✅ | | Conditional display (AND/OR/computed) | ❌ | ❌ | ⚠️ | ✅ | | Multi-step + progress bar | ❌ | ❌ | ❌ | ✅ | | Repeat groups (dynamic arrays) | ❌ | ❌ | ✅ | ✅ | | OTP flow (send + verify + resend) | ❌ | ❌ | ❌ | ✅ | | File-to-bucket upload/delete | ❌ | ❌ | ❌ | ✅ | | Cascading dropdowns | ❌ | ❌ | ❌ | ✅ | | Multilingual labels | ❌ | ❌ | ⚠️ | ✅ | | Radix UI + Tailwind (shadcn-compatible) | ❌ | ❌ | ❌ | ✅ | | Zod validation (auto-generated) | ❌ | ❌ | ❌ | ✅ | | Draft persistence (Zustand) | ❌ | ❌ | ❌ | ✅ |
The sweet spot: if your stack is React + Zod + Radix UI + Tailwind (the modern default), forme-engine plugs in natively and eliminates weeks of form infrastructure work.
Overview
forme-engine lets you describe a form as a plain JSON object and render it as a fully functional, validated, multi-step React form. No boilerplate. No manual useState for every field. You define the structure — the engine handles the rest.
Key capabilities:
- All standard field types — text, textarea, dropdown, radio, checkbox, date, number, boolean, password, file
- Rich media fields — image, video, audio, document, PDF, voice recording
- Rich text editor — TipTap integration
- OTP verification — built-in phone/email OTP flow with resend logic
- File-to-bucket uploads — upload and delete files via a configurable API
- Conditional display — show/hide sections and fields based on other field values
- Repeat groups — dynamic lists of fields (e.g. list of passengers, children, etc.)
- Multi-step pagination — by section or by field count, with progress bar
- Multilingual labels — every text supports
{ fr: "...", en: "..." }objects - Zod validation — automatic schema generation from field definitions
- Form persistence — draft saving via Zustand
- Theming — primary color + per-container style overrides
Installation
npm install forme-enginePeer dependencies
The following packages must be installed in your project:
npm install react react-dom react-hook-form zod zustand lucide-react react-hot-toast \
@tiptap/react @tiptap/starter-kit @tiptap/extension-image \
@tiptap/extension-link @tiptap/extension-placeholderQuick start
import { FormEngine } from "forme-engine";
import type { FormStructure } from "forme-engine";
const form: FormStructure = {
name: "Contact",
provider: "Acme Corp",
description: "Simple contact form",
status: "ACTIVE",
isDeclaration: 0,
isTest: 0,
theme: { primaryColor: "#008080" },
layoutOptions: {
paginationMode: "bySection",
progressBarType: { type: "linear", visible: true },
},
sections: [
{
sectionId: "section-contact",
title: { fr: "Vos informations", en: "Your information" },
formFields: [
{
formFieldId: "f-name",
fieldName: "full_name",
fieldType: "text",
label: { fr: "Nom complet", en: "Full name" },
response: { responseValue: "" },
width: "half",
validations: [
{ validationType: "required", errMsg: "Le nom est requis" },
],
},
{
formFieldId: "f-email",
fieldName: "email",
fieldType: "text",
label: "Email",
response: { responseValue: "" },
width: "half",
validations: [
{ validationType: "required", errMsg: "L'email est requis" },
{ validationType: "email", errMsg: "Email invalide" },
],
},
],
},
],
};
export default function App() {
return (
<FormEngine
form={form}
formId="contact-form"
onSubmit={(data) => console.log(data)}
currentLang="fr"
submitButtonText="Envoyer"
/>
);
}<FormEngine> props
| Prop | Type | Required | Default | Description |
|------|------|:--------:|---------|-------------|
| form | FormStructure | ✅ | — | The complete form definition object |
| formId | string | ✅ | — | Unique instance ID — used for draft persistence |
| onSubmit | (data: Record<string, any>) => void | ✅ | — | Called when the form is valid and submitted |
| currentLang | string | | "fr" | Active language code for multilingual labels |
| submitButtonText | string | | "Continuer" | Label of the submit / next button |
| paginationMode | "byFields" \| "bySection" | | "byFields" | Overrides form.layoutOptions.paginationMode |
Form structure reference
FormStructure
The root object passed to the form prop.
{
name: string;
provider: string;
description: string;
status: "ACTIVE" | "INACTIVE";
isDeclaration: number; // 1 = official declaration, 0 = standard
isTest: number; // 1 = test, 0 = production
version?: string;
createdAt?: string; // ISO date string
updatedAt?: string; // ISO date string
sections: Section[];
theme?: FormTheme;
layoutOptions?: FormEngineLayoutOptions;
}FormTheme
{
primaryColor?: string; // Hex color — e.g. "#008080"
}FormEngineLayoutOptions
{
paginationMode?: "byFields" | "bySection";
progressBarType?: ProgressBarType;
headerOptions?: HeaderOptions;
displayDeleteButton?: boolean; // Show "clear all" button — default: true
containerStyles?: ContainerStylesOptions;
}ProgressBarType
| type value | Description |
|---|---|
| "default_step" | Classic numbered steps |
| "linear" | Horizontal progress bar |
| "pastel" | Pastel-toned step bar |
| "custom" | Custom colors via colors |
| "section_bubble" | Bubble indicators per section |
| "section_bubble_pastel" | Pastel bubble indicators |
progressBarType: {
type: "linear",
visible: true,
colors: {
background: "#e5e7eb",
foreground: "#008080",
},
}HeaderOptions
{
visible?: boolean;
type: "default" | "custom";
customHeader?: React.ReactNode; // Only used when type === "custom"
}ContainerStylesOptions
Customize the border and background of the form containers:
{
mainContainer?: ContainerStyles; // Outer container (sections + buttons)
sectionContainer?: ContainerStyles; // Inner container (each section)
}
// ContainerStyles
{
borderColor?: string; // e.g. "#e5e7eb"
borderWidth?: string | number; // e.g. "1px" or 1
backgroundColor?: string; // e.g. "#f9fafb"
}Section
A section groups related fields. Each section becomes a page in "bySection" mode.
{
sectionId: string;
title?: string | { [lang: string]: string } | null;
subTitle?: string | { [lang: string]: string } | null;
isSpacer?: string;
position?: number;
formFields: FormField[];
conditionalDisplay?: ConditionalDisplayGroup;
}FormField
The core building block. Every field in section.formFields follows this shape:
{
formFieldId: string; // Unique ID — UUID recommended
fieldName: string; // Technical key — becomes the key in onSubmit data
slug?: string; // Human-readable key for URLs / exports
fieldType: FieldType; // See table below
label: string | { [lang: string]: string };
response: { responseValue: string | string[] | number | number[] | boolean | File | File[] | null };
selectOptions?: SelectOption[];
validations?: ValidationRule[];
position?: number;
placeholder?: string | { [lang: string]: string };
tooltip?: string | { [lang: string]: string };
helpText?: string | { [lang: string]: string };
example?: string | { [lang: string]: string };
width?: "full" | "half";
enableSearch?: boolean;
conditionalDisplay?: ConditionalDisplayGroup;
repeatGroup?: FormRepeatGroup;
repeatRule?: RepeatRule;
fileToBucketManage?: FileToBucketManage;
otpFormFieldExecOptions?: OTPFormFieldExecOptions;
emailFormFieldExecOptions?: EmailFormFieldExecOptions;
detailInfos?: DetailInfosConfig;
dynamicFilterRule?: DynamicFilterRule;
flattenOnSubmit?: boolean;
}Field types
| fieldType | UI rendered | Notes |
|---|---|---|
| "text" | Text input | Also used for email with validation |
| "textarea" | Multi-line input | |
| "dropdown" | Select list | Requires selectOptions. Supports enableSearch |
| "radio" | Radio buttons | Requires selectOptions |
| "checkbox" | Checkboxes | Requires selectOptions |
| "date" | Date picker | |
| "number" | Numeric input | |
| "bool" | Toggle / switch | |
| "file" | File upload | Generic |
| "password" | Password input | Masked |
| "IMAGE" | Image upload | Preview included |
| "VIDEO" | Video upload | |
| "AUDIO" | Audio upload | |
| "DOCUMENT" | Document upload | |
| "PDF" | PDF upload | |
| "VOICE" | Voice recording | Browser mic |
| "TIP_TAP_DOC_TEXT" | Rich text editor | TipTap-powered |
| "OTP" | OTP code input | Requires otpFormFieldExecOptions |
SelectOption
Used with dropdown, radio, and checkbox fields:
{
value: string;
label: string | { [lang: string]: string };
points?: number; // Score / weight for quiz-like forms
icon?: string; // Icon name or URL
}ValidationRule
{
validationType?: ValidationType;
value?: number | string | string[];
errMsg: string; // Error message displayed to the user
}Validation types
| validationType | value type | Description |
|---|---|---|
| "required" | — | Field must not be empty |
| "minLength" | number | Minimum character count |
| "maxLength" | number | Maximum character count |
| "regex" | string | Must match the given pattern |
| "email" | — | Must be a valid email address |
| "number" | — | Must be a valid number |
| "fileSize" | number | Max file size in bytes |
| "phone_number" | — | Must be a valid phone number |
| "fileType" | string[] | Allowed MIME types — e.g. ["image/png", "image/jpeg"] |
Conditional display
Show or hide a section or field based on the values of other fields.
{
rules: ConditionalDisplayRule | ConditionalDisplayRule[];
logic?: "AND" | "OR"; // Default: "AND"
}ConditionalDisplayRule
{
fieldName?: string | string[]; // Target by technical key
formFieldId?: string | string[]; // Target by UUID
operator?: ConditionalOperator;
value?: string | number | boolean | string[] | number[];
numeric_compare_to?: number; // For aggregation comparisons
typeComputation?: "SOMME" | "MULTIPLICATION";
}Operators
"==" · "!=" · ">" · "<" · "IN" · "NOT IN" · "CONTAINS" · "NOT CONTAINS"
Examples
// Show field only if "has_vehicle" equals "yes"
conditionalDisplay: {
rules: { fieldName: "has_vehicle", operator: "==", value: "yes" },
}
// Show section if country is France OR Belgium
conditionalDisplay: {
rules: { fieldName: "country", operator: "IN", value: ["FR", "BE"] },
}
// Show if score1 + score2 > 10
conditionalDisplay: {
rules: {
fieldName: ["score1", "score2"],
typeComputation: "SOMME",
operator: ">",
numeric_compare_to: 10,
},
}Repeat groups
A FormRepeatGroup defines a block of fields that users can repeat N times (e.g. list of children, co-applicants).
// On FormField
repeatGroup: {
repeatGroupId: string;
fieldName: string;
label: string | { [lang: string]: string };
minRepeats?: number;
maxRepeats?: number;
initialRepeats?: number;
formFields: FormField[];
position?: number;
conditionalDisplay?: ConditionalDisplayGroup;
}
repeatRule: {
min?: number;
max?: number;
prefillEmpty?: boolean; // Pre-fill one empty instance on initial render
}OTP field
{
formFieldId: "f-otp",
fieldName: "otp_code",
fieldType: "OTP",
label: "Code de vérification",
response: { responseValue: "" },
otpFormFieldExecOptions: {
serverDns: "https://api.example.com",
postApiEndPoint: "/api/otp/verify",
otpLength: 6,
linkedEmailFieldName: "email", // Triggers OTP send when this field is filled
sendOTPApiConfig: {
serverDns: "https://api.example.com",
postApiEndPoint: "/api/otp/send",
bearer: "my-token",
},
autoValidate: true,
enableResend: true,
resendCooldownSeconds: 60,
},
}To trigger OTP send automatically from the email field, add emailFormFieldExecOptions to it:
{
formFieldId: "f-email",
fieldName: "email",
fieldType: "text",
label: "Email",
response: { responseValue: "" },
validations: [{ validationType: "email", errMsg: "Email invalide" }],
emailFormFieldExecOptions: {
triggerOTPSend: true,
linkedOTPFieldName: "otp_code",
otpSendApiConfig: {
serverDns: "https://api.example.com",
postApiEndPoint: "/api/otp/send",
},
},
}File uploads to a bucket
Add fileToBucketManage to any file-type field to automatically upload on selection and delete on removal:
fileToBucketManage: {
uploadOption: {
serverDns: "https://bucket.example.com",
postApiEndPoint: "/api/files/upload-file",
payload: { secret_key: "my-secret", folder_name: "avatars" },
bearer: "my-token",
timeoutMs: 30000,
},
deleteOption: {
serverDns: "https://bucket.example.com",
deleteApiEndPoint: "/api/files/delete-file",
payload: { secret_key: "my-secret", folder_name: "avatars" },
},
}Dynamic filter (cascading dropdowns)
Filter a dropdown's options based on another field's selected value:
{
formFieldId: "f-city",
fieldName: "city",
fieldType: "dropdown",
label: "Ville",
response: { responseValue: "" },
dynamicFilterRule: {
dependentFieldName: "country", // When "country" changes, reload city options
filterType: "exact",
dataSource: {
type: "static",
data: {
FR: [
{ value: "paris", label: "Paris" },
{ value: "lyon", label: "Lyon" },
],
BE: [
{ value: "bruxelles", label: "Bruxelles" },
{ value: "liege", label: "Liège" },
],
},
},
},
}Detail fields on selection
Automatically reveal additional fields when the user selects a specific option:
{
formFieldId: "f-sector",
fieldName: "sector",
fieldType: "dropdown",
label: "Secteur",
response: { responseValue: "" },
selectOptions: [
{ value: "agri", label: "Agriculture" },
{ value: "other", label: "Autre" },
],
detailInfos: {
other: {
label: "Précisez votre secteur",
formFields: [
{
formFieldId: "f-sector-detail",
fieldName: "sector_detail",
fieldType: "text",
label: "Secteur exact",
response: { responseValue: "" },
},
],
},
},
}Multilingual support
Every label, placeholder, tooltip, helpText, and example field accepts either a plain string or a language map:
label: { fr: "Nom complet", en: "Full name", es: "Nombre completo" }Pass currentLang="en" to <FormEngine> to switch the active language at runtime.
What onSubmit receives
The onSubmit callback receives a flat Record<string, any> where each key is a field's fieldName. File fields return the uploaded URL (if fileToBucketManage is configured) or the File object. Repeat groups return an array of objects.
// Example output
{
full_name: "Alice Dupont",
email: "[email protected]",
sector: "other",
sector_detail: "Fintech",
passengers: [
{ passenger_name: "Bob", passenger_age: 32 },
{ passenger_name: "Carol", passenger_age: 28 },
],
id_document: "https://bucket.example.com/files/doc-abc123.pdf",
}Complete real-world example
A 3-step registration form with:
- Step 1 — identity (text + email + OTP verification)
- Step 2 — profile (dropdown + conditional field + cascading dropdown)
- Step 3 — passengers (repeat group, 1–5 entries)
import { FormEngine } from "forme-engine";
import type { FormStructure } from "forme-engine";
const registrationForm: FormStructure = {
name: "Registration",
provider: "Acme Corp",
description: "Multi-step registration with OTP and repeat group",
status: "ACTIVE",
isDeclaration: 0,
isTest: 0,
theme: { primaryColor: "#6366f1" },
layoutOptions: {
paginationMode: "bySection",
progressBarType: { type: "linear", visible: true },
displayDeleteButton: false,
},
sections: [
// ── Step 1 — Identity + OTP ──────────────────────────────────────
{
sectionId: "s-identity",
title: { fr: "Votre identité", en: "Your identity" },
formFields: [
{
formFieldId: "f-firstname",
fieldName: "first_name",
fieldType: "text",
label: { fr: "Prénom", en: "First name" },
response: { responseValue: "" },
width: "half",
validations: [{ validationType: "required", errMsg: "Requis" }],
},
{
formFieldId: "f-lastname",
fieldName: "last_name",
fieldType: "text",
label: { fr: "Nom", en: "Last name" },
response: { responseValue: "" },
width: "half",
validations: [{ validationType: "required", errMsg: "Requis" }],
},
{
formFieldId: "f-email",
fieldName: "email",
fieldType: "text",
label: "Email",
response: { responseValue: "" },
validations: [
{ validationType: "required", errMsg: "Requis" },
{ validationType: "email", errMsg: "Email invalide" },
],
// Triggers OTP send automatically when a valid email is entered
emailFormFieldExecOptions: {
triggerOTPSend: true,
linkedOTPFieldName: "otp_code",
otpSendApiConfig: {
serverDns: "https://api.example.com",
postApiEndPoint: "/api/otp/send",
},
},
},
{
formFieldId: "f-otp",
fieldName: "otp_code",
fieldType: "OTP",
label: { fr: "Code de vérification (6 chiffres)", en: "Verification code (6 digits)" },
response: { responseValue: "" },
otpFormFieldExecOptions: {
serverDns: "https://api.example.com",
postApiEndPoint: "/api/otp/verify",
otpLength: 6,
linkedEmailFieldName: "email",
autoValidate: true,
enableResend: true,
resendCooldownSeconds: 60,
},
},
],
},
// ── Step 2 — Profile (conditional + cascading) ───────────────────
{
sectionId: "s-profile",
title: { fr: "Votre profil", en: "Your profile" },
formFields: [
{
formFieldId: "f-country",
fieldName: "country",
fieldType: "dropdown",
label: { fr: "Pays", en: "Country" },
response: { responseValue: "" },
enableSearch: true,
selectOptions: [
{ value: "FR", label: { fr: "France", en: "France" } },
{ value: "BE", label: { fr: "Belgique", en: "Belgium" } },
{ value: "CI", label: { fr: "Côte d'Ivoire", en: "Ivory Coast" } },
],
validations: [{ validationType: "required", errMsg: "Requis" }],
},
{
// City options change dynamically based on selected country
formFieldId: "f-city",
fieldName: "city",
fieldType: "dropdown",
label: { fr: "Ville", en: "City" },
response: { responseValue: "" },
dynamicFilterRule: {
dependentFieldName: "country",
filterType: "exact",
dataSource: {
type: "static",
data: {
FR: [{ value: "paris", label: "Paris" }, { value: "lyon", label: "Lyon" }],
BE: [{ value: "bruxelles", label: "Bruxelles" }, { value: "liege", label: "Liège" }],
CI: [{ value: "abidjan", label: "Abidjan" }, { value: "bouake", label: "Bouaké" }],
},
},
},
},
{
formFieldId: "f-sector",
fieldName: "sector",
fieldType: "dropdown",
label: { fr: "Secteur d'activité", en: "Business sector" },
response: { responseValue: "" },
selectOptions: [
{ value: "agri", label: { fr: "Agriculture", en: "Agriculture" } },
{ value: "tech", label: "Tech" },
{ value: "other", label: { fr: "Autre", en: "Other" } },
],
// Reveals a free-text field when "Autre" is selected
detailInfos: {
other: {
label: { fr: "Précisez", en: "Specify" },
formFields: [
{
formFieldId: "f-sector-detail",
fieldName: "sector_detail",
fieldType: "text",
label: { fr: "Secteur exact", en: "Exact sector" },
response: { responseValue: "" },
},
],
},
},
},
],
},
// ── Step 3 — Passengers (repeat group) ───────────────────────────
{
sectionId: "s-passengers",
title: { fr: "Passagers", en: "Passengers" },
formFields: [
{
formFieldId: "f-passengers",
fieldName: "passengers",
fieldType: "text",
label: { fr: "Passagers", en: "Passengers" },
response: { responseValue: "" },
repeatGroup: {
repeatGroupId: "rg-passengers",
fieldName: "passengers",
label: { fr: "Passager", en: "Passenger" },
minRepeats: 1,
maxRepeats: 5,
initialRepeats: 1,
formFields: [
{
formFieldId: "f-p-name",
fieldName: "passenger_name",
fieldType: "text",
label: { fr: "Nom complet", en: "Full name" },
response: { responseValue: "" },
width: "half",
validations: [{ validationType: "required", errMsg: "Requis" }],
},
{
formFieldId: "f-p-age",
fieldName: "passenger_age",
fieldType: "number",
label: { fr: "Âge", en: "Age" },
response: { responseValue: "" },
width: "half",
validations: [{ validationType: "required", errMsg: "Requis" }],
},
{
formFieldId: "f-p-passport",
fieldName: "passport_scan",
fieldType: "PDF",
label: { fr: "Scan passeport", en: "Passport scan" },
response: { responseValue: "" },
fileToBucketManage: {
uploadOption: {
serverDns: "https://bucket.example.com",
postApiEndPoint: "/api/files/upload-file",
payload: { folder_name: "passports" },
bearer: "my-token",
},
deleteOption: {
serverDns: "https://bucket.example.com",
deleteApiEndPoint: "/api/files/delete-file",
payload: { folder_name: "passports" },
},
},
},
],
},
repeatRule: { min: 1, max: 5, prefillEmpty: true },
},
],
},
],
};
export default function RegistrationPage() {
return (
<FormEngine
form={registrationForm}
formId="registration-2024"
currentLang="fr"
submitButtonText="Suivant"
onSubmit={(data) => {
// data.first_name, data.last_name, data.email
// data.country, data.city, data.sector, data.sector_detail?
// data.passengers: [{ passenger_name, passenger_age, passport_scan }]
console.log(data);
}}
/>
);
}License
MIT © Lionel TOTON
