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 ADDED
@@ -0,0 +1,92 @@
1
+ # react-form-atlas-engine
2
+
3
+ [![npm version](https://img.shields.io/npm/v/react-form-atlas-engine.svg?style=flat-square)](https://www.npmjs.com/package/react-form-atlas-engine)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue.svg?style=flat-square)](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
+ [![Try on StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](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
+
@@ -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 };
@@ -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
+ }