@fogpipe/forma-react 0.12.0 → 0.14.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/src/context.ts CHANGED
@@ -17,7 +17,9 @@ export const FormaContext = createContext<UseFormaReturn | null>(null);
17
17
  export function useFormaContext(): UseFormaReturn {
18
18
  const context = useContext(FormaContext);
19
19
  if (!context) {
20
- throw new Error("useFormaContext must be used within a FormaContext.Provider");
20
+ throw new Error(
21
+ "useFormaContext must be used within a FormaContext.Provider",
22
+ );
21
23
  }
22
24
  return context;
23
25
  }
package/src/events.ts ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Event system for forma-react
3
+ *
4
+ * Lightweight event emitter for form lifecycle events.
5
+ * Events are for side effects (analytics, data injection, external state sync)
6
+ * — they do not trigger React re-renders.
7
+ */
8
+
9
+ import type { FieldError } from "@fogpipe/forma-core";
10
+ import type { PageState } from "./useForma.js";
11
+
12
+ // ============================================================================
13
+ // Event Type Definitions
14
+ // ============================================================================
15
+
16
+ /**
17
+ * Map of all forma event names to their payload types.
18
+ */
19
+ export interface FormaEventMap {
20
+ /**
21
+ * Fires after a field value changes via user input, setFieldValue, setValues, or resetForm.
22
+ * Does NOT fire for computed/calculated field changes or on initial mount.
23
+ */
24
+ fieldChanged: {
25
+ /** Field path (e.g., "age" or "medications[0].dosage") */
26
+ path: string;
27
+ /** New value */
28
+ value: unknown;
29
+ /** Value before the change */
30
+ previousValue: unknown;
31
+ /** What triggered the change */
32
+ source: "user" | "reset" | "setValues";
33
+ };
34
+
35
+ /**
36
+ * Fires at the start of submitForm(), before validation.
37
+ * The `data` object is mutable — consumers can inject extra fields.
38
+ * Async handlers are awaited before proceeding to validation.
39
+ */
40
+ preSubmit: {
41
+ /** Mutable form data — add/modify fields here */
42
+ data: Record<string, unknown>;
43
+ /** Read-only snapshot of computed values */
44
+ computed: Record<string, unknown>;
45
+ };
46
+
47
+ /**
48
+ * Fires after onSubmit resolves/rejects or after validation failure.
49
+ */
50
+ postSubmit: {
51
+ /** The submitted data (reflects any preSubmit mutations) */
52
+ data: Record<string, unknown>;
53
+ /** Whether submission succeeded */
54
+ success: boolean;
55
+ /** Present when onSubmit threw an error */
56
+ error?: Error;
57
+ /** Present when validation failed (onSubmit was never called) */
58
+ validationErrors?: FieldError[];
59
+ };
60
+
61
+ /**
62
+ * Fires when the wizard page changes via nextPage, previousPage, or goToPage.
63
+ * Does NOT fire on initial render or automatic page clamping.
64
+ */
65
+ pageChanged: {
66
+ /** Previous page index */
67
+ fromIndex: number;
68
+ /** New page index */
69
+ toIndex: number;
70
+ /** The new current page */
71
+ page: PageState;
72
+ };
73
+
74
+ /**
75
+ * Fires after resetForm() completes and state is back to initial values.
76
+ */
77
+ formReset: Record<string, never>;
78
+ }
79
+
80
+ // ============================================================================
81
+ // Helper Types
82
+ // ============================================================================
83
+
84
+ /**
85
+ * Listener function type for a specific event.
86
+ */
87
+ export type FormaEventListener<K extends keyof FormaEventMap> = (
88
+ event: FormaEventMap[K],
89
+ ) => void | Promise<void>;
90
+
91
+ /**
92
+ * Declarative event listener map for useForma `on` option.
93
+ */
94
+ export type FormaEvents = Partial<{
95
+ [K in keyof FormaEventMap]: FormaEventListener<K>;
96
+ }>;
97
+
98
+ // ============================================================================
99
+ // Event Emitter
100
+ // ============================================================================
101
+
102
+ /**
103
+ * Lightweight event emitter for forma lifecycle events.
104
+ * Uses Map<string, Set<listener>> internally — no external dependencies.
105
+ */
106
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
+ type AnyListener = (...args: any[]) => void | Promise<void>;
108
+
109
+ export class FormaEventEmitter {
110
+ private listeners = new Map<string, Set<AnyListener>>();
111
+
112
+ /**
113
+ * Register a listener for an event. Returns an unsubscribe function.
114
+ */
115
+ on<K extends keyof FormaEventMap>(
116
+ event: K,
117
+ listener: FormaEventListener<K>,
118
+ ): () => void {
119
+ if (!this.listeners.has(event)) {
120
+ this.listeners.set(event, new Set());
121
+ }
122
+ this.listeners.get(event)!.add(listener);
123
+
124
+ return () => {
125
+ const set = this.listeners.get(event);
126
+ if (set) {
127
+ set.delete(listener);
128
+ if (set.size === 0) {
129
+ this.listeners.delete(event);
130
+ }
131
+ }
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Fire an event synchronously. Listener errors are caught and logged
137
+ * to prevent one listener from breaking others.
138
+ */
139
+ fire<K extends keyof FormaEventMap>(event: K, payload: FormaEventMap[K]): void {
140
+ const set = this.listeners.get(event);
141
+ if (!set || set.size === 0) return;
142
+
143
+ for (const listener of set) {
144
+ try {
145
+ listener(payload);
146
+ } catch (error) {
147
+ console.error(`[forma] Error in "${event}" event listener:`, error);
148
+ }
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Fire an event and await all async listeners sequentially.
154
+ * Used for preSubmit where handlers can be async.
155
+ */
156
+ async fireAsync<K extends keyof FormaEventMap>(
157
+ event: K,
158
+ payload: FormaEventMap[K],
159
+ ): Promise<void> {
160
+ const set = this.listeners.get(event);
161
+ if (!set || set.size === 0) return;
162
+
163
+ for (const listener of set) {
164
+ try {
165
+ await listener(payload);
166
+ } catch (error) {
167
+ console.error(`[forma] Error in "${event}" event listener:`, error);
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Check if any listeners are registered for an event.
174
+ */
175
+ hasListeners(event: keyof FormaEventMap): boolean {
176
+ const set = this.listeners.get(event);
177
+ return set !== undefined && set.size > 0;
178
+ }
179
+
180
+ /**
181
+ * Remove all listeners. Called on cleanup.
182
+ */
183
+ clear(): void {
184
+ this.listeners.clear();
185
+ }
186
+ }
package/src/index.ts CHANGED
@@ -20,7 +20,12 @@ export type {
20
20
 
21
21
  // Hook
22
22
  export { useForma } from "./useForma.js";
23
- export type { UseFormaOptions, UseFormaReturn, PageState, WizardHelpers } from "./useForma.js";
23
+ export type {
24
+ UseFormaOptions,
25
+ UseFormaReturn,
26
+ PageState,
27
+ WizardHelpers,
28
+ } from "./useForma.js";
24
29
 
25
30
  // Components
26
31
  export { FormRenderer } from "./FormRenderer.js";
@@ -90,4 +95,9 @@ export type {
90
95
  LayoutProps,
91
96
  FieldWrapperProps,
92
97
  PageWrapperProps,
98
+
99
+ // Event types
100
+ FormaEventMap,
101
+ FormaEvents,
102
+ FormaEventListener,
93
103
  } from "./types.js";
package/src/types.ts CHANGED
@@ -2,7 +2,12 @@
2
2
  * Type definitions for forma-react components
3
3
  */
4
4
 
5
- import type { Forma, FieldDefinition, FieldError, SelectOption } from "@fogpipe/forma-core";
5
+ import type {
6
+ Forma,
7
+ FieldDefinition,
8
+ FieldError,
9
+ SelectOption,
10
+ } from "@fogpipe/forma-core";
6
11
 
7
12
  /**
8
13
  * Base props shared by all field components
@@ -52,7 +57,10 @@ export interface BaseFieldProps {
52
57
  /**
53
58
  * Props for text-based fields (text, email, password, url, textarea)
54
59
  */
55
- export interface TextFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
60
+ export interface TextFieldProps extends Omit<
61
+ BaseFieldProps,
62
+ "value" | "onChange"
63
+ > {
56
64
  fieldType: "text" | "email" | "password" | "url" | "textarea";
57
65
  value: string;
58
66
  onChange: (value: string) => void;
@@ -61,7 +69,10 @@ export interface TextFieldProps extends Omit<BaseFieldProps, "value" | "onChange
61
69
  /**
62
70
  * Props for number fields
63
71
  */
64
- export interface NumberFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
72
+ export interface NumberFieldProps extends Omit<
73
+ BaseFieldProps,
74
+ "value" | "onChange"
75
+ > {
65
76
  fieldType: "number";
66
77
  value: number | null;
67
78
  onChange: (value: number | null) => void;
@@ -73,7 +84,10 @@ export interface NumberFieldProps extends Omit<BaseFieldProps, "value" | "onChan
73
84
  /**
74
85
  * Props for integer fields
75
86
  */
76
- export interface IntegerFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
87
+ export interface IntegerFieldProps extends Omit<
88
+ BaseFieldProps,
89
+ "value" | "onChange"
90
+ > {
77
91
  fieldType: "integer";
78
92
  value: number | null;
79
93
  onChange: (value: number | null) => void;
@@ -85,7 +99,10 @@ export interface IntegerFieldProps extends Omit<BaseFieldProps, "value" | "onCha
85
99
  /**
86
100
  * Props for boolean fields
87
101
  */
88
- export interface BooleanFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
102
+ export interface BooleanFieldProps extends Omit<
103
+ BaseFieldProps,
104
+ "value" | "onChange"
105
+ > {
89
106
  fieldType: "boolean";
90
107
  value: boolean;
91
108
  onChange: (value: boolean) => void;
@@ -94,7 +111,10 @@ export interface BooleanFieldProps extends Omit<BaseFieldProps, "value" | "onCha
94
111
  /**
95
112
  * Props for date fields
96
113
  */
97
- export interface DateFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
114
+ export interface DateFieldProps extends Omit<
115
+ BaseFieldProps,
116
+ "value" | "onChange"
117
+ > {
98
118
  fieldType: "date";
99
119
  value: string | null;
100
120
  onChange: (value: string | null) => void;
@@ -103,7 +123,10 @@ export interface DateFieldProps extends Omit<BaseFieldProps, "value" | "onChange
103
123
  /**
104
124
  * Props for datetime fields
105
125
  */
106
- export interface DateTimeFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
126
+ export interface DateTimeFieldProps extends Omit<
127
+ BaseFieldProps,
128
+ "value" | "onChange"
129
+ > {
107
130
  fieldType: "datetime";
108
131
  value: string | null;
109
132
  onChange: (value: string | null) => void;
@@ -112,7 +135,10 @@ export interface DateTimeFieldProps extends Omit<BaseFieldProps, "value" | "onCh
112
135
  /**
113
136
  * Props for select fields (single selection)
114
137
  */
115
- export interface SelectFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
138
+ export interface SelectFieldProps extends Omit<
139
+ BaseFieldProps,
140
+ "value" | "onChange"
141
+ > {
116
142
  fieldType: "select";
117
143
  value: string | null;
118
144
  onChange: (value: string | null) => void;
@@ -122,7 +148,10 @@ export interface SelectFieldProps extends Omit<BaseFieldProps, "value" | "onChan
122
148
  /**
123
149
  * Props for multi-select fields
124
150
  */
125
- export interface MultiSelectFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
151
+ export interface MultiSelectFieldProps extends Omit<
152
+ BaseFieldProps,
153
+ "value" | "onChange"
154
+ > {
126
155
  fieldType: "multiselect";
127
156
  value: string[];
128
157
  onChange: (value: string[]) => void;
@@ -189,7 +218,10 @@ export interface ArrayHelpers {
189
218
  /** Swap items at two indices */
190
219
  swap: (indexA: number, indexB: number) => void;
191
220
  /** Get field props for an item field */
192
- getItemFieldProps: (index: number, fieldName: string) => ArrayItemFieldPropsResult;
221
+ getItemFieldProps: (
222
+ index: number,
223
+ fieldName: string,
224
+ ) => ArrayItemFieldPropsResult;
193
225
  /** Minimum number of items allowed */
194
226
  minItems: number;
195
227
  /** Maximum number of items allowed */
@@ -203,7 +235,10 @@ export interface ArrayHelpers {
203
235
  /**
204
236
  * Props for array fields
205
237
  */
206
- export interface ArrayFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
238
+ export interface ArrayFieldProps extends Omit<
239
+ BaseFieldProps,
240
+ "value" | "onChange"
241
+ > {
207
242
  fieldType: "array";
208
243
  value: unknown[];
209
244
  onChange: (value: unknown[]) => void;
@@ -217,7 +252,10 @@ export interface ArrayFieldProps extends Omit<BaseFieldProps, "value" | "onChang
217
252
  /**
218
253
  * Props for object fields
219
254
  */
220
- export interface ObjectFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
255
+ export interface ObjectFieldProps extends Omit<
256
+ BaseFieldProps,
257
+ "value" | "onChange"
258
+ > {
221
259
  fieldType: "object";
222
260
  value: Record<string, unknown>;
223
261
  onChange: (value: Record<string, unknown>) => void;
@@ -236,7 +274,10 @@ export interface ComputedFieldProps extends Omit<BaseFieldProps, "onChange"> {
236
274
  /**
237
275
  * Props for array item fields (within array context)
238
276
  */
239
- export interface ArrayItemFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
277
+ export interface ArrayItemFieldProps extends Omit<
278
+ BaseFieldProps,
279
+ "value" | "onChange"
280
+ > {
240
281
  /** The field type */
241
282
  fieldType: string;
242
283
  /** Current value */
@@ -252,7 +293,10 @@ export interface ArrayItemFieldProps extends Omit<BaseFieldProps, "value" | "onC
252
293
  /**
253
294
  * Props for display fields (read-only presentation content)
254
295
  */
255
- export interface DisplayFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
296
+ export interface DisplayFieldProps extends Omit<
297
+ BaseFieldProps,
298
+ "value" | "onChange"
299
+ > {
256
300
  fieldType: "display";
257
301
  /** Static content (markdown/text) */
258
302
  content?: string;
@@ -432,6 +476,13 @@ export type { UseFormaReturn as FormState } from "./useForma.js";
432
476
  export type { UseFormaOptions } from "./useForma.js";
433
477
  export type { PageState, WizardHelpers } from "./useForma.js";
434
478
 
479
+ // Re-export event types
480
+ export type {
481
+ FormaEventMap,
482
+ FormaEvents,
483
+ FormaEventListener,
484
+ } from "./events.js";
485
+
435
486
  // Re-export ValidationResult for convenience
436
487
  export type { ValidationResult } from "@fogpipe/forma-core";
437
488