@ainelo/form-engine 1.0.3 → 1.0.4

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 +605 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,605 @@
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
+ ## Overview
17
+
18
+ `@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.
19
+
20
+ **Key capabilities:**
21
+
22
+ - **All standard field types** — text, textarea, dropdown, radio, checkbox, date, number, boolean, password, file
23
+ - **Rich media fields** — image, video, audio, document, PDF, voice recording
24
+ - **Rich text editor** — TipTap integration
25
+ - **OTP verification** — built-in phone/email OTP flow with resend logic
26
+ - **File-to-bucket uploads** — upload and delete files via a configurable API
27
+ - **Conditional display** — show/hide sections and fields based on other field values
28
+ - **Repeat groups** — dynamic lists of fields (e.g. list of passengers, children, etc.)
29
+ - **Multi-step pagination** — by section or by field count, with progress bar
30
+ - **Multilingual labels** — every text supports `{ fr: "...", en: "..." }` objects
31
+ - **Zod validation** — automatic schema generation from field definitions
32
+ - **Form persistence** — draft saving via Zustand
33
+ - **Theming** — primary color + per-container style overrides
34
+
35
+ ---
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ npm install @ainelo/form-engine
41
+ ```
42
+
43
+ ### Peer dependencies
44
+
45
+ The following packages must be installed in your project:
46
+
47
+ ```bash
48
+ npm install react react-dom react-hook-form zod zustand lucide-react react-hot-toast \
49
+ @tiptap/react @tiptap/starter-kit @tiptap/extension-image \
50
+ @tiptap/extension-link @tiptap/extension-placeholder
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Quick start
56
+
57
+ ```tsx
58
+ import { FormEngine } from "@ainelo/form-engine";
59
+ import type { FormStructure } from "@ainelo/form-engine";
60
+
61
+ const form: FormStructure = {
62
+ name: "Contact",
63
+ provider: "Acme Corp",
64
+ description: "Simple contact form",
65
+ status: "ACTIVE",
66
+ isDeclaration: 0,
67
+ isTest: 0,
68
+ theme: { primaryColor: "#008080" },
69
+ layoutOptions: {
70
+ paginationMode: "bySection",
71
+ progressBarType: { type: "linear", visible: true },
72
+ },
73
+ sections: [
74
+ {
75
+ sectionId: "section-contact",
76
+ title: { fr: "Vos informations", en: "Your information" },
77
+ formFields: [
78
+ {
79
+ formFieldId: "f-name",
80
+ fieldName: "full_name",
81
+ fieldType: "text",
82
+ label: { fr: "Nom complet", en: "Full name" },
83
+ response: { responseValue: "" },
84
+ width: "half",
85
+ validations: [
86
+ { validationType: "required", errMsg: "Le nom est requis" },
87
+ ],
88
+ },
89
+ {
90
+ formFieldId: "f-email",
91
+ fieldName: "email",
92
+ fieldType: "text",
93
+ label: "Email",
94
+ response: { responseValue: "" },
95
+ width: "half",
96
+ validations: [
97
+ { validationType: "required", errMsg: "L'email est requis" },
98
+ { validationType: "email", errMsg: "Email invalide" },
99
+ ],
100
+ },
101
+ ],
102
+ },
103
+ ],
104
+ };
105
+
106
+ export default function App() {
107
+ return (
108
+ <FormEngine
109
+ form={form}
110
+ formId="contact-form"
111
+ onSubmit={(data) => console.log(data)}
112
+ currentLang="fr"
113
+ submitButtonText="Envoyer"
114
+ />
115
+ );
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ## `<FormEngine>` props
122
+
123
+ | Prop | Type | Required | Default | Description |
124
+ |------|------|:--------:|---------|-------------|
125
+ | `form` | `FormStructure` | ✅ | — | The complete form definition object |
126
+ | `formId` | `string` | ✅ | — | Unique instance ID — used for draft persistence |
127
+ | `onSubmit` | `(data: Record<string, any>) => void` | ✅ | — | Called when the form is valid and submitted |
128
+ | `currentLang` | `string` | | `"fr"` | Active language code for multilingual labels |
129
+ | `submitButtonText` | `string` | | `"Continuer"` | Label of the submit / next button |
130
+ | `paginationMode` | `"byFields" \| "bySection"` | | `"byFields"` | Overrides `form.layoutOptions.paginationMode` |
131
+
132
+ ---
133
+
134
+ ## Form structure reference
135
+
136
+ ### `FormStructure`
137
+
138
+ The root object passed to the `form` prop.
139
+
140
+ ```ts
141
+ {
142
+ name: string;
143
+ provider: string;
144
+ description: string;
145
+ status: "ACTIVE" | "INACTIVE";
146
+ isDeclaration: number; // 1 = official declaration, 0 = standard
147
+ isTest: number; // 1 = test, 0 = production
148
+ version?: string;
149
+ createdAt?: string; // ISO date string
150
+ updatedAt?: string; // ISO date string
151
+ sections: Section[];
152
+ theme?: FormTheme;
153
+ layoutOptions?: FormEngineLayoutOptions;
154
+ }
155
+ ```
156
+
157
+ ---
158
+
159
+ ### `FormTheme`
160
+
161
+ ```ts
162
+ {
163
+ primaryColor?: string; // Hex color — e.g. "#008080"
164
+ }
165
+ ```
166
+
167
+ ---
168
+
169
+ ### `FormEngineLayoutOptions`
170
+
171
+ ```ts
172
+ {
173
+ paginationMode?: "byFields" | "bySection";
174
+ progressBarType?: ProgressBarType;
175
+ headerOptions?: HeaderOptions;
176
+ displayDeleteButton?: boolean; // Show "clear all" button — default: true
177
+ containerStyles?: ContainerStylesOptions;
178
+ }
179
+ ```
180
+
181
+ #### `ProgressBarType`
182
+
183
+ | `type` value | Description |
184
+ |---|---|
185
+ | `"default_step"` | Classic numbered steps |
186
+ | `"linear"` | Horizontal progress bar |
187
+ | `"pastel"` | Pastel-toned step bar |
188
+ | `"custom"` | Custom colors via `colors` |
189
+ | `"section_bubble"` | Bubble indicators per section |
190
+ | `"section_bubble_pastel"` | Pastel bubble indicators |
191
+
192
+ ```ts
193
+ progressBarType: {
194
+ type: "linear",
195
+ visible: true,
196
+ colors: {
197
+ background: "#e5e7eb",
198
+ foreground: "#008080",
199
+ },
200
+ }
201
+ ```
202
+
203
+ #### `HeaderOptions`
204
+
205
+ ```ts
206
+ {
207
+ visible?: boolean;
208
+ type: "default" | "custom";
209
+ customHeader?: React.ReactNode; // Only used when type === "custom"
210
+ }
211
+ ```
212
+
213
+ #### `ContainerStylesOptions`
214
+
215
+ Customize the border and background of the form containers:
216
+
217
+ ```ts
218
+ {
219
+ mainContainer?: ContainerStyles; // Outer container (sections + buttons)
220
+ sectionContainer?: ContainerStyles; // Inner container (each section)
221
+ }
222
+
223
+ // ContainerStyles
224
+ {
225
+ borderColor?: string; // e.g. "#e5e7eb"
226
+ borderWidth?: string | number; // e.g. "1px" or 1
227
+ backgroundColor?: string; // e.g. "#f9fafb"
228
+ }
229
+ ```
230
+
231
+ ---
232
+
233
+ ### `Section`
234
+
235
+ A section groups related fields. Each section becomes a page in `"bySection"` mode.
236
+
237
+ ```ts
238
+ {
239
+ sectionId: string;
240
+ title?: string | { [lang: string]: string } | null;
241
+ subTitle?: string | { [lang: string]: string } | null;
242
+ isSpacer?: string;
243
+ position?: number;
244
+ formFields: FormField[];
245
+ conditionalDisplay?: ConditionalDisplayGroup;
246
+ }
247
+ ```
248
+
249
+ ---
250
+
251
+ ### `FormField`
252
+
253
+ The core building block. Every field in `section.formFields` follows this shape:
254
+
255
+ ```ts
256
+ {
257
+ formFieldId: string; // Unique ID — UUID recommended
258
+ fieldName: string; // Technical key — becomes the key in onSubmit data
259
+ slug?: string; // Human-readable key for URLs / exports
260
+ fieldType: FieldType; // See table below
261
+ label: string | { [lang: string]: string };
262
+ response: { responseValue: string | string[] | number | number[] | boolean | File | File[] | null };
263
+ selectOptions?: SelectOption[];
264
+ validations?: ValidationRule[];
265
+ position?: number;
266
+ placeholder?: string | { [lang: string]: string };
267
+ tooltip?: string | { [lang: string]: string };
268
+ helpText?: string | { [lang: string]: string };
269
+ example?: string | { [lang: string]: string };
270
+ width?: "full" | "half";
271
+ enableSearch?: boolean;
272
+ conditionalDisplay?: ConditionalDisplayGroup;
273
+ repeatGroup?: FormRepeatGroup;
274
+ repeatRule?: RepeatRule;
275
+ fileToBucketManage?: FileToBucketManage;
276
+ otpFormFieldExecOptions?: OTPFormFieldExecOptions;
277
+ emailFormFieldExecOptions?: EmailFormFieldExecOptions;
278
+ detailInfos?: DetailInfosConfig;
279
+ dynamicFilterRule?: DynamicFilterRule;
280
+ flattenOnSubmit?: boolean;
281
+ }
282
+ ```
283
+
284
+ #### Field types
285
+
286
+ | `fieldType` | UI rendered | Notes |
287
+ |---|---|---|
288
+ | `"text"` | Text input | Also used for email with validation |
289
+ | `"textarea"` | Multi-line input | |
290
+ | `"dropdown"` | Select list | Requires `selectOptions`. Supports `enableSearch` |
291
+ | `"radio"` | Radio buttons | Requires `selectOptions` |
292
+ | `"checkbox"` | Checkboxes | Requires `selectOptions` |
293
+ | `"date"` | Date picker | |
294
+ | `"number"` | Numeric input | |
295
+ | `"bool"` | Toggle / switch | |
296
+ | `"file"` | File upload | Generic |
297
+ | `"password"` | Password input | Masked |
298
+ | `"IMAGE"` | Image upload | Preview included |
299
+ | `"VIDEO"` | Video upload | |
300
+ | `"AUDIO"` | Audio upload | |
301
+ | `"DOCUMENT"` | Document upload | |
302
+ | `"PDF"` | PDF upload | |
303
+ | `"VOICE"` | Voice recording | Browser mic |
304
+ | `"TIP_TAP_DOC_TEXT"` | Rich text editor | TipTap-powered |
305
+ | `"OTP"` | OTP code input | Requires `otpFormFieldExecOptions` |
306
+
307
+ ---
308
+
309
+ ### `SelectOption`
310
+
311
+ Used with `dropdown`, `radio`, and `checkbox` fields:
312
+
313
+ ```ts
314
+ {
315
+ value: string;
316
+ label: string | { [lang: string]: string };
317
+ points?: number; // Score / weight for quiz-like forms
318
+ icon?: string; // Icon name or URL
319
+ }
320
+ ```
321
+
322
+ ---
323
+
324
+ ### `ValidationRule`
325
+
326
+ ```ts
327
+ {
328
+ validationType?: ValidationType;
329
+ value?: number | string | string[];
330
+ errMsg: string; // Error message displayed to the user
331
+ }
332
+ ```
333
+
334
+ #### Validation types
335
+
336
+ | `validationType` | `value` type | Description |
337
+ |---|---|---|
338
+ | `"required"` | — | Field must not be empty |
339
+ | `"minLength"` | `number` | Minimum character count |
340
+ | `"maxLength"` | `number` | Maximum character count |
341
+ | `"regex"` | `string` | Must match the given pattern |
342
+ | `"email"` | — | Must be a valid email address |
343
+ | `"number"` | — | Must be a valid number |
344
+ | `"fileSize"` | `number` | Max file size in bytes |
345
+ | `"phone_number"` | — | Must be a valid phone number |
346
+ | `"fileType"` | `string[]` | Allowed MIME types — e.g. `["image/png", "image/jpeg"]` |
347
+
348
+ ---
349
+
350
+ ## Conditional display
351
+
352
+ Show or hide a section or field based on the values of other fields.
353
+
354
+ ```ts
355
+ {
356
+ rules: ConditionalDisplayRule | ConditionalDisplayRule[];
357
+ logic?: "AND" | "OR"; // Default: "AND"
358
+ }
359
+ ```
360
+
361
+ ### `ConditionalDisplayRule`
362
+
363
+ ```ts
364
+ {
365
+ fieldName?: string | string[]; // Target by technical key
366
+ formFieldId?: string | string[]; // Target by UUID
367
+ operator?: ConditionalOperator;
368
+ value?: string | number | boolean | string[] | number[];
369
+ numeric_compare_to?: number; // For aggregation comparisons
370
+ typeComputation?: "SOMME" | "MULTIPLICATION";
371
+ }
372
+ ```
373
+
374
+ ### Operators
375
+
376
+ `"=="` · `"!="` · `">"` · `"<"` · `"IN"` · `"NOT IN"` · `"CONTAINS"` · `"NOT CONTAINS"`
377
+
378
+ ### Examples
379
+
380
+ ```ts
381
+ // Show field only if "has_vehicle" equals "yes"
382
+ conditionalDisplay: {
383
+ rules: { fieldName: "has_vehicle", operator: "==", value: "yes" },
384
+ }
385
+
386
+ // Show section if country is France OR Belgium
387
+ conditionalDisplay: {
388
+ rules: { fieldName: "country", operator: "IN", value: ["FR", "BE"] },
389
+ }
390
+
391
+ // Show if score1 + score2 > 10
392
+ conditionalDisplay: {
393
+ rules: {
394
+ fieldName: ["score1", "score2"],
395
+ typeComputation: "SOMME",
396
+ operator: ">",
397
+ numeric_compare_to: 10,
398
+ },
399
+ }
400
+ ```
401
+
402
+ ---
403
+
404
+ ## Repeat groups
405
+
406
+ A `FormRepeatGroup` defines a block of fields that users can repeat N times (e.g. list of children, co-applicants).
407
+
408
+ ```ts
409
+ // On FormField
410
+ repeatGroup: {
411
+ repeatGroupId: string;
412
+ fieldName: string;
413
+ label: string | { [lang: string]: string };
414
+ minRepeats?: number;
415
+ maxRepeats?: number;
416
+ initialRepeats?: number;
417
+ formFields: FormField[];
418
+ position?: number;
419
+ conditionalDisplay?: ConditionalDisplayGroup;
420
+ }
421
+
422
+ repeatRule: {
423
+ min?: number;
424
+ max?: number;
425
+ prefillEmpty?: boolean; // Pre-fill one empty instance on initial render
426
+ }
427
+ ```
428
+
429
+ ---
430
+
431
+ ## OTP field
432
+
433
+ ```ts
434
+ {
435
+ formFieldId: "f-otp",
436
+ fieldName: "otp_code",
437
+ fieldType: "OTP",
438
+ label: "Code de vérification",
439
+ response: { responseValue: "" },
440
+ otpFormFieldExecOptions: {
441
+ serverDns: "https://api.example.com",
442
+ postApiEndPoint: "/api/otp/verify",
443
+ otpLength: 6,
444
+ linkedEmailFieldName: "email", // Triggers OTP send when this field is filled
445
+ sendOTPApiConfig: {
446
+ serverDns: "https://api.example.com",
447
+ postApiEndPoint: "/api/otp/send",
448
+ bearer: "my-token",
449
+ },
450
+ autoValidate: true,
451
+ enableResend: true,
452
+ resendCooldownSeconds: 60,
453
+ },
454
+ }
455
+ ```
456
+
457
+ To trigger OTP send automatically from the email field, add `emailFormFieldExecOptions` to it:
458
+
459
+ ```ts
460
+ {
461
+ formFieldId: "f-email",
462
+ fieldName: "email",
463
+ fieldType: "text",
464
+ label: "Email",
465
+ response: { responseValue: "" },
466
+ validations: [{ validationType: "email", errMsg: "Email invalide" }],
467
+ emailFormFieldExecOptions: {
468
+ triggerOTPSend: true,
469
+ linkedOTPFieldName: "otp_code",
470
+ otpSendApiConfig: {
471
+ serverDns: "https://api.example.com",
472
+ postApiEndPoint: "/api/otp/send",
473
+ },
474
+ },
475
+ }
476
+ ```
477
+
478
+ ---
479
+
480
+ ## File uploads to a bucket
481
+
482
+ Add `fileToBucketManage` to any file-type field to automatically upload on selection and delete on removal:
483
+
484
+ ```ts
485
+ fileToBucketManage: {
486
+ uploadOption: {
487
+ serverDns: "https://bucket.example.com",
488
+ postApiEndPoint: "/api/files/upload-file",
489
+ payload: { secret_key: "my-secret", folder_name: "avatars" },
490
+ bearer: "my-token",
491
+ timeoutMs: 30000,
492
+ },
493
+ deleteOption: {
494
+ serverDns: "https://bucket.example.com",
495
+ deleteApiEndPoint: "/api/files/delete-file",
496
+ payload: { secret_key: "my-secret", folder_name: "avatars" },
497
+ },
498
+ }
499
+ ```
500
+
501
+ ---
502
+
503
+ ## Dynamic filter (cascading dropdowns)
504
+
505
+ Filter a dropdown's options based on another field's selected value:
506
+
507
+ ```ts
508
+ {
509
+ formFieldId: "f-city",
510
+ fieldName: "city",
511
+ fieldType: "dropdown",
512
+ label: "Ville",
513
+ response: { responseValue: "" },
514
+ dynamicFilterRule: {
515
+ dependentFieldName: "country", // When "country" changes, reload city options
516
+ filterType: "exact",
517
+ dataSource: {
518
+ type: "static",
519
+ data: {
520
+ FR: [
521
+ { value: "paris", label: "Paris" },
522
+ { value: "lyon", label: "Lyon" },
523
+ ],
524
+ BE: [
525
+ { value: "bruxelles", label: "Bruxelles" },
526
+ { value: "liege", label: "Liège" },
527
+ ],
528
+ },
529
+ },
530
+ },
531
+ }
532
+ ```
533
+
534
+ ---
535
+
536
+ ## Detail fields on selection
537
+
538
+ Automatically reveal additional fields when the user selects a specific option:
539
+
540
+ ```ts
541
+ {
542
+ formFieldId: "f-sector",
543
+ fieldName: "sector",
544
+ fieldType: "dropdown",
545
+ label: "Secteur",
546
+ response: { responseValue: "" },
547
+ selectOptions: [
548
+ { value: "agri", label: "Agriculture" },
549
+ { value: "other", label: "Autre" },
550
+ ],
551
+ detailInfos: {
552
+ other: {
553
+ label: "Précisez votre secteur",
554
+ formFields: [
555
+ {
556
+ formFieldId: "f-sector-detail",
557
+ fieldName: "sector_detail",
558
+ fieldType: "text",
559
+ label: "Secteur exact",
560
+ response: { responseValue: "" },
561
+ },
562
+ ],
563
+ },
564
+ },
565
+ }
566
+ ```
567
+
568
+ ---
569
+
570
+ ## Multilingual support
571
+
572
+ Every `label`, `placeholder`, `tooltip`, `helpText`, and `example` field accepts either a plain string or a language map:
573
+
574
+ ```ts
575
+ label: { fr: "Nom complet", en: "Full name", es: "Nombre completo" }
576
+ ```
577
+
578
+ Pass `currentLang="en"` to `<FormEngine>` to switch the active language at runtime.
579
+
580
+ ---
581
+
582
+ ## What `onSubmit` receives
583
+
584
+ 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.
585
+
586
+ ```ts
587
+ // Example output
588
+ {
589
+ full_name: "Alice Dupont",
590
+ email: "alice@example.com",
591
+ sector: "other",
592
+ sector_detail: "Fintech",
593
+ passengers: [
594
+ { passenger_name: "Bob", passenger_age: 32 },
595
+ { passenger_name: "Carol", passenger_age: 28 },
596
+ ],
597
+ id_document: "https://bucket.example.com/files/doc-abc123.pdf",
598
+ }
599
+ ```
600
+
601
+ ---
602
+
603
+ ## License
604
+
605
+ 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.4",
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",