@ainelo/form-engine 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +869 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,869 @@
1
+ <div align="center">
2
+
3
+ # `@ainelo/form-engine`
4
+
5
+ **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.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@ainelo/form-engine?color=teal&style=flat-square)](https://www.npmjs.com/package/@ainelo/form-engine)
8
+ [![license](https://img.shields.io/npm/l/@ainelo/form-engine?style=flat-square)](./LICENSE)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/)
10
+ [![React](https://img.shields.io/badge/React-18+-61DAFB?style=flat-square&logo=react)](https://react.dev/)
11
+
12
+ </div>
13
+
14
+ ---
15
+
16
+ ## Why `@ainelo/form-engine`?
17
+
18
+ Most React form libraries give you primitives — you still wire validation, conditionals, multi-step logic, file uploads, and OTP flows yourself. `@ainelo/form-engine` is the only library in the React ecosystem that combines all of the following **out of the box**, with zero boilerplate:
19
+
20
+ | Feature | react-hook-form | Formik | RJSF | **form-engine** |
21
+ |---|:---:|:---:|:---:|:---:|
22
+ | JSON-driven rendering | ❌ | ❌ | ✅ | ✅ |
23
+ | Conditional display (AND/OR/computed) | ❌ | ❌ | ⚠️ | ✅ |
24
+ | Multi-step + progress bar | ❌ | ❌ | ❌ | ✅ |
25
+ | Repeat groups (dynamic arrays) | ❌ | ❌ | ✅ | ✅ |
26
+ | OTP flow (send + verify + resend) | ❌ | ❌ | ❌ | ✅ |
27
+ | File-to-bucket upload/delete | ❌ | ❌ | ❌ | ✅ |
28
+ | Cascading dropdowns | ❌ | ❌ | ❌ | ✅ |
29
+ | Multilingual labels | ❌ | ❌ | ⚠️ | ✅ |
30
+ | Radix UI + Tailwind (shadcn-compatible) | ❌ | ❌ | ❌ | ✅ |
31
+ | Zod validation (auto-generated) | ❌ | ❌ | ❌ | ✅ |
32
+ | Draft persistence (Zustand) | ❌ | ❌ | ❌ | ✅ |
33
+
34
+ **The sweet spot:** if your stack is React + Zod + Radix UI + Tailwind (the modern default), `@ainelo/form-engine` plugs in natively and eliminates weeks of form infrastructure work.
35
+
36
+ ---
37
+
38
+ ## Overview
39
+
40
+ `@ainelo/form-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.
41
+
42
+ **Key capabilities:**
43
+
44
+ - **All standard field types** — text, textarea, dropdown, radio, checkbox, date, number, boolean, password, file
45
+ - **Rich media fields** — image, video, audio, document, PDF, voice recording
46
+ - **Rich text editor** — TipTap integration
47
+ - **OTP verification** — built-in phone/email OTP flow with resend logic
48
+ - **File-to-bucket uploads** — upload and delete files via a configurable API
49
+ - **Conditional display** — show/hide sections and fields based on other field values
50
+ - **Repeat groups** — dynamic lists of fields (e.g. list of passengers, children, etc.)
51
+ - **Multi-step pagination** — by section or by field count, with progress bar
52
+ - **Multilingual labels** — every text supports `{ fr: "...", en: "..." }` objects
53
+ - **Zod validation** — automatic schema generation from field definitions
54
+ - **Form persistence** — draft saving via Zustand
55
+ - **Theming** — primary color + per-container style overrides
56
+
57
+ ---
58
+
59
+ ## Installation
60
+
61
+ ```bash
62
+ npm install @ainelo/form-engine
63
+ ```
64
+
65
+ ### Peer dependencies
66
+
67
+ The following packages must be installed in your project:
68
+
69
+ ```bash
70
+ npm install react react-dom react-hook-form zod zustand lucide-react react-hot-toast \
71
+ @tiptap/react @tiptap/starter-kit @tiptap/extension-image \
72
+ @tiptap/extension-link @tiptap/extension-placeholder
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Quick start
78
+
79
+ ```tsx
80
+ import { FormEngine } from "@ainelo/form-engine";
81
+ import type { FormStructure } from "@ainelo/form-engine";
82
+
83
+ const form: FormStructure = {
84
+ name: "Contact",
85
+ provider: "Acme Corp",
86
+ description: "Simple contact form",
87
+ status: "ACTIVE",
88
+ isDeclaration: 0,
89
+ isTest: 0,
90
+ theme: { primaryColor: "#008080" },
91
+ layoutOptions: {
92
+ paginationMode: "bySection",
93
+ progressBarType: { type: "linear", visible: true },
94
+ },
95
+ sections: [
96
+ {
97
+ sectionId: "section-contact",
98
+ title: { fr: "Vos informations", en: "Your information" },
99
+ formFields: [
100
+ {
101
+ formFieldId: "f-name",
102
+ fieldName: "full_name",
103
+ fieldType: "text",
104
+ label: { fr: "Nom complet", en: "Full name" },
105
+ response: { responseValue: "" },
106
+ width: "half",
107
+ validations: [
108
+ { validationType: "required", errMsg: "Le nom est requis" },
109
+ ],
110
+ },
111
+ {
112
+ formFieldId: "f-email",
113
+ fieldName: "email",
114
+ fieldType: "text",
115
+ label: "Email",
116
+ response: { responseValue: "" },
117
+ width: "half",
118
+ validations: [
119
+ { validationType: "required", errMsg: "L'email est requis" },
120
+ { validationType: "email", errMsg: "Email invalide" },
121
+ ],
122
+ },
123
+ ],
124
+ },
125
+ ],
126
+ };
127
+
128
+ export default function App() {
129
+ return (
130
+ <FormEngine
131
+ form={form}
132
+ formId="contact-form"
133
+ onSubmit={(data) => console.log(data)}
134
+ currentLang="fr"
135
+ submitButtonText="Envoyer"
136
+ />
137
+ );
138
+ }
139
+ ```
140
+
141
+ ---
142
+
143
+ ## `<FormEngine>` props
144
+
145
+ | Prop | Type | Required | Default | Description |
146
+ |------|------|:--------:|---------|-------------|
147
+ | `form` | `FormStructure` | ✅ | — | The complete form definition object |
148
+ | `formId` | `string` | ✅ | — | Unique instance ID — used for draft persistence |
149
+ | `onSubmit` | `(data: Record<string, any>) => void` | ✅ | — | Called when the form is valid and submitted |
150
+ | `currentLang` | `string` | | `"fr"` | Active language code for multilingual labels |
151
+ | `submitButtonText` | `string` | | `"Continuer"` | Label of the submit / next button |
152
+ | `paginationMode` | `"byFields" \| "bySection"` | | `"byFields"` | Overrides `form.layoutOptions.paginationMode` |
153
+
154
+ ---
155
+
156
+ ## Form structure reference
157
+
158
+ ### `FormStructure`
159
+
160
+ The root object passed to the `form` prop.
161
+
162
+ ```ts
163
+ {
164
+ name: string;
165
+ provider: string;
166
+ description: string;
167
+ status: "ACTIVE" | "INACTIVE";
168
+ isDeclaration: number; // 1 = official declaration, 0 = standard
169
+ isTest: number; // 1 = test, 0 = production
170
+ version?: string;
171
+ createdAt?: string; // ISO date string
172
+ updatedAt?: string; // ISO date string
173
+ sections: Section[];
174
+ theme?: FormTheme;
175
+ layoutOptions?: FormEngineLayoutOptions;
176
+ }
177
+ ```
178
+
179
+ ---
180
+
181
+ ### `FormTheme`
182
+
183
+ ```ts
184
+ {
185
+ primaryColor?: string; // Hex color — e.g. "#008080"
186
+ }
187
+ ```
188
+
189
+ ---
190
+
191
+ ### `FormEngineLayoutOptions`
192
+
193
+ ```ts
194
+ {
195
+ paginationMode?: "byFields" | "bySection";
196
+ progressBarType?: ProgressBarType;
197
+ headerOptions?: HeaderOptions;
198
+ displayDeleteButton?: boolean; // Show "clear all" button — default: true
199
+ containerStyles?: ContainerStylesOptions;
200
+ }
201
+ ```
202
+
203
+ #### `ProgressBarType`
204
+
205
+ | `type` value | Description |
206
+ |---|---|
207
+ | `"default_step"` | Classic numbered steps |
208
+ | `"linear"` | Horizontal progress bar |
209
+ | `"pastel"` | Pastel-toned step bar |
210
+ | `"custom"` | Custom colors via `colors` |
211
+ | `"section_bubble"` | Bubble indicators per section |
212
+ | `"section_bubble_pastel"` | Pastel bubble indicators |
213
+
214
+ ```ts
215
+ progressBarType: {
216
+ type: "linear",
217
+ visible: true,
218
+ colors: {
219
+ background: "#e5e7eb",
220
+ foreground: "#008080",
221
+ },
222
+ }
223
+ ```
224
+
225
+ #### `HeaderOptions`
226
+
227
+ ```ts
228
+ {
229
+ visible?: boolean;
230
+ type: "default" | "custom";
231
+ customHeader?: React.ReactNode; // Only used when type === "custom"
232
+ }
233
+ ```
234
+
235
+ #### `ContainerStylesOptions`
236
+
237
+ Customize the border and background of the form containers:
238
+
239
+ ```ts
240
+ {
241
+ mainContainer?: ContainerStyles; // Outer container (sections + buttons)
242
+ sectionContainer?: ContainerStyles; // Inner container (each section)
243
+ }
244
+
245
+ // ContainerStyles
246
+ {
247
+ borderColor?: string; // e.g. "#e5e7eb"
248
+ borderWidth?: string | number; // e.g. "1px" or 1
249
+ backgroundColor?: string; // e.g. "#f9fafb"
250
+ }
251
+ ```
252
+
253
+ ---
254
+
255
+ ### `Section`
256
+
257
+ A section groups related fields. Each section becomes a page in `"bySection"` mode.
258
+
259
+ ```ts
260
+ {
261
+ sectionId: string;
262
+ title?: string | { [lang: string]: string } | null;
263
+ subTitle?: string | { [lang: string]: string } | null;
264
+ isSpacer?: string;
265
+ position?: number;
266
+ formFields: FormField[];
267
+ conditionalDisplay?: ConditionalDisplayGroup;
268
+ }
269
+ ```
270
+
271
+ ---
272
+
273
+ ### `FormField`
274
+
275
+ The core building block. Every field in `section.formFields` follows this shape:
276
+
277
+ ```ts
278
+ {
279
+ formFieldId: string; // Unique ID — UUID recommended
280
+ fieldName: string; // Technical key — becomes the key in onSubmit data
281
+ slug?: string; // Human-readable key for URLs / exports
282
+ fieldType: FieldType; // See table below
283
+ label: string | { [lang: string]: string };
284
+ response: { responseValue: string | string[] | number | number[] | boolean | File | File[] | null };
285
+ selectOptions?: SelectOption[];
286
+ validations?: ValidationRule[];
287
+ position?: number;
288
+ placeholder?: string | { [lang: string]: string };
289
+ tooltip?: string | { [lang: string]: string };
290
+ helpText?: string | { [lang: string]: string };
291
+ example?: string | { [lang: string]: string };
292
+ width?: "full" | "half";
293
+ enableSearch?: boolean;
294
+ conditionalDisplay?: ConditionalDisplayGroup;
295
+ repeatGroup?: FormRepeatGroup;
296
+ repeatRule?: RepeatRule;
297
+ fileToBucketManage?: FileToBucketManage;
298
+ otpFormFieldExecOptions?: OTPFormFieldExecOptions;
299
+ emailFormFieldExecOptions?: EmailFormFieldExecOptions;
300
+ detailInfos?: DetailInfosConfig;
301
+ dynamicFilterRule?: DynamicFilterRule;
302
+ flattenOnSubmit?: boolean;
303
+ }
304
+ ```
305
+
306
+ #### Field types
307
+
308
+ | `fieldType` | UI rendered | Notes |
309
+ |---|---|---|
310
+ | `"text"` | Text input | Also used for email with validation |
311
+ | `"textarea"` | Multi-line input | |
312
+ | `"dropdown"` | Select list | Requires `selectOptions`. Supports `enableSearch` |
313
+ | `"radio"` | Radio buttons | Requires `selectOptions` |
314
+ | `"checkbox"` | Checkboxes | Requires `selectOptions` |
315
+ | `"date"` | Date picker | |
316
+ | `"number"` | Numeric input | |
317
+ | `"bool"` | Toggle / switch | |
318
+ | `"file"` | File upload | Generic |
319
+ | `"password"` | Password input | Masked |
320
+ | `"IMAGE"` | Image upload | Preview included |
321
+ | `"VIDEO"` | Video upload | |
322
+ | `"AUDIO"` | Audio upload | |
323
+ | `"DOCUMENT"` | Document upload | |
324
+ | `"PDF"` | PDF upload | |
325
+ | `"VOICE"` | Voice recording | Browser mic |
326
+ | `"TIP_TAP_DOC_TEXT"` | Rich text editor | TipTap-powered |
327
+ | `"OTP"` | OTP code input | Requires `otpFormFieldExecOptions` |
328
+
329
+ ---
330
+
331
+ ### `SelectOption`
332
+
333
+ Used with `dropdown`, `radio`, and `checkbox` fields:
334
+
335
+ ```ts
336
+ {
337
+ value: string;
338
+ label: string | { [lang: string]: string };
339
+ points?: number; // Score / weight for quiz-like forms
340
+ icon?: string; // Icon name or URL
341
+ }
342
+ ```
343
+
344
+ ---
345
+
346
+ ### `ValidationRule`
347
+
348
+ ```ts
349
+ {
350
+ validationType?: ValidationType;
351
+ value?: number | string | string[];
352
+ errMsg: string; // Error message displayed to the user
353
+ }
354
+ ```
355
+
356
+ #### Validation types
357
+
358
+ | `validationType` | `value` type | Description |
359
+ |---|---|---|
360
+ | `"required"` | — | Field must not be empty |
361
+ | `"minLength"` | `number` | Minimum character count |
362
+ | `"maxLength"` | `number` | Maximum character count |
363
+ | `"regex"` | `string` | Must match the given pattern |
364
+ | `"email"` | — | Must be a valid email address |
365
+ | `"number"` | — | Must be a valid number |
366
+ | `"fileSize"` | `number` | Max file size in bytes |
367
+ | `"phone_number"` | — | Must be a valid phone number |
368
+ | `"fileType"` | `string[]` | Allowed MIME types — e.g. `["image/png", "image/jpeg"]` |
369
+
370
+ ---
371
+
372
+ ## Conditional display
373
+
374
+ Show or hide a section or field based on the values of other fields.
375
+
376
+ ```ts
377
+ {
378
+ rules: ConditionalDisplayRule | ConditionalDisplayRule[];
379
+ logic?: "AND" | "OR"; // Default: "AND"
380
+ }
381
+ ```
382
+
383
+ ### `ConditionalDisplayRule`
384
+
385
+ ```ts
386
+ {
387
+ fieldName?: string | string[]; // Target by technical key
388
+ formFieldId?: string | string[]; // Target by UUID
389
+ operator?: ConditionalOperator;
390
+ value?: string | number | boolean | string[] | number[];
391
+ numeric_compare_to?: number; // For aggregation comparisons
392
+ typeComputation?: "SOMME" | "MULTIPLICATION";
393
+ }
394
+ ```
395
+
396
+ ### Operators
397
+
398
+ `"=="` · `"!="` · `">"` · `"<"` · `"IN"` · `"NOT IN"` · `"CONTAINS"` · `"NOT CONTAINS"`
399
+
400
+ ### Examples
401
+
402
+ ```ts
403
+ // Show field only if "has_vehicle" equals "yes"
404
+ conditionalDisplay: {
405
+ rules: { fieldName: "has_vehicle", operator: "==", value: "yes" },
406
+ }
407
+
408
+ // Show section if country is France OR Belgium
409
+ conditionalDisplay: {
410
+ rules: { fieldName: "country", operator: "IN", value: ["FR", "BE"] },
411
+ }
412
+
413
+ // Show if score1 + score2 > 10
414
+ conditionalDisplay: {
415
+ rules: {
416
+ fieldName: ["score1", "score2"],
417
+ typeComputation: "SOMME",
418
+ operator: ">",
419
+ numeric_compare_to: 10,
420
+ },
421
+ }
422
+ ```
423
+
424
+ ---
425
+
426
+ ## Repeat groups
427
+
428
+ A `FormRepeatGroup` defines a block of fields that users can repeat N times (e.g. list of children, co-applicants).
429
+
430
+ ```ts
431
+ // On FormField
432
+ repeatGroup: {
433
+ repeatGroupId: string;
434
+ fieldName: string;
435
+ label: string | { [lang: string]: string };
436
+ minRepeats?: number;
437
+ maxRepeats?: number;
438
+ initialRepeats?: number;
439
+ formFields: FormField[];
440
+ position?: number;
441
+ conditionalDisplay?: ConditionalDisplayGroup;
442
+ }
443
+
444
+ repeatRule: {
445
+ min?: number;
446
+ max?: number;
447
+ prefillEmpty?: boolean; // Pre-fill one empty instance on initial render
448
+ }
449
+ ```
450
+
451
+ ---
452
+
453
+ ## OTP field
454
+
455
+ ```ts
456
+ {
457
+ formFieldId: "f-otp",
458
+ fieldName: "otp_code",
459
+ fieldType: "OTP",
460
+ label: "Code de vérification",
461
+ response: { responseValue: "" },
462
+ otpFormFieldExecOptions: {
463
+ serverDns: "https://api.example.com",
464
+ postApiEndPoint: "/api/otp/verify",
465
+ otpLength: 6,
466
+ linkedEmailFieldName: "email", // Triggers OTP send when this field is filled
467
+ sendOTPApiConfig: {
468
+ serverDns: "https://api.example.com",
469
+ postApiEndPoint: "/api/otp/send",
470
+ bearer: "my-token",
471
+ },
472
+ autoValidate: true,
473
+ enableResend: true,
474
+ resendCooldownSeconds: 60,
475
+ },
476
+ }
477
+ ```
478
+
479
+ To trigger OTP send automatically from the email field, add `emailFormFieldExecOptions` to it:
480
+
481
+ ```ts
482
+ {
483
+ formFieldId: "f-email",
484
+ fieldName: "email",
485
+ fieldType: "text",
486
+ label: "Email",
487
+ response: { responseValue: "" },
488
+ validations: [{ validationType: "email", errMsg: "Email invalide" }],
489
+ emailFormFieldExecOptions: {
490
+ triggerOTPSend: true,
491
+ linkedOTPFieldName: "otp_code",
492
+ otpSendApiConfig: {
493
+ serverDns: "https://api.example.com",
494
+ postApiEndPoint: "/api/otp/send",
495
+ },
496
+ },
497
+ }
498
+ ```
499
+
500
+ ---
501
+
502
+ ## File uploads to a bucket
503
+
504
+ Add `fileToBucketManage` to any file-type field to automatically upload on selection and delete on removal:
505
+
506
+ ```ts
507
+ fileToBucketManage: {
508
+ uploadOption: {
509
+ serverDns: "https://bucket.example.com",
510
+ postApiEndPoint: "/api/files/upload-file",
511
+ payload: { secret_key: "my-secret", folder_name: "avatars" },
512
+ bearer: "my-token",
513
+ timeoutMs: 30000,
514
+ },
515
+ deleteOption: {
516
+ serverDns: "https://bucket.example.com",
517
+ deleteApiEndPoint: "/api/files/delete-file",
518
+ payload: { secret_key: "my-secret", folder_name: "avatars" },
519
+ },
520
+ }
521
+ ```
522
+
523
+ ---
524
+
525
+ ## Dynamic filter (cascading dropdowns)
526
+
527
+ Filter a dropdown's options based on another field's selected value:
528
+
529
+ ```ts
530
+ {
531
+ formFieldId: "f-city",
532
+ fieldName: "city",
533
+ fieldType: "dropdown",
534
+ label: "Ville",
535
+ response: { responseValue: "" },
536
+ dynamicFilterRule: {
537
+ dependentFieldName: "country", // When "country" changes, reload city options
538
+ filterType: "exact",
539
+ dataSource: {
540
+ type: "static",
541
+ data: {
542
+ FR: [
543
+ { value: "paris", label: "Paris" },
544
+ { value: "lyon", label: "Lyon" },
545
+ ],
546
+ BE: [
547
+ { value: "bruxelles", label: "Bruxelles" },
548
+ { value: "liege", label: "Liège" },
549
+ ],
550
+ },
551
+ },
552
+ },
553
+ }
554
+ ```
555
+
556
+ ---
557
+
558
+ ## Detail fields on selection
559
+
560
+ Automatically reveal additional fields when the user selects a specific option:
561
+
562
+ ```ts
563
+ {
564
+ formFieldId: "f-sector",
565
+ fieldName: "sector",
566
+ fieldType: "dropdown",
567
+ label: "Secteur",
568
+ response: { responseValue: "" },
569
+ selectOptions: [
570
+ { value: "agri", label: "Agriculture" },
571
+ { value: "other", label: "Autre" },
572
+ ],
573
+ detailInfos: {
574
+ other: {
575
+ label: "Précisez votre secteur",
576
+ formFields: [
577
+ {
578
+ formFieldId: "f-sector-detail",
579
+ fieldName: "sector_detail",
580
+ fieldType: "text",
581
+ label: "Secteur exact",
582
+ response: { responseValue: "" },
583
+ },
584
+ ],
585
+ },
586
+ },
587
+ }
588
+ ```
589
+
590
+ ---
591
+
592
+ ## Multilingual support
593
+
594
+ Every `label`, `placeholder`, `tooltip`, `helpText`, and `example` field accepts either a plain string or a language map:
595
+
596
+ ```ts
597
+ label: { fr: "Nom complet", en: "Full name", es: "Nombre completo" }
598
+ ```
599
+
600
+ Pass `currentLang="en"` to `<FormEngine>` to switch the active language at runtime.
601
+
602
+ ---
603
+
604
+ ## What `onSubmit` receives
605
+
606
+ 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.
607
+
608
+ ```ts
609
+ // Example output
610
+ {
611
+ full_name: "Alice Dupont",
612
+ email: "alice@example.com",
613
+ sector: "other",
614
+ sector_detail: "Fintech",
615
+ passengers: [
616
+ { passenger_name: "Bob", passenger_age: 32 },
617
+ { passenger_name: "Carol", passenger_age: 28 },
618
+ ],
619
+ id_document: "https://bucket.example.com/files/doc-abc123.pdf",
620
+ }
621
+ ```
622
+
623
+ ---
624
+
625
+ ## Complete real-world example
626
+
627
+ A 3-step registration form with:
628
+ - **Step 1** — identity (text + email + OTP verification)
629
+ - **Step 2** — profile (dropdown + conditional field + cascading dropdown)
630
+ - **Step 3** — passengers (repeat group, 1–5 entries)
631
+
632
+ ```tsx
633
+ import { FormEngine } from "@ainelo/form-engine";
634
+ import type { FormStructure } from "@ainelo/form-engine";
635
+
636
+ const registrationForm: FormStructure = {
637
+ name: "Registration",
638
+ provider: "Acme Corp",
639
+ description: "Multi-step registration with OTP and repeat group",
640
+ status: "ACTIVE",
641
+ isDeclaration: 0,
642
+ isTest: 0,
643
+ theme: { primaryColor: "#6366f1" },
644
+ layoutOptions: {
645
+ paginationMode: "bySection",
646
+ progressBarType: { type: "linear", visible: true },
647
+ displayDeleteButton: false,
648
+ },
649
+ sections: [
650
+ // ── Step 1 — Identity + OTP ──────────────────────────────────────
651
+ {
652
+ sectionId: "s-identity",
653
+ title: { fr: "Votre identité", en: "Your identity" },
654
+ formFields: [
655
+ {
656
+ formFieldId: "f-firstname",
657
+ fieldName: "first_name",
658
+ fieldType: "text",
659
+ label: { fr: "Prénom", en: "First name" },
660
+ response: { responseValue: "" },
661
+ width: "half",
662
+ validations: [{ validationType: "required", errMsg: "Requis" }],
663
+ },
664
+ {
665
+ formFieldId: "f-lastname",
666
+ fieldName: "last_name",
667
+ fieldType: "text",
668
+ label: { fr: "Nom", en: "Last name" },
669
+ response: { responseValue: "" },
670
+ width: "half",
671
+ validations: [{ validationType: "required", errMsg: "Requis" }],
672
+ },
673
+ {
674
+ formFieldId: "f-email",
675
+ fieldName: "email",
676
+ fieldType: "text",
677
+ label: "Email",
678
+ response: { responseValue: "" },
679
+ validations: [
680
+ { validationType: "required", errMsg: "Requis" },
681
+ { validationType: "email", errMsg: "Email invalide" },
682
+ ],
683
+ // Triggers OTP send automatically when a valid email is entered
684
+ emailFormFieldExecOptions: {
685
+ triggerOTPSend: true,
686
+ linkedOTPFieldName: "otp_code",
687
+ otpSendApiConfig: {
688
+ serverDns: "https://api.example.com",
689
+ postApiEndPoint: "/api/otp/send",
690
+ },
691
+ },
692
+ },
693
+ {
694
+ formFieldId: "f-otp",
695
+ fieldName: "otp_code",
696
+ fieldType: "OTP",
697
+ label: { fr: "Code de vérification (6 chiffres)", en: "Verification code (6 digits)" },
698
+ response: { responseValue: "" },
699
+ otpFormFieldExecOptions: {
700
+ serverDns: "https://api.example.com",
701
+ postApiEndPoint: "/api/otp/verify",
702
+ otpLength: 6,
703
+ linkedEmailFieldName: "email",
704
+ autoValidate: true,
705
+ enableResend: true,
706
+ resendCooldownSeconds: 60,
707
+ },
708
+ },
709
+ ],
710
+ },
711
+
712
+ // ── Step 2 — Profile (conditional + cascading) ───────────────────
713
+ {
714
+ sectionId: "s-profile",
715
+ title: { fr: "Votre profil", en: "Your profile" },
716
+ formFields: [
717
+ {
718
+ formFieldId: "f-country",
719
+ fieldName: "country",
720
+ fieldType: "dropdown",
721
+ label: { fr: "Pays", en: "Country" },
722
+ response: { responseValue: "" },
723
+ enableSearch: true,
724
+ selectOptions: [
725
+ { value: "FR", label: { fr: "France", en: "France" } },
726
+ { value: "BE", label: { fr: "Belgique", en: "Belgium" } },
727
+ { value: "CI", label: { fr: "Côte d'Ivoire", en: "Ivory Coast" } },
728
+ ],
729
+ validations: [{ validationType: "required", errMsg: "Requis" }],
730
+ },
731
+ {
732
+ // City options change dynamically based on selected country
733
+ formFieldId: "f-city",
734
+ fieldName: "city",
735
+ fieldType: "dropdown",
736
+ label: { fr: "Ville", en: "City" },
737
+ response: { responseValue: "" },
738
+ dynamicFilterRule: {
739
+ dependentFieldName: "country",
740
+ filterType: "exact",
741
+ dataSource: {
742
+ type: "static",
743
+ data: {
744
+ FR: [{ value: "paris", label: "Paris" }, { value: "lyon", label: "Lyon" }],
745
+ BE: [{ value: "bruxelles", label: "Bruxelles" }, { value: "liege", label: "Liège" }],
746
+ CI: [{ value: "abidjan", label: "Abidjan" }, { value: "bouake", label: "Bouaké" }],
747
+ },
748
+ },
749
+ },
750
+ },
751
+ {
752
+ formFieldId: "f-sector",
753
+ fieldName: "sector",
754
+ fieldType: "dropdown",
755
+ label: { fr: "Secteur d'activité", en: "Business sector" },
756
+ response: { responseValue: "" },
757
+ selectOptions: [
758
+ { value: "agri", label: { fr: "Agriculture", en: "Agriculture" } },
759
+ { value: "tech", label: "Tech" },
760
+ { value: "other", label: { fr: "Autre", en: "Other" } },
761
+ ],
762
+ // Reveals a free-text field when "Autre" is selected
763
+ detailInfos: {
764
+ other: {
765
+ label: { fr: "Précisez", en: "Specify" },
766
+ formFields: [
767
+ {
768
+ formFieldId: "f-sector-detail",
769
+ fieldName: "sector_detail",
770
+ fieldType: "text",
771
+ label: { fr: "Secteur exact", en: "Exact sector" },
772
+ response: { responseValue: "" },
773
+ },
774
+ ],
775
+ },
776
+ },
777
+ },
778
+ ],
779
+ },
780
+
781
+ // ── Step 3 — Passengers (repeat group) ───────────────────────────
782
+ {
783
+ sectionId: "s-passengers",
784
+ title: { fr: "Passagers", en: "Passengers" },
785
+ formFields: [
786
+ {
787
+ formFieldId: "f-passengers",
788
+ fieldName: "passengers",
789
+ fieldType: "text",
790
+ label: { fr: "Passagers", en: "Passengers" },
791
+ response: { responseValue: "" },
792
+ repeatGroup: {
793
+ repeatGroupId: "rg-passengers",
794
+ fieldName: "passengers",
795
+ label: { fr: "Passager", en: "Passenger" },
796
+ minRepeats: 1,
797
+ maxRepeats: 5,
798
+ initialRepeats: 1,
799
+ formFields: [
800
+ {
801
+ formFieldId: "f-p-name",
802
+ fieldName: "passenger_name",
803
+ fieldType: "text",
804
+ label: { fr: "Nom complet", en: "Full name" },
805
+ response: { responseValue: "" },
806
+ width: "half",
807
+ validations: [{ validationType: "required", errMsg: "Requis" }],
808
+ },
809
+ {
810
+ formFieldId: "f-p-age",
811
+ fieldName: "passenger_age",
812
+ fieldType: "number",
813
+ label: { fr: "Âge", en: "Age" },
814
+ response: { responseValue: "" },
815
+ width: "half",
816
+ validations: [{ validationType: "required", errMsg: "Requis" }],
817
+ },
818
+ {
819
+ formFieldId: "f-p-passport",
820
+ fieldName: "passport_scan",
821
+ fieldType: "PDF",
822
+ label: { fr: "Scan passeport", en: "Passport scan" },
823
+ response: { responseValue: "" },
824
+ fileToBucketManage: {
825
+ uploadOption: {
826
+ serverDns: "https://bucket.example.com",
827
+ postApiEndPoint: "/api/files/upload-file",
828
+ payload: { folder_name: "passports" },
829
+ bearer: "my-token",
830
+ },
831
+ deleteOption: {
832
+ serverDns: "https://bucket.example.com",
833
+ deleteApiEndPoint: "/api/files/delete-file",
834
+ payload: { folder_name: "passports" },
835
+ },
836
+ },
837
+ },
838
+ ],
839
+ },
840
+ repeatRule: { min: 1, max: 5, prefillEmpty: true },
841
+ },
842
+ ],
843
+ },
844
+ ],
845
+ };
846
+
847
+ export default function RegistrationPage() {
848
+ return (
849
+ <FormEngine
850
+ form={registrationForm}
851
+ formId="registration-2024"
852
+ currentLang="fr"
853
+ submitButtonText="Suivant"
854
+ onSubmit={(data) => {
855
+ // data.first_name, data.last_name, data.email
856
+ // data.country, data.city, data.sector, data.sector_detail?
857
+ // data.passengers: [{ passenger_name, passenger_age, passport_scan }]
858
+ console.log(data);
859
+ }}
860
+ />
861
+ );
862
+ }
863
+ ```
864
+
865
+ ---
866
+
867
+ ## License
868
+
869
+ MIT © [Lionel TOTON](mailto:totonlionel@gmail.com)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainelo/form-engine",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "React form generation library — conditional display, multi-step pagination, OTP, file upload, repeat groups",
5
5
  "author": {
6
6
  "name": "TOTON Lionel",