@ainelo/form-engine 1.0.4 → 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.
- package/README.md +264 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,28 @@
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
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
|
+
|
|
16
38
|
## Overview
|
|
17
39
|
|
|
18
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.
|
|
@@ -600,6 +622,248 @@ The `onSubmit` callback receives a flat `Record<string, any>` where each key is
|
|
|
600
622
|
|
|
601
623
|
---
|
|
602
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
|
+
|
|
603
867
|
## License
|
|
604
868
|
|
|
605
869
|
MIT © [Lionel TOTON](mailto:totonlionel@gmail.com)
|
package/package.json
CHANGED