react-form-atlas-engine 1.1.0
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 +92 -0
- package/dist/index.d.mts +174 -0
- package/dist/index.d.ts +174 -0
- package/dist/index.js +457 -0
- package/dist/index.mjs +427 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# react-form-atlas-engine
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/react-form-atlas-engine)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
**The Brain of Your Forms.** Framework-agnostic graph-based form engine that eliminates "condition hell" in complex multi-step flows.
|
|
8
|
+
|
|
9
|
+
## 🚀 Why Graph-Based?
|
|
10
|
+
|
|
11
|
+
Traditional linear forms (`[Step1, Step2, Step3]`) break when logic gets complex. **React Form Atlas** treats your form as a **Directed Acyclic Graph (DAG)**.
|
|
12
|
+
|
|
13
|
+
```mermaid
|
|
14
|
+
graph LR
|
|
15
|
+
Start((Start)) --> UserType{User Type?}
|
|
16
|
+
UserType -->|Business| Biz[Business Details]
|
|
17
|
+
UserType -->|Individual| Pers[Personal Details]
|
|
18
|
+
Biz --> Tax[Tax Info]
|
|
19
|
+
Pers --> Complete((Complete))
|
|
20
|
+
Tax --> Complete
|
|
21
|
+
style Start fill:#f9f,stroke:#333,stroke-width:2px
|
|
22
|
+
style Complete fill:#9f9,stroke:#333,stroke-width:2px
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| Feature | ❌ Linear Forms | ✅ React Form Atlas (Graph) |
|
|
26
|
+
| :--- | :--- | :--- |
|
|
27
|
+
| **Logic** | Nested `if/else` spaghetti | Declarative Edges |
|
|
28
|
+
| **Navigation** | Hardcoded implementation | Auto-computed Paths |
|
|
29
|
+
| **Progress** | `Step / Total` (Inaccurate) | Weighted Path Calculation |
|
|
30
|
+
| **Validation** | Field-level only | Predictive Path Validation |
|
|
31
|
+
|
|
32
|
+
## 📦 Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install react-form-atlas-engine
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## ⚡ Quick Start
|
|
39
|
+
|
|
40
|
+
[](https://stackblitz.com/edit/node?file=index.js&dependencies=react-form-atlas-engine)
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
import { FormEngine } from 'react-form-atlas-engine';
|
|
44
|
+
|
|
45
|
+
// 1. Define your map (Schema)
|
|
46
|
+
const schema = {
|
|
47
|
+
id: 'onboarding-flow',
|
|
48
|
+
initial: 'welcome',
|
|
49
|
+
states: {
|
|
50
|
+
welcome: {
|
|
51
|
+
on: { NEXT: 'userType' }
|
|
52
|
+
},
|
|
53
|
+
userType: {
|
|
54
|
+
on: {
|
|
55
|
+
BUSINESS: 'businessDetails',
|
|
56
|
+
INDIVIDUAL: 'personalDetails'
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
businessDetails: { on: { NEXT: 'complete' } },
|
|
60
|
+
personalDetails: { on: { NEXT: 'complete' } },
|
|
61
|
+
complete: { type: 'final' }
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// 2. Initialize the engine
|
|
66
|
+
const engine = new FormEngine({
|
|
67
|
+
schema,
|
|
68
|
+
autoSave: true, // Auto-saves to IndexedDB
|
|
69
|
+
onComplete: (data) => console.log('🎉 Form Completed:', data)
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await engine.start();
|
|
73
|
+
|
|
74
|
+
// 3. Drive!
|
|
75
|
+
console.log(engine.getCurrentState()); // 'welcome'
|
|
76
|
+
await engine.transition('NEXT');
|
|
77
|
+
console.log(engine.getCurrentState()); // 'userType'
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 📚 Documentation
|
|
81
|
+
|
|
82
|
+
- [**Core Concepts**](https://github.com/Mehulbirare/react-form/blob/main/docs/core-concepts.md) - Learn about Nodes, Edges, and Travelers.
|
|
83
|
+
- [**API Reference**](https://github.com/Mehulbirare/react-form/blob/main/docs/api-reference.md) - Full method documentation.
|
|
84
|
+
- [**Examples**](https://github.com/Mehulbirare/react-form/tree/main/examples) - Real-world usage.
|
|
85
|
+
|
|
86
|
+
## 📄 License
|
|
87
|
+
|
|
88
|
+
MIT © [Mehul Birare](https://github.com/Mehulbirare)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for React Form Atlas
|
|
3
|
+
*/
|
|
4
|
+
type TransitionEvent = string;
|
|
5
|
+
interface FormState {
|
|
6
|
+
id: string;
|
|
7
|
+
on?: Record<TransitionEvent, string | ConditionalTransition>;
|
|
8
|
+
meta?: {
|
|
9
|
+
weight?: number;
|
|
10
|
+
validation?: ValidationRule[];
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
interface ConditionalTransition {
|
|
15
|
+
target: string;
|
|
16
|
+
cond?: (context: FormContext) => boolean;
|
|
17
|
+
}
|
|
18
|
+
interface FormSchema {
|
|
19
|
+
id: string;
|
|
20
|
+
initial: string;
|
|
21
|
+
states: Record<string, FormState>;
|
|
22
|
+
context?: Record<string, any>;
|
|
23
|
+
}
|
|
24
|
+
interface FormContext {
|
|
25
|
+
[key: string]: any;
|
|
26
|
+
}
|
|
27
|
+
interface ValidationRule {
|
|
28
|
+
type: 'required' | 'email' | 'min' | 'max' | 'pattern' | 'custom';
|
|
29
|
+
message: string;
|
|
30
|
+
value?: any;
|
|
31
|
+
validator?: (value: any, context: FormContext) => boolean;
|
|
32
|
+
}
|
|
33
|
+
interface ValidationResult {
|
|
34
|
+
valid: boolean;
|
|
35
|
+
errors: string[];
|
|
36
|
+
}
|
|
37
|
+
interface StepChangeEvent {
|
|
38
|
+
from: string;
|
|
39
|
+
to: string;
|
|
40
|
+
context: FormContext;
|
|
41
|
+
}
|
|
42
|
+
interface FormEngineOptions {
|
|
43
|
+
schema: FormSchema;
|
|
44
|
+
autoSave?: boolean;
|
|
45
|
+
storageKey?: string;
|
|
46
|
+
onStepChange?: (event: StepChangeEvent) => void;
|
|
47
|
+
onComplete?: (context: FormContext) => void;
|
|
48
|
+
onError?: (error: Error) => void;
|
|
49
|
+
}
|
|
50
|
+
interface FormEngineState {
|
|
51
|
+
currentState: string;
|
|
52
|
+
context: FormContext;
|
|
53
|
+
history: string[];
|
|
54
|
+
completedSteps: Set<string>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Form Engine - The core state machine for graph-based forms
|
|
59
|
+
*/
|
|
60
|
+
declare class FormEngine {
|
|
61
|
+
private schema;
|
|
62
|
+
private state;
|
|
63
|
+
private storage;
|
|
64
|
+
private listeners;
|
|
65
|
+
private options;
|
|
66
|
+
constructor(options: FormEngineOptions);
|
|
67
|
+
/**
|
|
68
|
+
* Start the form engine
|
|
69
|
+
*/
|
|
70
|
+
start(): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Transition to the next state based on an event
|
|
73
|
+
*/
|
|
74
|
+
transition(event: string, data?: any): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Go back to the previous state
|
|
77
|
+
*/
|
|
78
|
+
back(): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Update context without transitioning
|
|
81
|
+
*/
|
|
82
|
+
updateContext(data: Partial<FormContext>): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Get current progress percentage
|
|
85
|
+
*/
|
|
86
|
+
getProgress(): number;
|
|
87
|
+
/**
|
|
88
|
+
* Get current state
|
|
89
|
+
*/
|
|
90
|
+
getCurrentState(): string;
|
|
91
|
+
/**
|
|
92
|
+
* Get current context
|
|
93
|
+
*/
|
|
94
|
+
getContext(): FormContext;
|
|
95
|
+
/**
|
|
96
|
+
* Get possible next states
|
|
97
|
+
*/
|
|
98
|
+
getPossibleNextStates(): string[];
|
|
99
|
+
/**
|
|
100
|
+
* Reset the form
|
|
101
|
+
*/
|
|
102
|
+
reset(): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Event emitter
|
|
105
|
+
*/
|
|
106
|
+
on(event: string, callback: Function): void;
|
|
107
|
+
/**
|
|
108
|
+
* Remove event listener
|
|
109
|
+
*/
|
|
110
|
+
off(event: string, callback: Function): void;
|
|
111
|
+
/**
|
|
112
|
+
* Emit event
|
|
113
|
+
*/
|
|
114
|
+
private emit;
|
|
115
|
+
/**
|
|
116
|
+
* Get all possible paths from current state
|
|
117
|
+
*/
|
|
118
|
+
getPossiblePaths(): string[][];
|
|
119
|
+
/**
|
|
120
|
+
* Check if we can go back
|
|
121
|
+
*/
|
|
122
|
+
canGoBack(): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Get the full state (for debugging)
|
|
125
|
+
*/
|
|
126
|
+
getState(): FormEngineState;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Storage adapter for persisting form state
|
|
131
|
+
*/
|
|
132
|
+
declare class StorageAdapter {
|
|
133
|
+
private storageKey;
|
|
134
|
+
private useIndexedDB;
|
|
135
|
+
constructor(storageKey?: string);
|
|
136
|
+
save(state: FormEngineState): Promise<void>;
|
|
137
|
+
load(): Promise<FormEngineState | null>;
|
|
138
|
+
clear(): Promise<void>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Validator for form fields and paths
|
|
143
|
+
*/
|
|
144
|
+
declare class Validator {
|
|
145
|
+
/**
|
|
146
|
+
* Validate a single field value
|
|
147
|
+
*/
|
|
148
|
+
static validateField(value: any, rules: ValidationRule[], context: FormContext): ValidationResult;
|
|
149
|
+
/**
|
|
150
|
+
* Predictive validation: Check if the current path is still valid
|
|
151
|
+
* This prevents users from entering data that makes future steps impossible
|
|
152
|
+
*/
|
|
153
|
+
static validatePath(currentState: string, context: FormContext, schema: any): ValidationResult;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Calculate the total weight of a path through the form graph
|
|
158
|
+
*/
|
|
159
|
+
declare class PathCalculator {
|
|
160
|
+
/**
|
|
161
|
+
* Calculate the total possible weight from current state to completion
|
|
162
|
+
*/
|
|
163
|
+
static calculateRemainingWeight(currentState: string, schema: FormSchema, visited?: Set<string>): number;
|
|
164
|
+
/**
|
|
165
|
+
* Calculate progress percentage based on completed path weight
|
|
166
|
+
*/
|
|
167
|
+
static calculateProgress(currentState: string, completedSteps: Set<string>, schema: FormSchema): number;
|
|
168
|
+
/**
|
|
169
|
+
* Get all possible paths from current state
|
|
170
|
+
*/
|
|
171
|
+
static getPossiblePaths(currentState: string, schema: FormSchema, context?: any): string[][];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { type ConditionalTransition, type FormContext, FormEngine, type FormEngineOptions, type FormEngineState, type FormSchema, type FormState, PathCalculator, type StepChangeEvent, StorageAdapter, type TransitionEvent, type ValidationResult, type ValidationRule, Validator };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for React Form Atlas
|
|
3
|
+
*/
|
|
4
|
+
type TransitionEvent = string;
|
|
5
|
+
interface FormState {
|
|
6
|
+
id: string;
|
|
7
|
+
on?: Record<TransitionEvent, string | ConditionalTransition>;
|
|
8
|
+
meta?: {
|
|
9
|
+
weight?: number;
|
|
10
|
+
validation?: ValidationRule[];
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
interface ConditionalTransition {
|
|
15
|
+
target: string;
|
|
16
|
+
cond?: (context: FormContext) => boolean;
|
|
17
|
+
}
|
|
18
|
+
interface FormSchema {
|
|
19
|
+
id: string;
|
|
20
|
+
initial: string;
|
|
21
|
+
states: Record<string, FormState>;
|
|
22
|
+
context?: Record<string, any>;
|
|
23
|
+
}
|
|
24
|
+
interface FormContext {
|
|
25
|
+
[key: string]: any;
|
|
26
|
+
}
|
|
27
|
+
interface ValidationRule {
|
|
28
|
+
type: 'required' | 'email' | 'min' | 'max' | 'pattern' | 'custom';
|
|
29
|
+
message: string;
|
|
30
|
+
value?: any;
|
|
31
|
+
validator?: (value: any, context: FormContext) => boolean;
|
|
32
|
+
}
|
|
33
|
+
interface ValidationResult {
|
|
34
|
+
valid: boolean;
|
|
35
|
+
errors: string[];
|
|
36
|
+
}
|
|
37
|
+
interface StepChangeEvent {
|
|
38
|
+
from: string;
|
|
39
|
+
to: string;
|
|
40
|
+
context: FormContext;
|
|
41
|
+
}
|
|
42
|
+
interface FormEngineOptions {
|
|
43
|
+
schema: FormSchema;
|
|
44
|
+
autoSave?: boolean;
|
|
45
|
+
storageKey?: string;
|
|
46
|
+
onStepChange?: (event: StepChangeEvent) => void;
|
|
47
|
+
onComplete?: (context: FormContext) => void;
|
|
48
|
+
onError?: (error: Error) => void;
|
|
49
|
+
}
|
|
50
|
+
interface FormEngineState {
|
|
51
|
+
currentState: string;
|
|
52
|
+
context: FormContext;
|
|
53
|
+
history: string[];
|
|
54
|
+
completedSteps: Set<string>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Form Engine - The core state machine for graph-based forms
|
|
59
|
+
*/
|
|
60
|
+
declare class FormEngine {
|
|
61
|
+
private schema;
|
|
62
|
+
private state;
|
|
63
|
+
private storage;
|
|
64
|
+
private listeners;
|
|
65
|
+
private options;
|
|
66
|
+
constructor(options: FormEngineOptions);
|
|
67
|
+
/**
|
|
68
|
+
* Start the form engine
|
|
69
|
+
*/
|
|
70
|
+
start(): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Transition to the next state based on an event
|
|
73
|
+
*/
|
|
74
|
+
transition(event: string, data?: any): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Go back to the previous state
|
|
77
|
+
*/
|
|
78
|
+
back(): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Update context without transitioning
|
|
81
|
+
*/
|
|
82
|
+
updateContext(data: Partial<FormContext>): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Get current progress percentage
|
|
85
|
+
*/
|
|
86
|
+
getProgress(): number;
|
|
87
|
+
/**
|
|
88
|
+
* Get current state
|
|
89
|
+
*/
|
|
90
|
+
getCurrentState(): string;
|
|
91
|
+
/**
|
|
92
|
+
* Get current context
|
|
93
|
+
*/
|
|
94
|
+
getContext(): FormContext;
|
|
95
|
+
/**
|
|
96
|
+
* Get possible next states
|
|
97
|
+
*/
|
|
98
|
+
getPossibleNextStates(): string[];
|
|
99
|
+
/**
|
|
100
|
+
* Reset the form
|
|
101
|
+
*/
|
|
102
|
+
reset(): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Event emitter
|
|
105
|
+
*/
|
|
106
|
+
on(event: string, callback: Function): void;
|
|
107
|
+
/**
|
|
108
|
+
* Remove event listener
|
|
109
|
+
*/
|
|
110
|
+
off(event: string, callback: Function): void;
|
|
111
|
+
/**
|
|
112
|
+
* Emit event
|
|
113
|
+
*/
|
|
114
|
+
private emit;
|
|
115
|
+
/**
|
|
116
|
+
* Get all possible paths from current state
|
|
117
|
+
*/
|
|
118
|
+
getPossiblePaths(): string[][];
|
|
119
|
+
/**
|
|
120
|
+
* Check if we can go back
|
|
121
|
+
*/
|
|
122
|
+
canGoBack(): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Get the full state (for debugging)
|
|
125
|
+
*/
|
|
126
|
+
getState(): FormEngineState;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Storage adapter for persisting form state
|
|
131
|
+
*/
|
|
132
|
+
declare class StorageAdapter {
|
|
133
|
+
private storageKey;
|
|
134
|
+
private useIndexedDB;
|
|
135
|
+
constructor(storageKey?: string);
|
|
136
|
+
save(state: FormEngineState): Promise<void>;
|
|
137
|
+
load(): Promise<FormEngineState | null>;
|
|
138
|
+
clear(): Promise<void>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Validator for form fields and paths
|
|
143
|
+
*/
|
|
144
|
+
declare class Validator {
|
|
145
|
+
/**
|
|
146
|
+
* Validate a single field value
|
|
147
|
+
*/
|
|
148
|
+
static validateField(value: any, rules: ValidationRule[], context: FormContext): ValidationResult;
|
|
149
|
+
/**
|
|
150
|
+
* Predictive validation: Check if the current path is still valid
|
|
151
|
+
* This prevents users from entering data that makes future steps impossible
|
|
152
|
+
*/
|
|
153
|
+
static validatePath(currentState: string, context: FormContext, schema: any): ValidationResult;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Calculate the total weight of a path through the form graph
|
|
158
|
+
*/
|
|
159
|
+
declare class PathCalculator {
|
|
160
|
+
/**
|
|
161
|
+
* Calculate the total possible weight from current state to completion
|
|
162
|
+
*/
|
|
163
|
+
static calculateRemainingWeight(currentState: string, schema: FormSchema, visited?: Set<string>): number;
|
|
164
|
+
/**
|
|
165
|
+
* Calculate progress percentage based on completed path weight
|
|
166
|
+
*/
|
|
167
|
+
static calculateProgress(currentState: string, completedSteps: Set<string>, schema: FormSchema): number;
|
|
168
|
+
/**
|
|
169
|
+
* Get all possible paths from current state
|
|
170
|
+
*/
|
|
171
|
+
static getPossiblePaths(currentState: string, schema: FormSchema, context?: any): string[][];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { type ConditionalTransition, type FormContext, FormEngine, type FormEngineOptions, type FormEngineState, type FormSchema, type FormState, PathCalculator, type StepChangeEvent, StorageAdapter, type TransitionEvent, type ValidationResult, type ValidationRule, Validator };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FormEngine: () => FormEngine,
|
|
24
|
+
PathCalculator: () => PathCalculator,
|
|
25
|
+
StorageAdapter: () => StorageAdapter,
|
|
26
|
+
Validator: () => Validator
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/storage.ts
|
|
31
|
+
var import_idb_keyval = require("idb-keyval");
|
|
32
|
+
var StorageAdapter = class {
|
|
33
|
+
constructor(storageKey = "React Form Atlas-state") {
|
|
34
|
+
this.storageKey = storageKey;
|
|
35
|
+
this.useIndexedDB = typeof indexedDB !== "undefined";
|
|
36
|
+
}
|
|
37
|
+
async save(state) {
|
|
38
|
+
try {
|
|
39
|
+
const serialized = JSON.stringify({
|
|
40
|
+
...state,
|
|
41
|
+
completedSteps: Array.from(state.completedSteps)
|
|
42
|
+
});
|
|
43
|
+
if (this.useIndexedDB) {
|
|
44
|
+
await (0, import_idb_keyval.set)(this.storageKey, serialized);
|
|
45
|
+
} else {
|
|
46
|
+
localStorage.setItem(this.storageKey, serialized);
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error("Failed to save form state:", error);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async load() {
|
|
54
|
+
try {
|
|
55
|
+
let serialized = null;
|
|
56
|
+
if (this.useIndexedDB) {
|
|
57
|
+
const result = await (0, import_idb_keyval.get)(this.storageKey);
|
|
58
|
+
serialized = result;
|
|
59
|
+
} else {
|
|
60
|
+
serialized = localStorage.getItem(this.storageKey);
|
|
61
|
+
}
|
|
62
|
+
if (!serialized) return null;
|
|
63
|
+
const parsed = JSON.parse(serialized);
|
|
64
|
+
return {
|
|
65
|
+
...parsed,
|
|
66
|
+
completedSteps: new Set(parsed.completedSteps)
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error("Failed to load form state:", error);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async clear() {
|
|
74
|
+
try {
|
|
75
|
+
if (this.useIndexedDB) {
|
|
76
|
+
await (0, import_idb_keyval.del)(this.storageKey);
|
|
77
|
+
} else {
|
|
78
|
+
localStorage.removeItem(this.storageKey);
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error("Failed to clear form state:", error);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/validator.ts
|
|
88
|
+
var Validator = class {
|
|
89
|
+
/**
|
|
90
|
+
* Validate a single field value
|
|
91
|
+
*/
|
|
92
|
+
static validateField(value, rules, context) {
|
|
93
|
+
const errors = [];
|
|
94
|
+
for (const rule of rules) {
|
|
95
|
+
switch (rule.type) {
|
|
96
|
+
case "required":
|
|
97
|
+
if (value === void 0 || value === null || value === "") {
|
|
98
|
+
errors.push(rule.message);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
case "email":
|
|
102
|
+
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
103
|
+
errors.push(rule.message);
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
case "min":
|
|
107
|
+
if (typeof value === "number" && value < rule.value) {
|
|
108
|
+
errors.push(rule.message);
|
|
109
|
+
} else if (typeof value === "string" && value.length < rule.value) {
|
|
110
|
+
errors.push(rule.message);
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
case "max":
|
|
114
|
+
if (typeof value === "number" && value > rule.value) {
|
|
115
|
+
errors.push(rule.message);
|
|
116
|
+
} else if (typeof value === "string" && value.length > rule.value) {
|
|
117
|
+
errors.push(rule.message);
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
case "pattern":
|
|
121
|
+
if (value && !new RegExp(rule.value).test(value)) {
|
|
122
|
+
errors.push(rule.message);
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
case "custom":
|
|
126
|
+
if (rule.validator && !rule.validator(value, context)) {
|
|
127
|
+
errors.push(rule.message);
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
valid: errors.length === 0,
|
|
134
|
+
errors
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Predictive validation: Check if the current path is still valid
|
|
139
|
+
* This prevents users from entering data that makes future steps impossible
|
|
140
|
+
*/
|
|
141
|
+
static validatePath(currentState, context, schema) {
|
|
142
|
+
const errors = [];
|
|
143
|
+
const state = schema.states[currentState];
|
|
144
|
+
if (state?.meta?.validation) {
|
|
145
|
+
const result = this.validateField(
|
|
146
|
+
context[currentState],
|
|
147
|
+
state.meta.validation,
|
|
148
|
+
context
|
|
149
|
+
);
|
|
150
|
+
errors.push(...result.errors);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
valid: errors.length === 0,
|
|
154
|
+
errors
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/path-calculator.ts
|
|
160
|
+
var PathCalculator = class {
|
|
161
|
+
/**
|
|
162
|
+
* Calculate the total possible weight from current state to completion
|
|
163
|
+
*/
|
|
164
|
+
static calculateRemainingWeight(currentState, schema, visited = /* @__PURE__ */ new Set()) {
|
|
165
|
+
if (visited.has(currentState)) {
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
visited.add(currentState);
|
|
169
|
+
const state = schema.states[currentState];
|
|
170
|
+
if (!state || !state.on) {
|
|
171
|
+
return state?.meta?.weight || 1;
|
|
172
|
+
}
|
|
173
|
+
const currentWeight = state.meta?.weight || 1;
|
|
174
|
+
const transitions = Object.values(state.on);
|
|
175
|
+
if (transitions.length === 0) {
|
|
176
|
+
return currentWeight;
|
|
177
|
+
}
|
|
178
|
+
const maxFutureWeight = Math.max(
|
|
179
|
+
...transitions.map((transition) => {
|
|
180
|
+
const target = typeof transition === "string" ? transition : transition.target;
|
|
181
|
+
return this.calculateRemainingWeight(target, schema, new Set(visited));
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
return currentWeight + maxFutureWeight;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Calculate progress percentage based on completed path weight
|
|
188
|
+
*/
|
|
189
|
+
static calculateProgress(currentState, completedSteps, schema) {
|
|
190
|
+
const totalWeight = this.calculateRemainingWeight(schema.initial, schema);
|
|
191
|
+
let completedWeight = 0;
|
|
192
|
+
for (const step of completedSteps) {
|
|
193
|
+
const state = schema.states[step];
|
|
194
|
+
completedWeight += state?.meta?.weight || 1;
|
|
195
|
+
}
|
|
196
|
+
return totalWeight > 0 ? completedWeight / totalWeight * 100 : 0;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Get all possible paths from current state
|
|
200
|
+
*/
|
|
201
|
+
static getPossiblePaths(currentState, schema, context = {}) {
|
|
202
|
+
const paths = [];
|
|
203
|
+
const visited = /* @__PURE__ */ new Set();
|
|
204
|
+
const traverse = (state, currentPath) => {
|
|
205
|
+
if (visited.has(state)) return;
|
|
206
|
+
const stateConfig = schema.states[state];
|
|
207
|
+
if (!stateConfig) return;
|
|
208
|
+
const newPath = [...currentPath, state];
|
|
209
|
+
if (!stateConfig.on || Object.keys(stateConfig.on).length === 0) {
|
|
210
|
+
paths.push(newPath);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
visited.add(state);
|
|
214
|
+
for (const transition of Object.values(stateConfig.on)) {
|
|
215
|
+
const target = typeof transition === "string" ? transition : transition.target;
|
|
216
|
+
if (typeof transition === "object" && transition.cond) {
|
|
217
|
+
if (!transition.cond(context)) continue;
|
|
218
|
+
}
|
|
219
|
+
traverse(target, newPath);
|
|
220
|
+
}
|
|
221
|
+
visited.delete(state);
|
|
222
|
+
};
|
|
223
|
+
traverse(currentState, []);
|
|
224
|
+
return paths;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// src/engine.ts
|
|
229
|
+
var FormEngine = class {
|
|
230
|
+
constructor(options) {
|
|
231
|
+
this.storage = null;
|
|
232
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
233
|
+
this.options = options;
|
|
234
|
+
this.schema = options.schema;
|
|
235
|
+
this.state = {
|
|
236
|
+
currentState: this.schema.initial,
|
|
237
|
+
context: this.schema.context || {},
|
|
238
|
+
history: [],
|
|
239
|
+
completedSteps: /* @__PURE__ */ new Set()
|
|
240
|
+
};
|
|
241
|
+
if (options.autoSave) {
|
|
242
|
+
this.storage = new StorageAdapter(options.storageKey);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Start the form engine
|
|
247
|
+
*/
|
|
248
|
+
async start() {
|
|
249
|
+
if (this.storage) {
|
|
250
|
+
const savedState = await this.storage.load();
|
|
251
|
+
if (savedState) {
|
|
252
|
+
this.state = savedState;
|
|
253
|
+
this.emit("resumed", { state: this.state });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
this.emit("started", { state: this.state });
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Transition to the next state based on an event
|
|
260
|
+
*/
|
|
261
|
+
async transition(event, data) {
|
|
262
|
+
const currentStateConfig = this.schema.states[this.state.currentState];
|
|
263
|
+
if (!currentStateConfig?.on) {
|
|
264
|
+
throw new Error(`No transitions defined for state: ${this.state.currentState}`);
|
|
265
|
+
}
|
|
266
|
+
const transition = currentStateConfig.on[event];
|
|
267
|
+
if (!transition) {
|
|
268
|
+
throw new Error(`No transition found for event: ${event} in state: ${this.state.currentState}`);
|
|
269
|
+
}
|
|
270
|
+
if (data) {
|
|
271
|
+
this.state.context = { ...this.state.context, ...data };
|
|
272
|
+
}
|
|
273
|
+
let targetState;
|
|
274
|
+
if (typeof transition === "string") {
|
|
275
|
+
targetState = transition;
|
|
276
|
+
} else {
|
|
277
|
+
const conditionalTransition = transition;
|
|
278
|
+
if (conditionalTransition.cond && !conditionalTransition.cond(this.state.context)) {
|
|
279
|
+
throw new Error(`Condition not met for transition to: ${conditionalTransition.target}`);
|
|
280
|
+
}
|
|
281
|
+
targetState = conditionalTransition.target;
|
|
282
|
+
}
|
|
283
|
+
const validation = Validator.validatePath(
|
|
284
|
+
this.state.currentState,
|
|
285
|
+
this.state.context,
|
|
286
|
+
this.schema
|
|
287
|
+
);
|
|
288
|
+
if (!validation.valid) {
|
|
289
|
+
this.emit("validationError", { errors: validation.errors });
|
|
290
|
+
if (this.options.onError) {
|
|
291
|
+
this.options.onError(new Error(validation.errors.join(", ")));
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const previousState = this.state.currentState;
|
|
296
|
+
this.state.completedSteps.add(previousState);
|
|
297
|
+
this.state.history.push(previousState);
|
|
298
|
+
this.state.currentState = targetState;
|
|
299
|
+
if (this.storage) {
|
|
300
|
+
await this.storage.save(this.state);
|
|
301
|
+
}
|
|
302
|
+
const stepChangeEvent = {
|
|
303
|
+
from: previousState,
|
|
304
|
+
to: targetState,
|
|
305
|
+
context: this.state.context
|
|
306
|
+
};
|
|
307
|
+
this.emit("stepChange", stepChangeEvent);
|
|
308
|
+
if (this.options.onStepChange) {
|
|
309
|
+
this.options.onStepChange(stepChangeEvent);
|
|
310
|
+
}
|
|
311
|
+
const targetStateConfig = this.schema.states[targetState];
|
|
312
|
+
if (!targetStateConfig?.on || Object.keys(targetStateConfig.on).length === 0) {
|
|
313
|
+
this.emit("complete", { context: this.state.context });
|
|
314
|
+
if (this.options.onComplete) {
|
|
315
|
+
this.options.onComplete(this.state.context);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Go back to the previous state
|
|
321
|
+
*/
|
|
322
|
+
async back() {
|
|
323
|
+
if (this.state.history.length === 0) {
|
|
324
|
+
throw new Error("Cannot go back: no history available");
|
|
325
|
+
}
|
|
326
|
+
const previousState = this.state.history.pop();
|
|
327
|
+
this.state.completedSteps.delete(this.state.currentState);
|
|
328
|
+
const currentState = this.state.currentState;
|
|
329
|
+
this.state.currentState = previousState;
|
|
330
|
+
if (this.storage) {
|
|
331
|
+
await this.storage.save(this.state);
|
|
332
|
+
}
|
|
333
|
+
this.emit("stepChange", {
|
|
334
|
+
from: currentState,
|
|
335
|
+
to: previousState,
|
|
336
|
+
context: this.state.context
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Update context without transitioning
|
|
341
|
+
*/
|
|
342
|
+
async updateContext(data) {
|
|
343
|
+
this.state.context = { ...this.state.context, ...data };
|
|
344
|
+
if (this.storage) {
|
|
345
|
+
await this.storage.save(this.state);
|
|
346
|
+
}
|
|
347
|
+
this.emit("contextUpdate", { context: this.state.context });
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Get current progress percentage
|
|
351
|
+
*/
|
|
352
|
+
getProgress() {
|
|
353
|
+
return PathCalculator.calculateProgress(
|
|
354
|
+
this.state.currentState,
|
|
355
|
+
this.state.completedSteps,
|
|
356
|
+
this.schema
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Get current state
|
|
361
|
+
*/
|
|
362
|
+
getCurrentState() {
|
|
363
|
+
return this.state.currentState;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Get current context
|
|
367
|
+
*/
|
|
368
|
+
getContext() {
|
|
369
|
+
return { ...this.state.context };
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Get possible next states
|
|
373
|
+
*/
|
|
374
|
+
getPossibleNextStates() {
|
|
375
|
+
const currentStateConfig = this.schema.states[this.state.currentState];
|
|
376
|
+
if (!currentStateConfig?.on) {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
return Object.values(currentStateConfig.on).map(
|
|
380
|
+
(transition) => typeof transition === "string" ? transition : transition.target
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Reset the form
|
|
385
|
+
*/
|
|
386
|
+
async reset() {
|
|
387
|
+
this.state = {
|
|
388
|
+
currentState: this.schema.initial,
|
|
389
|
+
context: this.schema.context || {},
|
|
390
|
+
history: [],
|
|
391
|
+
completedSteps: /* @__PURE__ */ new Set()
|
|
392
|
+
};
|
|
393
|
+
if (this.storage) {
|
|
394
|
+
await this.storage.clear();
|
|
395
|
+
}
|
|
396
|
+
this.emit("reset", {});
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Event emitter
|
|
400
|
+
*/
|
|
401
|
+
on(event, callback) {
|
|
402
|
+
if (!this.listeners.has(event)) {
|
|
403
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
404
|
+
}
|
|
405
|
+
this.listeners.get(event).add(callback);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Remove event listener
|
|
409
|
+
*/
|
|
410
|
+
off(event, callback) {
|
|
411
|
+
const callbacks = this.listeners.get(event);
|
|
412
|
+
if (callbacks) {
|
|
413
|
+
callbacks.delete(callback);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Emit event
|
|
418
|
+
*/
|
|
419
|
+
emit(event, data) {
|
|
420
|
+
const callbacks = this.listeners.get(event);
|
|
421
|
+
if (callbacks) {
|
|
422
|
+
callbacks.forEach((callback) => callback(data));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Get all possible paths from current state
|
|
427
|
+
*/
|
|
428
|
+
getPossiblePaths() {
|
|
429
|
+
return PathCalculator.getPossiblePaths(
|
|
430
|
+
this.state.currentState,
|
|
431
|
+
this.schema,
|
|
432
|
+
this.state.context
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Check if we can go back
|
|
437
|
+
*/
|
|
438
|
+
canGoBack() {
|
|
439
|
+
return this.state.history.length > 0;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Get the full state (for debugging)
|
|
443
|
+
*/
|
|
444
|
+
getState() {
|
|
445
|
+
return {
|
|
446
|
+
...this.state,
|
|
447
|
+
completedSteps: new Set(this.state.completedSteps)
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
452
|
+
0 && (module.exports = {
|
|
453
|
+
FormEngine,
|
|
454
|
+
PathCalculator,
|
|
455
|
+
StorageAdapter,
|
|
456
|
+
Validator
|
|
457
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
// src/storage.ts
|
|
2
|
+
import { get, set, del } from "idb-keyval";
|
|
3
|
+
var StorageAdapter = class {
|
|
4
|
+
constructor(storageKey = "React Form Atlas-state") {
|
|
5
|
+
this.storageKey = storageKey;
|
|
6
|
+
this.useIndexedDB = typeof indexedDB !== "undefined";
|
|
7
|
+
}
|
|
8
|
+
async save(state) {
|
|
9
|
+
try {
|
|
10
|
+
const serialized = JSON.stringify({
|
|
11
|
+
...state,
|
|
12
|
+
completedSteps: Array.from(state.completedSteps)
|
|
13
|
+
});
|
|
14
|
+
if (this.useIndexedDB) {
|
|
15
|
+
await set(this.storageKey, serialized);
|
|
16
|
+
} else {
|
|
17
|
+
localStorage.setItem(this.storageKey, serialized);
|
|
18
|
+
}
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error("Failed to save form state:", error);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async load() {
|
|
25
|
+
try {
|
|
26
|
+
let serialized = null;
|
|
27
|
+
if (this.useIndexedDB) {
|
|
28
|
+
const result = await get(this.storageKey);
|
|
29
|
+
serialized = result;
|
|
30
|
+
} else {
|
|
31
|
+
serialized = localStorage.getItem(this.storageKey);
|
|
32
|
+
}
|
|
33
|
+
if (!serialized) return null;
|
|
34
|
+
const parsed = JSON.parse(serialized);
|
|
35
|
+
return {
|
|
36
|
+
...parsed,
|
|
37
|
+
completedSteps: new Set(parsed.completedSteps)
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error("Failed to load form state:", error);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async clear() {
|
|
45
|
+
try {
|
|
46
|
+
if (this.useIndexedDB) {
|
|
47
|
+
await del(this.storageKey);
|
|
48
|
+
} else {
|
|
49
|
+
localStorage.removeItem(this.storageKey);
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error("Failed to clear form state:", error);
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/validator.ts
|
|
59
|
+
var Validator = class {
|
|
60
|
+
/**
|
|
61
|
+
* Validate a single field value
|
|
62
|
+
*/
|
|
63
|
+
static validateField(value, rules, context) {
|
|
64
|
+
const errors = [];
|
|
65
|
+
for (const rule of rules) {
|
|
66
|
+
switch (rule.type) {
|
|
67
|
+
case "required":
|
|
68
|
+
if (value === void 0 || value === null || value === "") {
|
|
69
|
+
errors.push(rule.message);
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
case "email":
|
|
73
|
+
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
74
|
+
errors.push(rule.message);
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
case "min":
|
|
78
|
+
if (typeof value === "number" && value < rule.value) {
|
|
79
|
+
errors.push(rule.message);
|
|
80
|
+
} else if (typeof value === "string" && value.length < rule.value) {
|
|
81
|
+
errors.push(rule.message);
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case "max":
|
|
85
|
+
if (typeof value === "number" && value > rule.value) {
|
|
86
|
+
errors.push(rule.message);
|
|
87
|
+
} else if (typeof value === "string" && value.length > rule.value) {
|
|
88
|
+
errors.push(rule.message);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case "pattern":
|
|
92
|
+
if (value && !new RegExp(rule.value).test(value)) {
|
|
93
|
+
errors.push(rule.message);
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case "custom":
|
|
97
|
+
if (rule.validator && !rule.validator(value, context)) {
|
|
98
|
+
errors.push(rule.message);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
valid: errors.length === 0,
|
|
105
|
+
errors
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Predictive validation: Check if the current path is still valid
|
|
110
|
+
* This prevents users from entering data that makes future steps impossible
|
|
111
|
+
*/
|
|
112
|
+
static validatePath(currentState, context, schema) {
|
|
113
|
+
const errors = [];
|
|
114
|
+
const state = schema.states[currentState];
|
|
115
|
+
if (state?.meta?.validation) {
|
|
116
|
+
const result = this.validateField(
|
|
117
|
+
context[currentState],
|
|
118
|
+
state.meta.validation,
|
|
119
|
+
context
|
|
120
|
+
);
|
|
121
|
+
errors.push(...result.errors);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
valid: errors.length === 0,
|
|
125
|
+
errors
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/path-calculator.ts
|
|
131
|
+
var PathCalculator = class {
|
|
132
|
+
/**
|
|
133
|
+
* Calculate the total possible weight from current state to completion
|
|
134
|
+
*/
|
|
135
|
+
static calculateRemainingWeight(currentState, schema, visited = /* @__PURE__ */ new Set()) {
|
|
136
|
+
if (visited.has(currentState)) {
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
visited.add(currentState);
|
|
140
|
+
const state = schema.states[currentState];
|
|
141
|
+
if (!state || !state.on) {
|
|
142
|
+
return state?.meta?.weight || 1;
|
|
143
|
+
}
|
|
144
|
+
const currentWeight = state.meta?.weight || 1;
|
|
145
|
+
const transitions = Object.values(state.on);
|
|
146
|
+
if (transitions.length === 0) {
|
|
147
|
+
return currentWeight;
|
|
148
|
+
}
|
|
149
|
+
const maxFutureWeight = Math.max(
|
|
150
|
+
...transitions.map((transition) => {
|
|
151
|
+
const target = typeof transition === "string" ? transition : transition.target;
|
|
152
|
+
return this.calculateRemainingWeight(target, schema, new Set(visited));
|
|
153
|
+
})
|
|
154
|
+
);
|
|
155
|
+
return currentWeight + maxFutureWeight;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Calculate progress percentage based on completed path weight
|
|
159
|
+
*/
|
|
160
|
+
static calculateProgress(currentState, completedSteps, schema) {
|
|
161
|
+
const totalWeight = this.calculateRemainingWeight(schema.initial, schema);
|
|
162
|
+
let completedWeight = 0;
|
|
163
|
+
for (const step of completedSteps) {
|
|
164
|
+
const state = schema.states[step];
|
|
165
|
+
completedWeight += state?.meta?.weight || 1;
|
|
166
|
+
}
|
|
167
|
+
return totalWeight > 0 ? completedWeight / totalWeight * 100 : 0;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get all possible paths from current state
|
|
171
|
+
*/
|
|
172
|
+
static getPossiblePaths(currentState, schema, context = {}) {
|
|
173
|
+
const paths = [];
|
|
174
|
+
const visited = /* @__PURE__ */ new Set();
|
|
175
|
+
const traverse = (state, currentPath) => {
|
|
176
|
+
if (visited.has(state)) return;
|
|
177
|
+
const stateConfig = schema.states[state];
|
|
178
|
+
if (!stateConfig) return;
|
|
179
|
+
const newPath = [...currentPath, state];
|
|
180
|
+
if (!stateConfig.on || Object.keys(stateConfig.on).length === 0) {
|
|
181
|
+
paths.push(newPath);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
visited.add(state);
|
|
185
|
+
for (const transition of Object.values(stateConfig.on)) {
|
|
186
|
+
const target = typeof transition === "string" ? transition : transition.target;
|
|
187
|
+
if (typeof transition === "object" && transition.cond) {
|
|
188
|
+
if (!transition.cond(context)) continue;
|
|
189
|
+
}
|
|
190
|
+
traverse(target, newPath);
|
|
191
|
+
}
|
|
192
|
+
visited.delete(state);
|
|
193
|
+
};
|
|
194
|
+
traverse(currentState, []);
|
|
195
|
+
return paths;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// src/engine.ts
|
|
200
|
+
var FormEngine = class {
|
|
201
|
+
constructor(options) {
|
|
202
|
+
this.storage = null;
|
|
203
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
204
|
+
this.options = options;
|
|
205
|
+
this.schema = options.schema;
|
|
206
|
+
this.state = {
|
|
207
|
+
currentState: this.schema.initial,
|
|
208
|
+
context: this.schema.context || {},
|
|
209
|
+
history: [],
|
|
210
|
+
completedSteps: /* @__PURE__ */ new Set()
|
|
211
|
+
};
|
|
212
|
+
if (options.autoSave) {
|
|
213
|
+
this.storage = new StorageAdapter(options.storageKey);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Start the form engine
|
|
218
|
+
*/
|
|
219
|
+
async start() {
|
|
220
|
+
if (this.storage) {
|
|
221
|
+
const savedState = await this.storage.load();
|
|
222
|
+
if (savedState) {
|
|
223
|
+
this.state = savedState;
|
|
224
|
+
this.emit("resumed", { state: this.state });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
this.emit("started", { state: this.state });
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Transition to the next state based on an event
|
|
231
|
+
*/
|
|
232
|
+
async transition(event, data) {
|
|
233
|
+
const currentStateConfig = this.schema.states[this.state.currentState];
|
|
234
|
+
if (!currentStateConfig?.on) {
|
|
235
|
+
throw new Error(`No transitions defined for state: ${this.state.currentState}`);
|
|
236
|
+
}
|
|
237
|
+
const transition = currentStateConfig.on[event];
|
|
238
|
+
if (!transition) {
|
|
239
|
+
throw new Error(`No transition found for event: ${event} in state: ${this.state.currentState}`);
|
|
240
|
+
}
|
|
241
|
+
if (data) {
|
|
242
|
+
this.state.context = { ...this.state.context, ...data };
|
|
243
|
+
}
|
|
244
|
+
let targetState;
|
|
245
|
+
if (typeof transition === "string") {
|
|
246
|
+
targetState = transition;
|
|
247
|
+
} else {
|
|
248
|
+
const conditionalTransition = transition;
|
|
249
|
+
if (conditionalTransition.cond && !conditionalTransition.cond(this.state.context)) {
|
|
250
|
+
throw new Error(`Condition not met for transition to: ${conditionalTransition.target}`);
|
|
251
|
+
}
|
|
252
|
+
targetState = conditionalTransition.target;
|
|
253
|
+
}
|
|
254
|
+
const validation = Validator.validatePath(
|
|
255
|
+
this.state.currentState,
|
|
256
|
+
this.state.context,
|
|
257
|
+
this.schema
|
|
258
|
+
);
|
|
259
|
+
if (!validation.valid) {
|
|
260
|
+
this.emit("validationError", { errors: validation.errors });
|
|
261
|
+
if (this.options.onError) {
|
|
262
|
+
this.options.onError(new Error(validation.errors.join(", ")));
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const previousState = this.state.currentState;
|
|
267
|
+
this.state.completedSteps.add(previousState);
|
|
268
|
+
this.state.history.push(previousState);
|
|
269
|
+
this.state.currentState = targetState;
|
|
270
|
+
if (this.storage) {
|
|
271
|
+
await this.storage.save(this.state);
|
|
272
|
+
}
|
|
273
|
+
const stepChangeEvent = {
|
|
274
|
+
from: previousState,
|
|
275
|
+
to: targetState,
|
|
276
|
+
context: this.state.context
|
|
277
|
+
};
|
|
278
|
+
this.emit("stepChange", stepChangeEvent);
|
|
279
|
+
if (this.options.onStepChange) {
|
|
280
|
+
this.options.onStepChange(stepChangeEvent);
|
|
281
|
+
}
|
|
282
|
+
const targetStateConfig = this.schema.states[targetState];
|
|
283
|
+
if (!targetStateConfig?.on || Object.keys(targetStateConfig.on).length === 0) {
|
|
284
|
+
this.emit("complete", { context: this.state.context });
|
|
285
|
+
if (this.options.onComplete) {
|
|
286
|
+
this.options.onComplete(this.state.context);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Go back to the previous state
|
|
292
|
+
*/
|
|
293
|
+
async back() {
|
|
294
|
+
if (this.state.history.length === 0) {
|
|
295
|
+
throw new Error("Cannot go back: no history available");
|
|
296
|
+
}
|
|
297
|
+
const previousState = this.state.history.pop();
|
|
298
|
+
this.state.completedSteps.delete(this.state.currentState);
|
|
299
|
+
const currentState = this.state.currentState;
|
|
300
|
+
this.state.currentState = previousState;
|
|
301
|
+
if (this.storage) {
|
|
302
|
+
await this.storage.save(this.state);
|
|
303
|
+
}
|
|
304
|
+
this.emit("stepChange", {
|
|
305
|
+
from: currentState,
|
|
306
|
+
to: previousState,
|
|
307
|
+
context: this.state.context
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Update context without transitioning
|
|
312
|
+
*/
|
|
313
|
+
async updateContext(data) {
|
|
314
|
+
this.state.context = { ...this.state.context, ...data };
|
|
315
|
+
if (this.storage) {
|
|
316
|
+
await this.storage.save(this.state);
|
|
317
|
+
}
|
|
318
|
+
this.emit("contextUpdate", { context: this.state.context });
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get current progress percentage
|
|
322
|
+
*/
|
|
323
|
+
getProgress() {
|
|
324
|
+
return PathCalculator.calculateProgress(
|
|
325
|
+
this.state.currentState,
|
|
326
|
+
this.state.completedSteps,
|
|
327
|
+
this.schema
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Get current state
|
|
332
|
+
*/
|
|
333
|
+
getCurrentState() {
|
|
334
|
+
return this.state.currentState;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Get current context
|
|
338
|
+
*/
|
|
339
|
+
getContext() {
|
|
340
|
+
return { ...this.state.context };
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Get possible next states
|
|
344
|
+
*/
|
|
345
|
+
getPossibleNextStates() {
|
|
346
|
+
const currentStateConfig = this.schema.states[this.state.currentState];
|
|
347
|
+
if (!currentStateConfig?.on) {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
return Object.values(currentStateConfig.on).map(
|
|
351
|
+
(transition) => typeof transition === "string" ? transition : transition.target
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Reset the form
|
|
356
|
+
*/
|
|
357
|
+
async reset() {
|
|
358
|
+
this.state = {
|
|
359
|
+
currentState: this.schema.initial,
|
|
360
|
+
context: this.schema.context || {},
|
|
361
|
+
history: [],
|
|
362
|
+
completedSteps: /* @__PURE__ */ new Set()
|
|
363
|
+
};
|
|
364
|
+
if (this.storage) {
|
|
365
|
+
await this.storage.clear();
|
|
366
|
+
}
|
|
367
|
+
this.emit("reset", {});
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Event emitter
|
|
371
|
+
*/
|
|
372
|
+
on(event, callback) {
|
|
373
|
+
if (!this.listeners.has(event)) {
|
|
374
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
375
|
+
}
|
|
376
|
+
this.listeners.get(event).add(callback);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Remove event listener
|
|
380
|
+
*/
|
|
381
|
+
off(event, callback) {
|
|
382
|
+
const callbacks = this.listeners.get(event);
|
|
383
|
+
if (callbacks) {
|
|
384
|
+
callbacks.delete(callback);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Emit event
|
|
389
|
+
*/
|
|
390
|
+
emit(event, data) {
|
|
391
|
+
const callbacks = this.listeners.get(event);
|
|
392
|
+
if (callbacks) {
|
|
393
|
+
callbacks.forEach((callback) => callback(data));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Get all possible paths from current state
|
|
398
|
+
*/
|
|
399
|
+
getPossiblePaths() {
|
|
400
|
+
return PathCalculator.getPossiblePaths(
|
|
401
|
+
this.state.currentState,
|
|
402
|
+
this.schema,
|
|
403
|
+
this.state.context
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Check if we can go back
|
|
408
|
+
*/
|
|
409
|
+
canGoBack() {
|
|
410
|
+
return this.state.history.length > 0;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get the full state (for debugging)
|
|
414
|
+
*/
|
|
415
|
+
getState() {
|
|
416
|
+
return {
|
|
417
|
+
...this.state,
|
|
418
|
+
completedSteps: new Set(this.state.completedSteps)
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
export {
|
|
423
|
+
FormEngine,
|
|
424
|
+
PathCalculator,
|
|
425
|
+
StorageAdapter,
|
|
426
|
+
Validator
|
|
427
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-form-atlas-engine",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Framework-agnostic graph-based form engine",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"require": "./dist/index.js",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
24
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
25
|
+
"test": "vitest",
|
|
26
|
+
"test:coverage": "vitest --coverage"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"forms",
|
|
30
|
+
"state-machine",
|
|
31
|
+
"graph",
|
|
32
|
+
"branching-logic",
|
|
33
|
+
"multi-step-forms"
|
|
34
|
+
],
|
|
35
|
+
"author": "Mehul Birare",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/Mehulbirare/react-form.git",
|
|
39
|
+
"directory": "packages/core"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"tsup": "^8.0.1",
|
|
44
|
+
"typescript": "^5.3.3",
|
|
45
|
+
"vitest": "^1.2.0"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"idb-keyval": "^6.2.1"
|
|
49
|
+
}
|
|
50
|
+
}
|