@fogpipe/forma-react 0.13.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/README.md +74 -9
- package/dist/index.d.ts +90 -1
- package/dist/index.js +274 -50
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/events.test.ts +752 -0
- package/src/events.ts +186 -0
- package/src/index.ts +5 -0
- package/src/types.ts +7 -0
- package/src/useForma.ts +269 -58
package/README.md
CHANGED
|
@@ -193,6 +193,69 @@ function WizardForm({ spec }: { spec: Forma }) {
|
|
|
193
193
|
}
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
+
## Event System
|
|
197
|
+
|
|
198
|
+
The event system lets you observe form lifecycle events for side effects like analytics, data injection, and external state sync. Events do not trigger React re-renders.
|
|
199
|
+
|
|
200
|
+
### Available Events
|
|
201
|
+
|
|
202
|
+
| Event | Description |
|
|
203
|
+
| -------------- | ------------------------------------------------------- |
|
|
204
|
+
| `fieldChanged` | Fires after a field value changes |
|
|
205
|
+
| `preSubmit` | Fires before validation; `data` is mutable |
|
|
206
|
+
| `postSubmit` | Fires after submission (success, error, or invalid) |
|
|
207
|
+
| `pageChanged` | Fires when wizard page changes |
|
|
208
|
+
| `formReset` | Fires after `resetForm()` completes |
|
|
209
|
+
|
|
210
|
+
### Declarative Registration
|
|
211
|
+
|
|
212
|
+
Pass listeners via the `on` option — callbacks are stable (ref-based):
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
const forma = useForma({
|
|
216
|
+
spec,
|
|
217
|
+
onSubmit: handleSubmit,
|
|
218
|
+
on: {
|
|
219
|
+
fieldChanged: (event) => {
|
|
220
|
+
analytics.track("field_changed", { path: event.path, source: event.source });
|
|
221
|
+
},
|
|
222
|
+
preSubmit: async (event) => {
|
|
223
|
+
event.data.token = await getCSRFToken();
|
|
224
|
+
},
|
|
225
|
+
postSubmit: (event) => {
|
|
226
|
+
if (event.success) router.push("/thank-you");
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Imperative Registration
|
|
233
|
+
|
|
234
|
+
Register listeners dynamically — returns an unsubscribe function:
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
const forma = useForma({ spec, onSubmit: handleSubmit });
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
const unsubscribe = forma.on("fieldChanged", (event) => {
|
|
241
|
+
console.log(`${event.path}: ${event.previousValue} → ${event.value}`);
|
|
242
|
+
});
|
|
243
|
+
return unsubscribe;
|
|
244
|
+
}, [forma]);
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Event Payloads
|
|
248
|
+
|
|
249
|
+
**`fieldChanged`**: `{ path, value, previousValue, source }` where `source` is `'user' | 'reset' | 'setValues'`
|
|
250
|
+
|
|
251
|
+
**`preSubmit`**: `{ data, computed }` — mutate `data` to inject fields before validation
|
|
252
|
+
|
|
253
|
+
**`postSubmit`**: `{ data, success, error?, validationErrors? }`
|
|
254
|
+
|
|
255
|
+
**`pageChanged`**: `{ fromIndex, toIndex, page }`
|
|
256
|
+
|
|
257
|
+
**`formReset`**: `{}` (no payload)
|
|
258
|
+
|
|
196
259
|
## API Reference
|
|
197
260
|
|
|
198
261
|
### FormRenderer Props
|
|
@@ -239,15 +302,16 @@ formRef.current?.setValues({ name: "John" });
|
|
|
239
302
|
|
|
240
303
|
### useForma Methods
|
|
241
304
|
|
|
242
|
-
| Method | Description
|
|
243
|
-
| --------------------------------- |
|
|
244
|
-
| `setFieldValue(path, value)` | Set field value
|
|
245
|
-
| `setFieldTouched(path, touched?)` | Mark field as touched
|
|
246
|
-
| `setValues(values)` | Set multiple values
|
|
247
|
-
| `validateField(path)` | Validate single field
|
|
248
|
-
| `validateForm()` | Validate entire form
|
|
249
|
-
| `submitForm()` | Submit the form
|
|
250
|
-
| `resetForm()` | Reset to initial values
|
|
305
|
+
| Method | Description |
|
|
306
|
+
| --------------------------------- | ------------------------------------ |
|
|
307
|
+
| `setFieldValue(path, value)` | Set field value |
|
|
308
|
+
| `setFieldTouched(path, touched?)` | Mark field as touched |
|
|
309
|
+
| `setValues(values)` | Set multiple values |
|
|
310
|
+
| `validateField(path)` | Validate single field |
|
|
311
|
+
| `validateForm()` | Validate entire form |
|
|
312
|
+
| `submitForm()` | Submit the form |
|
|
313
|
+
| `resetForm()` | Reset to initial values |
|
|
314
|
+
| `on(event, listener)` | Register event listener; returns unsubscribe |
|
|
251
315
|
|
|
252
316
|
### useForma Options
|
|
253
317
|
|
|
@@ -260,6 +324,7 @@ formRef.current?.setValues({ name: "John" });
|
|
|
260
324
|
| `validateOn` | `"change" \| "blur" \| "submit"` | `"blur"` | When to validate |
|
|
261
325
|
| `referenceData` | `Record<string, unknown>` | - | Additional reference data |
|
|
262
326
|
| `validationDebounceMs` | `number` | `0` | Debounce validation (ms) |
|
|
327
|
+
| `on` | `FormaEvents` | - | Declarative event listeners |
|
|
263
328
|
|
|
264
329
|
## Error Boundary
|
|
265
330
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,84 @@ import * as React$1 from 'react';
|
|
|
4
4
|
import React__default from 'react';
|
|
5
5
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Event system for forma-react
|
|
9
|
+
*
|
|
10
|
+
* Lightweight event emitter for form lifecycle events.
|
|
11
|
+
* Events are for side effects (analytics, data injection, external state sync)
|
|
12
|
+
* — they do not trigger React re-renders.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Map of all forma event names to their payload types.
|
|
17
|
+
*/
|
|
18
|
+
interface FormaEventMap {
|
|
19
|
+
/**
|
|
20
|
+
* Fires after a field value changes via user input, setFieldValue, setValues, or resetForm.
|
|
21
|
+
* Does NOT fire for computed/calculated field changes or on initial mount.
|
|
22
|
+
*/
|
|
23
|
+
fieldChanged: {
|
|
24
|
+
/** Field path (e.g., "age" or "medications[0].dosage") */
|
|
25
|
+
path: string;
|
|
26
|
+
/** New value */
|
|
27
|
+
value: unknown;
|
|
28
|
+
/** Value before the change */
|
|
29
|
+
previousValue: unknown;
|
|
30
|
+
/** What triggered the change */
|
|
31
|
+
source: "user" | "reset" | "setValues";
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Fires at the start of submitForm(), before validation.
|
|
35
|
+
* The `data` object is mutable — consumers can inject extra fields.
|
|
36
|
+
* Async handlers are awaited before proceeding to validation.
|
|
37
|
+
*/
|
|
38
|
+
preSubmit: {
|
|
39
|
+
/** Mutable form data — add/modify fields here */
|
|
40
|
+
data: Record<string, unknown>;
|
|
41
|
+
/** Read-only snapshot of computed values */
|
|
42
|
+
computed: Record<string, unknown>;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Fires after onSubmit resolves/rejects or after validation failure.
|
|
46
|
+
*/
|
|
47
|
+
postSubmit: {
|
|
48
|
+
/** The submitted data (reflects any preSubmit mutations) */
|
|
49
|
+
data: Record<string, unknown>;
|
|
50
|
+
/** Whether submission succeeded */
|
|
51
|
+
success: boolean;
|
|
52
|
+
/** Present when onSubmit threw an error */
|
|
53
|
+
error?: Error;
|
|
54
|
+
/** Present when validation failed (onSubmit was never called) */
|
|
55
|
+
validationErrors?: FieldError[];
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Fires when the wizard page changes via nextPage, previousPage, or goToPage.
|
|
59
|
+
* Does NOT fire on initial render or automatic page clamping.
|
|
60
|
+
*/
|
|
61
|
+
pageChanged: {
|
|
62
|
+
/** Previous page index */
|
|
63
|
+
fromIndex: number;
|
|
64
|
+
/** New page index */
|
|
65
|
+
toIndex: number;
|
|
66
|
+
/** The new current page */
|
|
67
|
+
page: PageState;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Fires after resetForm() completes and state is back to initial values.
|
|
71
|
+
*/
|
|
72
|
+
formReset: Record<string, never>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Listener function type for a specific event.
|
|
76
|
+
*/
|
|
77
|
+
type FormaEventListener<K extends keyof FormaEventMap> = (event: FormaEventMap[K]) => void | Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Declarative event listener map for useForma `on` option.
|
|
80
|
+
*/
|
|
81
|
+
type FormaEvents = Partial<{
|
|
82
|
+
[K in keyof FormaEventMap]: FormaEventListener<K>;
|
|
83
|
+
}>;
|
|
84
|
+
|
|
7
85
|
/**
|
|
8
86
|
* Type definitions for forma-react components
|
|
9
87
|
*/
|
|
@@ -499,6 +577,12 @@ interface UseFormaOptions {
|
|
|
499
577
|
* Set to 0 (default) for immediate validation.
|
|
500
578
|
*/
|
|
501
579
|
validationDebounceMs?: number;
|
|
580
|
+
/**
|
|
581
|
+
* Declarative event listeners for form lifecycle events.
|
|
582
|
+
* Listeners are stable for the lifetime of the hook — the latest
|
|
583
|
+
* callback is always invoked via refs, without causing dependency changes.
|
|
584
|
+
*/
|
|
585
|
+
on?: FormaEvents;
|
|
502
586
|
}
|
|
503
587
|
/**
|
|
504
588
|
* Page state for multi-page forms
|
|
@@ -575,6 +659,11 @@ interface UseFormaReturn {
|
|
|
575
659
|
submitForm: () => Promise<void>;
|
|
576
660
|
/** Reset the form */
|
|
577
661
|
resetForm: () => void;
|
|
662
|
+
/**
|
|
663
|
+
* Register an imperative event listener. Returns an unsubscribe function.
|
|
664
|
+
* Multiple listeners per event are supported; they fire in registration order.
|
|
665
|
+
*/
|
|
666
|
+
on: <K extends keyof FormaEventMap>(event: K, listener: (payload: FormaEventMap[K]) => void | Promise<void>) => () => void;
|
|
578
667
|
/** Get props for any field */
|
|
579
668
|
getFieldProps: (path: string) => GetFieldPropsResult;
|
|
580
669
|
/** Get props for select field (includes options) */
|
|
@@ -722,4 +811,4 @@ declare const FormaContext: React$1.Context<UseFormaReturn | null>;
|
|
|
722
811
|
*/
|
|
723
812
|
declare function useFormaContext(): UseFormaReturn;
|
|
724
813
|
|
|
725
|
-
export { type ArrayComponentProps, type ArrayFieldProps, type ArrayHelpers, type ArrayItemFieldProps, type ArrayItemFieldPropsResult, type BaseFieldProps, type BooleanComponentProps, type BooleanFieldProps, type ComponentMap, type ComputedComponentProps, type ComputedFieldProps, type DateComponentProps, type DateFieldProps, type DateTimeComponentProps, type DateTimeFieldProps, type DisplayComponentProps, type DisplayFieldProps, type FieldComponentProps, type FieldProps, FieldRenderer, type FieldRendererProps, type FieldWrapperProps, FormRenderer, type FormRendererHandle, type FormRendererProps, type UseFormaReturn as FormState, FormaContext, FormaErrorBoundary, type FormaErrorBoundaryProps, type GetArrayHelpersResult, type GetFieldPropsResult, type GetSelectFieldPropsResult, type IntegerComponentProps, type IntegerFieldProps, type LayoutProps, type LegacyFieldProps, type MultiSelectComponentProps, type MultiSelectFieldProps, type NumberComponentProps, type NumberFieldProps, type ObjectComponentProps, type ObjectFieldProps, type PageState, type PageWrapperProps, type SelectComponentProps, type SelectFieldProps, type SelectionFieldProps, type TextComponentProps, type TextFieldProps, type UseFormaOptions, type UseFormaReturn, type WizardHelpers, useForma, useFormaContext };
|
|
814
|
+
export { type ArrayComponentProps, type ArrayFieldProps, type ArrayHelpers, type ArrayItemFieldProps, type ArrayItemFieldPropsResult, type BaseFieldProps, type BooleanComponentProps, type BooleanFieldProps, type ComponentMap, type ComputedComponentProps, type ComputedFieldProps, type DateComponentProps, type DateFieldProps, type DateTimeComponentProps, type DateTimeFieldProps, type DisplayComponentProps, type DisplayFieldProps, type FieldComponentProps, type FieldProps, FieldRenderer, type FieldRendererProps, type FieldWrapperProps, FormRenderer, type FormRendererHandle, type FormRendererProps, type UseFormaReturn as FormState, FormaContext, FormaErrorBoundary, type FormaErrorBoundaryProps, type FormaEventListener, type FormaEventMap, type FormaEvents, type GetArrayHelpersResult, type GetFieldPropsResult, type GetSelectFieldPropsResult, type IntegerComponentProps, type IntegerFieldProps, type LayoutProps, type LegacyFieldProps, type MultiSelectComponentProps, type MultiSelectFieldProps, type NumberComponentProps, type NumberFieldProps, type ObjectComponentProps, type ObjectFieldProps, type PageState, type PageWrapperProps, type SelectComponentProps, type SelectFieldProps, type SelectionFieldProps, type TextComponentProps, type TextFieldProps, type UseFormaOptions, type UseFormaReturn, type WizardHelpers, useForma, useFormaContext };
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,74 @@ import {
|
|
|
8
8
|
useState
|
|
9
9
|
} from "react";
|
|
10
10
|
import { isAdornableField } from "@fogpipe/forma-core";
|
|
11
|
+
|
|
12
|
+
// src/events.ts
|
|
13
|
+
var FormaEventEmitter = class {
|
|
14
|
+
listeners = /* @__PURE__ */ new Map();
|
|
15
|
+
/**
|
|
16
|
+
* Register a listener for an event. Returns an unsubscribe function.
|
|
17
|
+
*/
|
|
18
|
+
on(event, listener) {
|
|
19
|
+
if (!this.listeners.has(event)) {
|
|
20
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
21
|
+
}
|
|
22
|
+
this.listeners.get(event).add(listener);
|
|
23
|
+
return () => {
|
|
24
|
+
const set = this.listeners.get(event);
|
|
25
|
+
if (set) {
|
|
26
|
+
set.delete(listener);
|
|
27
|
+
if (set.size === 0) {
|
|
28
|
+
this.listeners.delete(event);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Fire an event synchronously. Listener errors are caught and logged
|
|
35
|
+
* to prevent one listener from breaking others.
|
|
36
|
+
*/
|
|
37
|
+
fire(event, payload) {
|
|
38
|
+
const set = this.listeners.get(event);
|
|
39
|
+
if (!set || set.size === 0) return;
|
|
40
|
+
for (const listener of set) {
|
|
41
|
+
try {
|
|
42
|
+
listener(payload);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(`[forma] Error in "${event}" event listener:`, error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fire an event and await all async listeners sequentially.
|
|
50
|
+
* Used for preSubmit where handlers can be async.
|
|
51
|
+
*/
|
|
52
|
+
async fireAsync(event, payload) {
|
|
53
|
+
const set = this.listeners.get(event);
|
|
54
|
+
if (!set || set.size === 0) return;
|
|
55
|
+
for (const listener of set) {
|
|
56
|
+
try {
|
|
57
|
+
await listener(payload);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(`[forma] Error in "${event}" event listener:`, error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check if any listeners are registered for an event.
|
|
65
|
+
*/
|
|
66
|
+
hasListeners(event) {
|
|
67
|
+
const set = this.listeners.get(event);
|
|
68
|
+
return set !== void 0 && set.size > 0;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Remove all listeners. Called on cleanup.
|
|
72
|
+
*/
|
|
73
|
+
clear() {
|
|
74
|
+
this.listeners.clear();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/useForma.ts
|
|
11
79
|
import {
|
|
12
80
|
getVisibility,
|
|
13
81
|
getRequired,
|
|
@@ -89,7 +157,8 @@ function useForma(options) {
|
|
|
89
157
|
onChange,
|
|
90
158
|
validateOn = "blur",
|
|
91
159
|
referenceData,
|
|
92
|
-
validationDebounceMs = 0
|
|
160
|
+
validationDebounceMs = 0,
|
|
161
|
+
on: onEvents
|
|
93
162
|
} = options;
|
|
94
163
|
const spec = useMemo(() => {
|
|
95
164
|
if (!referenceData) return inputSpec;
|
|
@@ -116,6 +185,30 @@ function useForma(options) {
|
|
|
116
185
|
const stateDataRef = useRef(state.data);
|
|
117
186
|
stateDataRef.current = state.data;
|
|
118
187
|
const hasInitialized = useRef(false);
|
|
188
|
+
const emitterRef = useRef(new FormaEventEmitter());
|
|
189
|
+
const onEventsRef = useRef(onEvents);
|
|
190
|
+
onEventsRef.current = onEvents;
|
|
191
|
+
const pendingEventsRef = useRef([]);
|
|
192
|
+
const isFiringEventsRef = useRef(false);
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
const emitter = emitterRef.current;
|
|
195
|
+
return () => {
|
|
196
|
+
emitter.clear();
|
|
197
|
+
};
|
|
198
|
+
}, []);
|
|
199
|
+
const fireEvent = useCallback(
|
|
200
|
+
(event, payload) => {
|
|
201
|
+
var _a;
|
|
202
|
+
try {
|
|
203
|
+
const handler = (_a = onEventsRef.current) == null ? void 0 : _a[event];
|
|
204
|
+
if (handler) handler(payload);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error(`[forma] Error in "${event}" event handler:`, error);
|
|
207
|
+
}
|
|
208
|
+
emitterRef.current.fire(event, payload);
|
|
209
|
+
},
|
|
210
|
+
[]
|
|
211
|
+
);
|
|
119
212
|
const computed = useMemo(
|
|
120
213
|
() => calculate(state.data, spec),
|
|
121
214
|
[state.data, spec]
|
|
@@ -196,21 +289,49 @@ function useForma(options) {
|
|
|
196
289
|
},
|
|
197
290
|
[state.data]
|
|
198
291
|
);
|
|
292
|
+
const getValueAtPath = useCallback((path) => {
|
|
293
|
+
const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
294
|
+
let value = stateDataRef.current;
|
|
295
|
+
for (const part of parts) {
|
|
296
|
+
if (value === null || value === void 0) return void 0;
|
|
297
|
+
value = value[part];
|
|
298
|
+
}
|
|
299
|
+
return value;
|
|
300
|
+
}, []);
|
|
301
|
+
const queueFieldChangedEvent = useCallback(
|
|
302
|
+
(path, value, source) => {
|
|
303
|
+
if (isFiringEventsRef.current) return;
|
|
304
|
+
const previousValue = getValueAtPath(path);
|
|
305
|
+
if (previousValue === value) return;
|
|
306
|
+
pendingEventsRef.current.push({
|
|
307
|
+
event: "fieldChanged",
|
|
308
|
+
payload: { path, value, previousValue, source }
|
|
309
|
+
});
|
|
310
|
+
},
|
|
311
|
+
[getValueAtPath]
|
|
312
|
+
);
|
|
199
313
|
const setFieldValue = useCallback(
|
|
200
314
|
(path, value) => {
|
|
315
|
+
queueFieldChangedEvent(path, value, "user");
|
|
201
316
|
setNestedValue(path, value);
|
|
202
317
|
if (validateOn === "change") {
|
|
203
318
|
dispatch({ type: "SET_FIELD_TOUCHED", field: path, touched: true });
|
|
204
319
|
}
|
|
205
320
|
},
|
|
206
|
-
[validateOn, setNestedValue]
|
|
321
|
+
[validateOn, setNestedValue, queueFieldChangedEvent]
|
|
207
322
|
);
|
|
208
323
|
const setFieldTouched = useCallback((path, touched = true) => {
|
|
209
324
|
dispatch({ type: "SET_FIELD_TOUCHED", field: path, touched });
|
|
210
325
|
}, []);
|
|
211
|
-
const setValues = useCallback(
|
|
212
|
-
|
|
213
|
-
|
|
326
|
+
const setValues = useCallback(
|
|
327
|
+
(values) => {
|
|
328
|
+
for (const [key, value] of Object.entries(values)) {
|
|
329
|
+
queueFieldChangedEvent(key, value, "setValues");
|
|
330
|
+
}
|
|
331
|
+
dispatch({ type: "SET_VALUES", values });
|
|
332
|
+
},
|
|
333
|
+
[queueFieldChangedEvent]
|
|
334
|
+
);
|
|
214
335
|
const validateField = useCallback(
|
|
215
336
|
(path) => {
|
|
216
337
|
return validation.errors.filter((e) => e.field === path);
|
|
@@ -221,25 +342,83 @@ function useForma(options) {
|
|
|
221
342
|
return validation;
|
|
222
343
|
}, [validation]);
|
|
223
344
|
const submitForm = useCallback(async () => {
|
|
345
|
+
var _a;
|
|
224
346
|
dispatch({ type: "SET_SUBMITTING", isSubmitting: true });
|
|
347
|
+
const submissionData = { ...state.data };
|
|
348
|
+
let postSubmitPayload;
|
|
225
349
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
350
|
+
const preSubmitPayload = {
|
|
351
|
+
data: submissionData,
|
|
352
|
+
computed: { ...computed }
|
|
353
|
+
};
|
|
354
|
+
const declarativePreSubmit = (_a = onEventsRef.current) == null ? void 0 : _a.preSubmit;
|
|
355
|
+
if (declarativePreSubmit) {
|
|
356
|
+
await declarativePreSubmit(preSubmitPayload);
|
|
357
|
+
}
|
|
358
|
+
if (emitterRef.current.hasListeners("preSubmit")) {
|
|
359
|
+
await emitterRef.current.fireAsync("preSubmit", preSubmitPayload);
|
|
360
|
+
}
|
|
361
|
+
if (!immediateValidation.valid) {
|
|
362
|
+
postSubmitPayload = {
|
|
363
|
+
data: submissionData,
|
|
364
|
+
success: false,
|
|
365
|
+
validationErrors: immediateValidation.errors
|
|
366
|
+
};
|
|
367
|
+
} else if (onSubmit) {
|
|
368
|
+
try {
|
|
369
|
+
await onSubmit(submissionData);
|
|
370
|
+
postSubmitPayload = { data: submissionData, success: true };
|
|
371
|
+
} catch (error) {
|
|
372
|
+
postSubmitPayload = {
|
|
373
|
+
data: submissionData,
|
|
374
|
+
success: false,
|
|
375
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
postSubmitPayload = { data: submissionData, success: true };
|
|
228
380
|
}
|
|
229
381
|
dispatch({ type: "SET_SUBMITTED", isSubmitted: true });
|
|
230
382
|
} finally {
|
|
231
383
|
dispatch({ type: "SET_SUBMITTING", isSubmitting: false });
|
|
384
|
+
if (postSubmitPayload) {
|
|
385
|
+
fireEvent("postSubmit", postSubmitPayload);
|
|
386
|
+
}
|
|
232
387
|
}
|
|
233
|
-
}, [immediateValidation, onSubmit, state.data]);
|
|
388
|
+
}, [immediateValidation, onSubmit, state.data, computed, fireEvent]);
|
|
234
389
|
const resetForm = useCallback(() => {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
390
|
+
const resetData = {
|
|
391
|
+
...getDefaultBooleanValues(spec),
|
|
392
|
+
...getFieldDefaults(spec),
|
|
393
|
+
...initialData
|
|
394
|
+
};
|
|
395
|
+
if (!isFiringEventsRef.current) {
|
|
396
|
+
const currentData = stateDataRef.current;
|
|
397
|
+
const allKeys = /* @__PURE__ */ new Set([
|
|
398
|
+
...Object.keys(currentData),
|
|
399
|
+
...Object.keys(resetData)
|
|
400
|
+
]);
|
|
401
|
+
for (const key of allKeys) {
|
|
402
|
+
const currentVal = currentData[key];
|
|
403
|
+
const resetVal = resetData[key];
|
|
404
|
+
if (currentVal !== resetVal) {
|
|
405
|
+
pendingEventsRef.current.push({
|
|
406
|
+
event: "fieldChanged",
|
|
407
|
+
payload: {
|
|
408
|
+
path: key,
|
|
409
|
+
value: resetVal,
|
|
410
|
+
previousValue: currentVal,
|
|
411
|
+
source: "reset"
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
241
415
|
}
|
|
242
|
-
|
|
416
|
+
pendingEventsRef.current.push({
|
|
417
|
+
event: "formReset",
|
|
418
|
+
payload: {}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
dispatch({ type: "RESET", initialData: resetData });
|
|
243
422
|
}, [spec, initialData]);
|
|
244
423
|
const wizard = useMemo(() => {
|
|
245
424
|
if (!spec.pages || spec.pages.length === 0) return null;
|
|
@@ -270,16 +449,44 @@ function useForma(options) {
|
|
|
270
449
|
currentPage,
|
|
271
450
|
goToPage: (index) => {
|
|
272
451
|
const validIndex = Math.min(Math.max(0, index), maxPageIndex);
|
|
273
|
-
|
|
452
|
+
if (validIndex !== clampedPageIndex) {
|
|
453
|
+
dispatch({ type: "SET_PAGE", page: validIndex });
|
|
454
|
+
const newPage = visiblePages[validIndex];
|
|
455
|
+
if (newPage) {
|
|
456
|
+
fireEvent("pageChanged", {
|
|
457
|
+
fromIndex: clampedPageIndex,
|
|
458
|
+
toIndex: validIndex,
|
|
459
|
+
page: newPage
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
274
463
|
},
|
|
275
464
|
nextPage: () => {
|
|
276
465
|
if (hasNextPage) {
|
|
277
|
-
|
|
466
|
+
const toIndex = clampedPageIndex + 1;
|
|
467
|
+
dispatch({ type: "SET_PAGE", page: toIndex });
|
|
468
|
+
const newPage = visiblePages[toIndex];
|
|
469
|
+
if (newPage) {
|
|
470
|
+
fireEvent("pageChanged", {
|
|
471
|
+
fromIndex: clampedPageIndex,
|
|
472
|
+
toIndex,
|
|
473
|
+
page: newPage
|
|
474
|
+
});
|
|
475
|
+
}
|
|
278
476
|
}
|
|
279
477
|
},
|
|
280
478
|
previousPage: () => {
|
|
281
479
|
if (hasPreviousPage) {
|
|
282
|
-
|
|
480
|
+
const toIndex = clampedPageIndex - 1;
|
|
481
|
+
dispatch({ type: "SET_PAGE", page: toIndex });
|
|
482
|
+
const newPage = visiblePages[toIndex];
|
|
483
|
+
if (newPage) {
|
|
484
|
+
fireEvent("pageChanged", {
|
|
485
|
+
fromIndex: clampedPageIndex,
|
|
486
|
+
toIndex,
|
|
487
|
+
page: newPage
|
|
488
|
+
});
|
|
489
|
+
}
|
|
283
490
|
}
|
|
284
491
|
},
|
|
285
492
|
hasNextPage,
|
|
@@ -310,40 +517,51 @@ function useForma(options) {
|
|
|
310
517
|
return pageErrors.length === 0;
|
|
311
518
|
}
|
|
312
519
|
};
|
|
313
|
-
}, [spec, state.data, state.currentPage, computed, validation, visibility]);
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (parts.length === 1) {
|
|
326
|
-
dispatch({ type: "SET_FIELD_VALUE", field: path, value });
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
const newData = { ...stateDataRef.current };
|
|
330
|
-
let current = newData;
|
|
331
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
332
|
-
const part = parts[i];
|
|
333
|
-
const nextPart = parts[i + 1];
|
|
334
|
-
const isNextArrayIndex = /^\d+$/.test(nextPart);
|
|
335
|
-
if (current[part] === void 0) {
|
|
336
|
-
current[part] = isNextArrayIndex ? [] : {};
|
|
337
|
-
} else if (Array.isArray(current[part])) {
|
|
338
|
-
current[part] = [...current[part]];
|
|
339
|
-
} else {
|
|
340
|
-
current[part] = { ...current[part] };
|
|
520
|
+
}, [spec, state.data, state.currentPage, computed, validation, visibility, fireEvent]);
|
|
521
|
+
useEffect(() => {
|
|
522
|
+
const events = pendingEventsRef.current;
|
|
523
|
+
if (events.length === 0) return;
|
|
524
|
+
pendingEventsRef.current = [];
|
|
525
|
+
isFiringEventsRef.current = true;
|
|
526
|
+
try {
|
|
527
|
+
for (const pending of events) {
|
|
528
|
+
fireEvent(
|
|
529
|
+
pending.event,
|
|
530
|
+
pending.payload
|
|
531
|
+
);
|
|
341
532
|
}
|
|
342
|
-
|
|
533
|
+
} finally {
|
|
534
|
+
isFiringEventsRef.current = false;
|
|
343
535
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
536
|
+
});
|
|
537
|
+
const setValueAtPath = useCallback(
|
|
538
|
+
(path, value) => {
|
|
539
|
+
queueFieldChangedEvent(path, value, "user");
|
|
540
|
+
const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
541
|
+
if (parts.length === 1) {
|
|
542
|
+
dispatch({ type: "SET_FIELD_VALUE", field: path, value });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const newData = { ...stateDataRef.current };
|
|
546
|
+
let current = newData;
|
|
547
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
548
|
+
const part = parts[i];
|
|
549
|
+
const nextPart = parts[i + 1];
|
|
550
|
+
const isNextArrayIndex = /^\d+$/.test(nextPart);
|
|
551
|
+
if (current[part] === void 0) {
|
|
552
|
+
current[part] = isNextArrayIndex ? [] : {};
|
|
553
|
+
} else if (Array.isArray(current[part])) {
|
|
554
|
+
current[part] = [...current[part]];
|
|
555
|
+
} else {
|
|
556
|
+
current[part] = { ...current[part] };
|
|
557
|
+
}
|
|
558
|
+
current = current[part];
|
|
559
|
+
}
|
|
560
|
+
current[parts[parts.length - 1]] = value;
|
|
561
|
+
dispatch({ type: "SET_VALUES", values: newData });
|
|
562
|
+
},
|
|
563
|
+
[queueFieldChangedEvent]
|
|
564
|
+
);
|
|
347
565
|
const fieldHandlers = useRef(/* @__PURE__ */ new Map());
|
|
348
566
|
useEffect(() => {
|
|
349
567
|
const validFields = new Set(spec.fieldOrder);
|
|
@@ -563,6 +781,10 @@ function useForma(options) {
|
|
|
563
781
|
optionsVisibility
|
|
564
782
|
]
|
|
565
783
|
);
|
|
784
|
+
const on = useCallback(
|
|
785
|
+
(event, listener) => emitterRef.current.on(event, listener),
|
|
786
|
+
[]
|
|
787
|
+
);
|
|
566
788
|
return useMemo(
|
|
567
789
|
() => ({
|
|
568
790
|
data: state.data,
|
|
@@ -587,6 +809,7 @@ function useForma(options) {
|
|
|
587
809
|
validateForm,
|
|
588
810
|
submitForm,
|
|
589
811
|
resetForm,
|
|
812
|
+
on,
|
|
590
813
|
getFieldProps,
|
|
591
814
|
getSelectFieldProps,
|
|
592
815
|
getArrayHelpers
|
|
@@ -614,6 +837,7 @@ function useForma(options) {
|
|
|
614
837
|
validateForm,
|
|
615
838
|
submitForm,
|
|
616
839
|
resetForm,
|
|
840
|
+
on,
|
|
617
841
|
getFieldProps,
|
|
618
842
|
getSelectFieldProps,
|
|
619
843
|
getArrayHelpers
|