@fogpipe/forma-react 0.17.0 → 0.18.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.
Files changed (46) hide show
  1. package/README.md +111 -26
  2. package/dist/FormRenderer-D_ZVK44t.d.ts +558 -0
  3. package/dist/chunk-5K4QITFH.js +1276 -0
  4. package/dist/chunk-5K4QITFH.js.map +1 -0
  5. package/dist/defaults/index.d.ts +56 -0
  6. package/dist/defaults/index.js +895 -0
  7. package/dist/defaults/index.js.map +1 -0
  8. package/dist/defaults/styles/forma-defaults.css +696 -0
  9. package/dist/index.d.ts +13 -549
  10. package/dist/index.js +34 -1273
  11. package/dist/index.js.map +1 -1
  12. package/package.json +17 -3
  13. package/src/FieldRenderer.tsx +12 -4
  14. package/src/FormRenderer.tsx +26 -9
  15. package/src/__tests__/FieldRenderer.test.tsx +5 -1
  16. package/src/__tests__/FormRenderer.test.tsx +146 -0
  17. package/src/__tests__/canProceed.test.ts +243 -0
  18. package/src/__tests__/defaults/components.test.tsx +818 -0
  19. package/src/__tests__/defaults/integration.test.tsx +494 -0
  20. package/src/__tests__/defaults/layout.test.tsx +298 -0
  21. package/src/__tests__/events.test.ts +15 -5
  22. package/src/__tests__/useForma.test.ts +108 -5
  23. package/src/defaults/DefaultFormRenderer.tsx +43 -0
  24. package/src/defaults/componentMap.ts +45 -0
  25. package/src/defaults/components/ArrayField.tsx +183 -0
  26. package/src/defaults/components/BooleanInput.tsx +32 -0
  27. package/src/defaults/components/ComputedDisplay.tsx +26 -0
  28. package/src/defaults/components/DateInput.tsx +59 -0
  29. package/src/defaults/components/DisplayField.tsx +15 -0
  30. package/src/defaults/components/FallbackField.tsx +35 -0
  31. package/src/defaults/components/MatrixField.tsx +98 -0
  32. package/src/defaults/components/MultiSelectInput.tsx +51 -0
  33. package/src/defaults/components/NumberInput.tsx +73 -0
  34. package/src/defaults/components/ObjectField.tsx +22 -0
  35. package/src/defaults/components/SelectInput.tsx +44 -0
  36. package/src/defaults/components/TextInput.tsx +48 -0
  37. package/src/defaults/components/TextareaInput.tsx +46 -0
  38. package/src/defaults/index.ts +33 -0
  39. package/src/defaults/layout/FieldWrapper.tsx +83 -0
  40. package/src/defaults/layout/FormLayout.tsx +34 -0
  41. package/src/defaults/layout/PageWrapper.tsx +18 -0
  42. package/src/defaults/layout/WizardLayout.tsx +130 -0
  43. package/src/defaults/styles/forma-defaults.css +696 -0
  44. package/src/events.ts +4 -1
  45. package/src/types.ts +16 -4
  46. package/src/useForma.ts +48 -34
package/dist/index.js CHANGED
@@ -1,933 +1,14 @@
1
- // src/useForma.ts
2
1
  import {
3
- useCallback,
4
- useEffect,
5
- useMemo,
6
- useReducer,
7
- useRef,
8
- useState
9
- } from "react";
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
79
- import {
80
- getVisibility,
81
- getRequired,
82
- getEnabled,
83
- getReadonly,
84
- validate,
85
- calculate,
86
- getPageVisibility,
87
- getOptionsVisibility
88
- } from "@fogpipe/forma-core";
89
- function formReducer(state, action) {
90
- switch (action.type) {
91
- case "SET_FIELD_VALUE":
92
- return {
93
- ...state,
94
- data: { ...state.data, [action.field]: action.value },
95
- isDirty: true,
96
- isSubmitted: false
97
- // Clear on data change
98
- };
99
- case "SET_FIELD_TOUCHED":
100
- return {
101
- ...state,
102
- touched: { ...state.touched, [action.field]: action.touched }
103
- };
104
- case "SET_VALUES":
105
- return {
106
- ...state,
107
- data: { ...state.data, ...action.values },
108
- isDirty: true,
109
- isSubmitted: false
110
- // Clear on data change
111
- };
112
- case "SET_SUBMITTING":
113
- return { ...state, isSubmitting: action.isSubmitting };
114
- case "SET_SUBMITTED":
115
- return { ...state, isSubmitted: action.isSubmitted };
116
- case "SET_PAGE":
117
- return { ...state, currentPage: action.page };
118
- case "RESET":
119
- return {
120
- data: action.initialData,
121
- touched: {},
122
- isSubmitting: false,
123
- isSubmitted: false,
124
- isDirty: false,
125
- currentPage: 0
126
- };
127
- default:
128
- return state;
129
- }
130
- }
131
- function getDefaultBooleanValues(spec) {
132
- var _a;
133
- const defaults = {};
134
- for (const fieldPath of spec.fieldOrder) {
135
- const schemaProperty = (_a = spec.schema.properties) == null ? void 0 : _a[fieldPath];
136
- const fieldDef = spec.fields[fieldPath];
137
- if ((schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean") {
138
- defaults[fieldPath] = false;
139
- }
140
- }
141
- return defaults;
142
- }
143
- function getFieldDefaults(spec) {
144
- const defaults = {};
145
- for (const [fieldPath, fieldDef] of Object.entries(spec.fields)) {
146
- if (fieldDef.defaultValue !== void 0) {
147
- defaults[fieldPath] = fieldDef.defaultValue;
148
- }
149
- }
150
- return defaults;
151
- }
152
- function useForma(options) {
153
- const {
154
- spec: inputSpec,
155
- initialData = {},
156
- onSubmit,
157
- onChange,
158
- validateOn = "blur",
159
- referenceData,
160
- validationDebounceMs = 0,
161
- on: onEvents
162
- } = options;
163
- const spec = useMemo(() => {
164
- if (!referenceData) return inputSpec;
165
- return {
166
- ...inputSpec,
167
- referenceData: {
168
- ...inputSpec.referenceData,
169
- ...referenceData
170
- }
171
- };
172
- }, [inputSpec, referenceData]);
173
- const [state, dispatch] = useReducer(formReducer, {
174
- data: {
175
- ...getDefaultBooleanValues(spec),
176
- ...getFieldDefaults(spec),
177
- ...initialData
178
- },
179
- touched: {},
180
- isSubmitting: false,
181
- isSubmitted: false,
182
- isDirty: false,
183
- currentPage: 0
184
- });
185
- const stateDataRef = useRef(state.data);
186
- stateDataRef.current = state.data;
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
- );
212
- const computed = useMemo(
213
- () => calculate(state.data, spec),
214
- [state.data, spec]
215
- );
216
- const visibility = useMemo(
217
- () => getVisibility(state.data, spec, { computed }),
218
- [state.data, spec, computed]
219
- );
220
- const required = useMemo(
221
- () => getRequired(state.data, spec, { computed }),
222
- [state.data, spec, computed]
223
- );
224
- const enabled = useMemo(
225
- () => getEnabled(state.data, spec, { computed }),
226
- [state.data, spec, computed]
227
- );
228
- const readonly = useMemo(
229
- () => getReadonly(state.data, spec, { computed }),
230
- [state.data, spec, computed]
231
- );
232
- const optionsVisibility = useMemo(
233
- () => getOptionsVisibility(state.data, spec, { computed }),
234
- [state.data, spec, computed]
235
- );
236
- const immediateValidation = useMemo(
237
- () => validate(state.data, spec, { computed, onlyVisible: true }),
238
- [state.data, spec, computed]
239
- );
240
- const [debouncedValidation, setDebouncedValidation] = useState(immediateValidation);
241
- useEffect(() => {
242
- if (validationDebounceMs <= 0) {
243
- setDebouncedValidation(immediateValidation);
244
- return;
245
- }
246
- const timeoutId = setTimeout(() => {
247
- setDebouncedValidation(immediateValidation);
248
- }, validationDebounceMs);
249
- return () => clearTimeout(timeoutId);
250
- }, [immediateValidation, validationDebounceMs]);
251
- const validation = validationDebounceMs > 0 ? debouncedValidation : immediateValidation;
252
- useEffect(() => {
253
- if (hasInitialized.current) {
254
- onChange == null ? void 0 : onChange(state.data, computed);
255
- } else {
256
- hasInitialized.current = true;
257
- }
258
- }, [state.data, computed, onChange]);
259
- const setNestedValue = useCallback(
260
- (path, value) => {
261
- const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
262
- if (parts.length === 1) {
263
- dispatch({ type: "SET_FIELD_VALUE", field: path, value });
264
- return;
265
- }
266
- const buildNestedObject = (data, pathParts, val) => {
267
- const result = { ...data };
268
- let current = result;
269
- for (let i = 0; i < pathParts.length - 1; i++) {
270
- const part = pathParts[i];
271
- const nextPart = pathParts[i + 1];
272
- const isNextArrayIndex = /^\d+$/.test(nextPart);
273
- if (current[part] === void 0) {
274
- current[part] = isNextArrayIndex ? [] : {};
275
- } else if (Array.isArray(current[part])) {
276
- current[part] = [...current[part]];
277
- } else {
278
- current[part] = { ...current[part] };
279
- }
280
- current = current[part];
281
- }
282
- current[pathParts[pathParts.length - 1]] = val;
283
- return result;
284
- };
285
- dispatch({
286
- type: "SET_VALUES",
287
- values: buildNestedObject(state.data, parts, value)
288
- });
289
- },
290
- [state.data]
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
- );
313
- const setFieldValue = useCallback(
314
- (path, value) => {
315
- queueFieldChangedEvent(path, value, "user");
316
- setNestedValue(path, value);
317
- if (validateOn === "change") {
318
- dispatch({ type: "SET_FIELD_TOUCHED", field: path, touched: true });
319
- }
320
- },
321
- [validateOn, setNestedValue, queueFieldChangedEvent]
322
- );
323
- const setFieldTouched = useCallback((path, touched = true) => {
324
- dispatch({ type: "SET_FIELD_TOUCHED", field: path, touched });
325
- }, []);
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
- );
335
- const validateField = useCallback(
336
- (path) => {
337
- return validation.errors.filter((e) => e.field === path);
338
- },
339
- [validation]
340
- );
341
- const validateForm = useCallback(() => {
342
- return validation;
343
- }, [validation]);
344
- const submitForm = useCallback(async () => {
345
- var _a;
346
- dispatch({ type: "SET_SUBMITTING", isSubmitting: true });
347
- const submissionData = { ...state.data };
348
- let postSubmitPayload;
349
- try {
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 };
380
- }
381
- dispatch({ type: "SET_SUBMITTED", isSubmitted: true });
382
- } finally {
383
- dispatch({ type: "SET_SUBMITTING", isSubmitting: false });
384
- if (postSubmitPayload) {
385
- fireEvent("postSubmit", postSubmitPayload);
386
- }
387
- }
388
- }, [immediateValidation, onSubmit, state.data, computed, fireEvent]);
389
- const resetForm = useCallback(() => {
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
- }
415
- }
416
- pendingEventsRef.current.push({
417
- event: "formReset",
418
- payload: {}
419
- });
420
- }
421
- dispatch({ type: "RESET", initialData: resetData });
422
- }, [spec, initialData]);
423
- const wizard = useMemo(() => {
424
- if (!spec.pages || spec.pages.length === 0) return null;
425
- const pageVisibility = getPageVisibility(state.data, spec, { computed });
426
- const pages = spec.pages.map((p) => ({
427
- id: p.id,
428
- title: p.title,
429
- description: p.description,
430
- visible: pageVisibility[p.id] !== false,
431
- fields: p.fields
432
- }));
433
- const visiblePages = pages.filter((p) => p.visible);
434
- const maxPageIndex = Math.max(0, visiblePages.length - 1);
435
- const clampedPageIndex = Math.min(
436
- Math.max(0, state.currentPage),
437
- maxPageIndex
438
- );
439
- if (clampedPageIndex !== state.currentPage && visiblePages.length > 0) {
440
- dispatch({ type: "SET_PAGE", page: clampedPageIndex });
441
- }
442
- const currentPage = visiblePages[clampedPageIndex] || null;
443
- const hasNextPage = clampedPageIndex < visiblePages.length - 1;
444
- const hasPreviousPage = clampedPageIndex > 0;
445
- const isLastPage = clampedPageIndex === visiblePages.length - 1;
446
- return {
447
- pages,
448
- currentPageIndex: clampedPageIndex,
449
- currentPage,
450
- goToPage: (index) => {
451
- const validIndex = Math.min(Math.max(0, index), maxPageIndex);
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
- }
463
- },
464
- nextPage: () => {
465
- if (hasNextPage) {
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
- }
476
- }
477
- },
478
- previousPage: () => {
479
- if (hasPreviousPage) {
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
- }
490
- }
491
- },
492
- hasNextPage,
493
- hasPreviousPage,
494
- canProceed: (() => {
495
- if (!currentPage) return true;
496
- const pageErrors = validation.errors.filter((e) => {
497
- const isOnCurrentPage = currentPage.fields.includes(e.field) || currentPage.fields.some((f) => e.field.startsWith(`${f}[`));
498
- const isVisible = visibility[e.field] !== false;
499
- const isError = e.severity === "error";
500
- return isOnCurrentPage && isVisible && isError;
501
- });
502
- return pageErrors.length === 0;
503
- })(),
504
- isLastPage,
505
- touchCurrentPageFields: () => {
506
- if (currentPage) {
507
- currentPage.fields.forEach((field) => {
508
- dispatch({ type: "SET_FIELD_TOUCHED", field, touched: true });
509
- });
510
- }
511
- },
512
- validateCurrentPage: () => {
513
- if (!currentPage) return true;
514
- const pageErrors = validation.errors.filter(
515
- (e) => currentPage.fields.includes(e.field)
516
- );
517
- return pageErrors.length === 0;
518
- }
519
- };
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
- );
532
- }
533
- } finally {
534
- isFiringEventsRef.current = false;
535
- }
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
- );
565
- const fieldHandlers = useRef(/* @__PURE__ */ new Map());
566
- useEffect(() => {
567
- const validFields = new Set(spec.fieldOrder);
568
- for (const fieldId of spec.fieldOrder) {
569
- const fieldDef = spec.fields[fieldId];
570
- if ((fieldDef == null ? void 0 : fieldDef.type) === "array" && fieldDef.itemFields) {
571
- for (const key of fieldHandlers.current.keys()) {
572
- if (key.startsWith(`${fieldId}[`)) {
573
- validFields.add(key);
574
- }
575
- }
576
- }
577
- }
578
- for (const key of fieldHandlers.current.keys()) {
579
- const baseField = key.split("[")[0];
580
- if (!validFields.has(key) && !validFields.has(baseField)) {
581
- fieldHandlers.current.delete(key);
582
- }
583
- }
584
- }, [spec]);
585
- const getFieldHandlers = useCallback(
586
- (path) => {
587
- if (!fieldHandlers.current.has(path)) {
588
- fieldHandlers.current.set(path, {
589
- onChange: (value) => setValueAtPath(path, value),
590
- onBlur: () => setFieldTouched(path)
591
- });
592
- }
593
- return fieldHandlers.current.get(path);
594
- },
595
- [setValueAtPath, setFieldTouched]
596
- );
597
- const getFieldProps = useCallback(
598
- (path) => {
599
- var _a;
600
- const fieldDef = spec.fields[path];
601
- const handlers = getFieldHandlers(path);
602
- let fieldType = (fieldDef == null ? void 0 : fieldDef.type) || "text";
603
- if (!fieldType || fieldType === "computed") {
604
- const schemaProperty2 = spec.schema.properties[path];
605
- if (schemaProperty2) {
606
- if (schemaProperty2.type === "number") fieldType = "number";
607
- else if (schemaProperty2.type === "integer") fieldType = "integer";
608
- else if (schemaProperty2.type === "boolean") fieldType = "boolean";
609
- else if (schemaProperty2.type === "array") fieldType = "array";
610
- else if (schemaProperty2.type === "object") fieldType = "object";
611
- else if ("enum" in schemaProperty2 && schemaProperty2.enum)
612
- fieldType = "select";
613
- else if ("format" in schemaProperty2) {
614
- if (schemaProperty2.format === "date") fieldType = "date";
615
- else if (schemaProperty2.format === "date-time")
616
- fieldType = "datetime";
617
- else if (schemaProperty2.format === "email") fieldType = "email";
618
- else if (schemaProperty2.format === "uri") fieldType = "url";
619
- }
620
- }
621
- }
622
- const fieldErrors = validation.errors.filter((e) => e.field === path);
623
- const isTouched = state.touched[path] ?? false;
624
- const showErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
625
- const displayedErrors = showErrors ? fieldErrors : [];
626
- const hasErrors = displayedErrors.length > 0;
627
- const isRequired = required[path] ?? false;
628
- const schemaProperty = spec.schema.properties[path];
629
- const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
630
- const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
631
- const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);
632
- const adornerProps = fieldDef && isAdornableField(fieldDef) ? { prefix: fieldDef.prefix, suffix: fieldDef.suffix } : {};
633
- return {
634
- name: path,
635
- value: getValueAtPath(path),
636
- type: fieldType,
637
- label: (fieldDef == null ? void 0 : fieldDef.label) || path.charAt(0).toUpperCase() + path.slice(1),
638
- description: fieldDef == null ? void 0 : fieldDef.description,
639
- placeholder: fieldDef == null ? void 0 : fieldDef.placeholder,
640
- visible: visibility[path] !== false,
641
- enabled: enabled[path] !== false,
642
- readonly: readonly[path] ?? false,
643
- required: isRequired,
644
- showRequiredIndicator,
645
- touched: isTouched,
646
- errors: displayedErrors,
647
- onChange: handlers.onChange,
648
- onBlur: handlers.onBlur,
649
- // ARIA accessibility attributes
650
- "aria-invalid": hasErrors || void 0,
651
- "aria-describedby": hasErrors ? `${path}-error` : void 0,
652
- "aria-required": isRequired || void 0,
653
- // Adorner props (only for adornable field types)
654
- ...adornerProps,
655
- // Presentation variant
656
- variant: fieldDef == null ? void 0 : fieldDef.variant,
657
- variantConfig: fieldDef == null ? void 0 : fieldDef.variantConfig
658
- };
659
- },
660
- [
661
- spec,
662
- state.touched,
663
- state.isSubmitted,
664
- visibility,
665
- enabled,
666
- readonly,
667
- required,
668
- validation.errors,
669
- validateOn,
670
- getValueAtPath,
671
- getFieldHandlers
672
- ]
673
- );
674
- const getSelectFieldProps = useCallback(
675
- (path) => {
676
- const baseProps = getFieldProps(path);
677
- const visibleOptions = optionsVisibility[path] ?? [];
678
- return {
679
- ...baseProps,
680
- options: visibleOptions
681
- };
682
- },
683
- [getFieldProps, optionsVisibility]
684
- );
685
- const getArrayHelpers = useCallback(
686
- (path) => {
687
- const fieldDef = spec.fields[path];
688
- const currentValue = getValueAtPath(path) ?? [];
689
- const arrayDef = (fieldDef == null ? void 0 : fieldDef.type) === "array" ? fieldDef : void 0;
690
- const minItems = (arrayDef == null ? void 0 : arrayDef.minItems) ?? 0;
691
- const maxItems = (arrayDef == null ? void 0 : arrayDef.maxItems) ?? Infinity;
692
- const canAdd = currentValue.length < maxItems;
693
- const canRemove = currentValue.length > minItems;
694
- const getItemFieldProps = (index, fieldName) => {
695
- var _a;
696
- const itemPath = `${path}[${index}].${fieldName}`;
697
- const itemFieldDef = (_a = arrayDef == null ? void 0 : arrayDef.itemFields) == null ? void 0 : _a[fieldName];
698
- const handlers = getFieldHandlers(itemPath);
699
- const item = currentValue[index] ?? {};
700
- const itemValue = item[fieldName];
701
- const fieldErrors = validation.errors.filter(
702
- (e) => e.field === itemPath
703
- );
704
- const isTouched = state.touched[itemPath] ?? false;
705
- const showErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
706
- const visibleOptions = optionsVisibility[itemPath];
707
- return {
708
- name: itemPath,
709
- value: itemValue,
710
- type: (itemFieldDef == null ? void 0 : itemFieldDef.type) || "text",
711
- label: (itemFieldDef == null ? void 0 : itemFieldDef.label) || fieldName.charAt(0).toUpperCase() + fieldName.slice(1),
712
- description: itemFieldDef == null ? void 0 : itemFieldDef.description,
713
- placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
714
- visible: true,
715
- enabled: enabled[path] !== false,
716
- readonly: readonly[itemPath] ?? false,
717
- required: false,
718
- // TODO: Evaluate item field required
719
- showRequiredIndicator: false,
720
- // Item fields don't show required indicator
721
- touched: isTouched,
722
- errors: showErrors ? fieldErrors : [],
723
- onChange: handlers.onChange,
724
- onBlur: handlers.onBlur,
725
- options: visibleOptions
726
- };
727
- };
728
- return {
729
- items: currentValue,
730
- push: (item) => {
731
- if (canAdd) {
732
- setValueAtPath(path, [...currentValue, item]);
733
- }
734
- },
735
- remove: (index) => {
736
- if (canRemove) {
737
- const newArray = [...currentValue];
738
- newArray.splice(index, 1);
739
- setValueAtPath(path, newArray);
740
- }
741
- },
742
- move: (from, to) => {
743
- const newArray = [...currentValue];
744
- const [item] = newArray.splice(from, 1);
745
- newArray.splice(to, 0, item);
746
- setValueAtPath(path, newArray);
747
- },
748
- swap: (indexA, indexB) => {
749
- const newArray = [...currentValue];
750
- [newArray[indexA], newArray[indexB]] = [
751
- newArray[indexB],
752
- newArray[indexA]
753
- ];
754
- setValueAtPath(path, newArray);
755
- },
756
- insert: (index, item) => {
757
- if (canAdd) {
758
- const newArray = [...currentValue];
759
- newArray.splice(index, 0, item);
760
- setValueAtPath(path, newArray);
761
- }
762
- },
763
- getItemFieldProps,
764
- minItems,
765
- maxItems,
766
- canAdd,
767
- canRemove
768
- };
769
- },
770
- [
771
- spec.fields,
772
- getValueAtPath,
773
- setValueAtPath,
774
- getFieldHandlers,
775
- enabled,
776
- readonly,
777
- state.touched,
778
- state.isSubmitted,
779
- validation.errors,
780
- validateOn,
781
- optionsVisibility
782
- ]
783
- );
784
- const on = useCallback(
785
- (event, listener) => emitterRef.current.on(event, listener),
786
- []
787
- );
788
- return useMemo(
789
- () => ({
790
- data: state.data,
791
- computed,
792
- visibility,
793
- required,
794
- enabled,
795
- readonly,
796
- optionsVisibility,
797
- touched: state.touched,
798
- errors: validation.errors,
799
- isValid: validation.valid,
800
- isSubmitting: state.isSubmitting,
801
- isSubmitted: state.isSubmitted,
802
- isDirty: state.isDirty,
803
- spec,
804
- wizard,
805
- setFieldValue,
806
- setFieldTouched,
807
- setValues,
808
- validateField,
809
- validateForm,
810
- submitForm,
811
- resetForm,
812
- on,
813
- getFieldProps,
814
- getSelectFieldProps,
815
- getArrayHelpers
816
- }),
817
- [
818
- state.data,
819
- state.touched,
820
- state.isSubmitting,
821
- state.isSubmitted,
822
- state.isDirty,
823
- computed,
824
- visibility,
825
- required,
826
- enabled,
827
- readonly,
828
- optionsVisibility,
829
- validation.errors,
830
- validation.valid,
831
- spec,
832
- wizard,
833
- setFieldValue,
834
- setFieldTouched,
835
- setValues,
836
- validateField,
837
- validateForm,
838
- submitForm,
839
- resetForm,
840
- on,
841
- getFieldProps,
842
- getSelectFieldProps,
843
- getArrayHelpers
844
- ]
845
- );
846
- }
847
-
848
- // src/FormRenderer.tsx
849
- import React, {
850
- forwardRef,
851
- useImperativeHandle,
852
- useRef as useRef2,
853
- useMemo as useMemo2,
854
- useCallback as useCallback2
855
- } from "react";
856
- import { isAdornableField as isAdornableField2, isSelectionField } from "@fogpipe/forma-core";
857
-
858
- // src/context.ts
859
- import { createContext, useContext } from "react";
860
- var FormaContext = createContext(null);
861
- function useFormaContext() {
862
- const context = useContext(FormaContext);
863
- if (!context) {
864
- throw new Error(
865
- "useFormaContext must be used within a FormaContext.Provider"
866
- );
867
- }
868
- return context;
869
- }
2
+ FormRenderer,
3
+ FormaContext,
4
+ useForma,
5
+ useFormaContext
6
+ } from "./chunk-5K4QITFH.js";
870
7
 
871
- // src/FormRenderer.tsx
872
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
873
- function DefaultLayout({ children, onSubmit, isSubmitting }) {
874
- return /* @__PURE__ */ jsxs(
875
- "form",
876
- {
877
- onSubmit: (e) => {
878
- e.preventDefault();
879
- onSubmit();
880
- },
881
- children: [
882
- children,
883
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: isSubmitting, children: isSubmitting ? "Submitting..." : "Submit" })
884
- ]
885
- }
886
- );
887
- }
888
- function DefaultFieldWrapper({
889
- fieldPath,
890
- field,
891
- children,
892
- errors,
893
- showRequiredIndicator,
894
- visible
895
- }) {
896
- if (!visible) return null;
897
- const errorId = `${fieldPath}-error`;
898
- const descriptionId = field.description ? `${fieldPath}-description` : void 0;
899
- const hasErrors = errors.length > 0;
900
- return /* @__PURE__ */ jsxs("div", { className: "field-wrapper", "data-field-path": fieldPath, children: [
901
- field.label && /* @__PURE__ */ jsxs("label", { htmlFor: fieldPath, children: [
902
- field.label,
903
- showRequiredIndicator && /* @__PURE__ */ jsx("span", { className: "required", "aria-hidden": "true", children: "*" }),
904
- showRequiredIndicator && /* @__PURE__ */ jsx("span", { className: "sr-only", children: " (required)" })
905
- ] }),
906
- children,
907
- hasErrors && /* @__PURE__ */ jsx(
908
- "div",
909
- {
910
- id: errorId,
911
- className: "field-errors",
912
- role: "alert",
913
- "aria-live": "polite",
914
- children: errors.map((error, i) => /* @__PURE__ */ jsx("span", { className: "error", children: error.message }, i))
915
- }
916
- ),
917
- field.description && /* @__PURE__ */ jsx("p", { id: descriptionId, className: "field-description", children: field.description })
918
- ] });
919
- }
920
- function DefaultPageWrapper({
921
- title,
922
- description,
923
- children
924
- }) {
925
- return /* @__PURE__ */ jsxs("div", { className: "page-wrapper", children: [
926
- /* @__PURE__ */ jsx("h2", { children: title }),
927
- description && /* @__PURE__ */ jsx("p", { children: description }),
928
- children
929
- ] });
930
- }
8
+ // src/FieldRenderer.tsx
9
+ import React from "react";
10
+ import { isAdornableField } from "@fogpipe/forma-core";
11
+ import { jsx } from "react/jsx-runtime";
931
12
  function getNumberConstraints(schema) {
932
13
  if (!schema) return {};
933
14
  if (schema.type !== "number" && schema.type !== "integer") return {};
@@ -956,332 +37,6 @@ function createDefaultItem(itemFields) {
956
37
  }
957
38
  return item;
958
39
  }
959
- var FormRenderer = forwardRef(
960
- function FormRenderer2(props, ref) {
961
- const {
962
- spec,
963
- initialData,
964
- onSubmit,
965
- onChange,
966
- components,
967
- layout: Layout = DefaultLayout,
968
- fieldWrapper: FieldWrapper = DefaultFieldWrapper,
969
- pageWrapper: PageWrapper = DefaultPageWrapper,
970
- validateOn
971
- } = props;
972
- const forma = useForma({
973
- spec,
974
- initialData,
975
- onSubmit,
976
- onChange,
977
- validateOn
978
- });
979
- const fieldRefs = useRef2(/* @__PURE__ */ new Map());
980
- const focusField = useCallback2((path) => {
981
- const element = fieldRefs.current.get(path);
982
- element == null ? void 0 : element.focus();
983
- }, []);
984
- const focusFirstError = useCallback2(() => {
985
- const firstError = forma.errors[0];
986
- if (firstError) {
987
- focusField(firstError.field);
988
- }
989
- }, [forma.errors, focusField]);
990
- useImperativeHandle(
991
- ref,
992
- () => ({
993
- submitForm: forma.submitForm,
994
- resetForm: forma.resetForm,
995
- validateForm: forma.validateForm,
996
- focusField,
997
- focusFirstError,
998
- getValues: () => forma.data,
999
- setValues: forma.setValues,
1000
- isValid: forma.isValid,
1001
- isDirty: forma.isDirty
1002
- }),
1003
- [forma, focusField, focusFirstError]
1004
- );
1005
- const {
1006
- data: formaData,
1007
- computed: formaComputed,
1008
- visibility: formaVisibility,
1009
- required: formaRequired,
1010
- enabled: formaEnabled,
1011
- readonly: formaReadonly,
1012
- optionsVisibility: formaOptionsVisibility,
1013
- touched: formaTouched,
1014
- errors: formaErrors,
1015
- setFieldValue,
1016
- setFieldTouched,
1017
- getArrayHelpers
1018
- } = forma;
1019
- const fieldsToRender = useMemo2(() => {
1020
- var _a;
1021
- if (spec.pages && spec.pages.length > 0 && forma.wizard) {
1022
- const currentPage = forma.wizard.currentPage;
1023
- if (currentPage) {
1024
- return currentPage.fields;
1025
- }
1026
- return ((_a = spec.pages[0]) == null ? void 0 : _a.fields) ?? [];
1027
- }
1028
- return spec.fieldOrder;
1029
- }, [spec.pages, spec.fieldOrder, forma.wizard]);
1030
- const renderField = useCallback2(
1031
- (fieldPath) => {
1032
- var _a;
1033
- const fieldDef = spec.fields[fieldPath];
1034
- if (!fieldDef) return null;
1035
- const isVisible = formaVisibility[fieldPath] !== false;
1036
- if (!isVisible) {
1037
- return /* @__PURE__ */ jsx("div", { "data-field-path": fieldPath, hidden: true }, fieldPath);
1038
- }
1039
- const fieldType = fieldDef.type;
1040
- const componentKey = fieldType;
1041
- const Component = components[componentKey] || components.fallback;
1042
- if (!Component) {
1043
- console.warn(`No component found for field type: ${fieldType}`);
1044
- return null;
1045
- }
1046
- const errors = formaErrors.filter((e) => e.field === fieldPath);
1047
- const touched = formaTouched[fieldPath] ?? false;
1048
- const required = formaRequired[fieldPath] ?? false;
1049
- const disabled = formaEnabled[fieldPath] === false;
1050
- const schemaProperty = spec.schema.properties[fieldPath];
1051
- const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
1052
- const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
1053
- const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
1054
- const isReadonly = formaReadonly[fieldPath] ?? false;
1055
- const baseProps = {
1056
- name: fieldPath,
1057
- field: fieldDef,
1058
- value: formaData[fieldPath],
1059
- touched,
1060
- required,
1061
- disabled,
1062
- errors,
1063
- onChange: (value) => setFieldValue(fieldPath, value),
1064
- onBlur: () => setFieldTouched(fieldPath),
1065
- // Convenience properties
1066
- visible: true,
1067
- // Always true since we already filtered for visibility
1068
- enabled: !disabled,
1069
- readonly: isReadonly,
1070
- label: fieldDef.label ?? fieldPath,
1071
- description: fieldDef.description,
1072
- placeholder: fieldDef.placeholder,
1073
- // Adorner properties (only for adornable field types)
1074
- ...isAdornableField2(fieldDef) && {
1075
- prefix: fieldDef.prefix,
1076
- suffix: fieldDef.suffix
1077
- },
1078
- // Presentation variant
1079
- variant: fieldDef.variant,
1080
- variantConfig: fieldDef.variantConfig
1081
- };
1082
- let fieldProps = baseProps;
1083
- if (fieldType === "number" || fieldType === "integer") {
1084
- const constraints = getNumberConstraints(schemaProperty);
1085
- fieldProps = {
1086
- ...baseProps,
1087
- fieldType,
1088
- value: baseProps.value,
1089
- onChange: baseProps.onChange,
1090
- ...constraints
1091
- };
1092
- } else if (fieldType === "select" || fieldType === "multiselect") {
1093
- const selectOptions = isSelectionField(fieldDef) ? fieldDef.options : [];
1094
- fieldProps = {
1095
- ...baseProps,
1096
- fieldType,
1097
- value: baseProps.value,
1098
- onChange: baseProps.onChange,
1099
- options: formaOptionsVisibility[fieldPath] ?? selectOptions ?? []
1100
- };
1101
- } else if (fieldType === "array" && fieldDef.type === "array" && fieldDef.itemFields) {
1102
- const arrayValue = Array.isArray(baseProps.value) ? baseProps.value : [];
1103
- const minItems = fieldDef.minItems ?? 0;
1104
- const maxItems = fieldDef.maxItems ?? Infinity;
1105
- const itemFieldDefs = fieldDef.itemFields;
1106
- const baseHelpers = getArrayHelpers(fieldPath);
1107
- const pushWithDefault = (item) => {
1108
- const newItem = item ?? createDefaultItem(itemFieldDefs);
1109
- baseHelpers.push(newItem);
1110
- };
1111
- const getItemFieldPropsExtended = (index, fieldName) => {
1112
- const baseProps2 = baseHelpers.getItemFieldProps(index, fieldName);
1113
- const itemFieldDef = itemFieldDefs[fieldName];
1114
- const itemPath = `${fieldPath}[${index}].${fieldName}`;
1115
- return {
1116
- ...baseProps2,
1117
- itemIndex: index,
1118
- fieldName,
1119
- options: formaOptionsVisibility[itemPath] ?? (itemFieldDef && isSelectionField(itemFieldDef) ? itemFieldDef.options : void 0)
1120
- };
1121
- };
1122
- const helpers = {
1123
- items: arrayValue,
1124
- push: pushWithDefault,
1125
- insert: baseHelpers.insert,
1126
- remove: baseHelpers.remove,
1127
- move: baseHelpers.move,
1128
- swap: baseHelpers.swap,
1129
- getItemFieldProps: getItemFieldPropsExtended,
1130
- minItems,
1131
- maxItems,
1132
- canAdd: arrayValue.length < maxItems,
1133
- canRemove: arrayValue.length > minItems
1134
- };
1135
- fieldProps = {
1136
- ...baseProps,
1137
- fieldType: "array",
1138
- value: arrayValue,
1139
- onChange: baseProps.onChange,
1140
- helpers,
1141
- itemFields: itemFieldDefs,
1142
- itemFieldOrder: fieldDef.itemFieldOrder,
1143
- minItems,
1144
- maxItems
1145
- };
1146
- } else if (fieldType === "matrix" && fieldDef.type === "matrix") {
1147
- const matrixValue = baseProps.value ?? null;
1148
- const rows = fieldDef.rows.map((row) => ({
1149
- id: row.id,
1150
- label: row.label,
1151
- visible: formaVisibility[`${fieldPath}.${row.id}`] !== false
1152
- }));
1153
- fieldProps = {
1154
- ...baseProps,
1155
- fieldType: "matrix",
1156
- value: matrixValue,
1157
- onChange: baseProps.onChange,
1158
- rows,
1159
- columns: fieldDef.columns,
1160
- multiSelect: fieldDef.multiSelect ?? false
1161
- };
1162
- } else if (fieldType === "display" && fieldDef.type === "display") {
1163
- const sourceValue = fieldDef.source ? formaData[fieldDef.source] ?? formaComputed[fieldDef.source] : void 0;
1164
- const {
1165
- onChange: _onChange,
1166
- value: _value,
1167
- ...displayBaseProps
1168
- } = baseProps;
1169
- fieldProps = {
1170
- ...displayBaseProps,
1171
- fieldType: "display",
1172
- content: fieldDef.content,
1173
- sourceValue,
1174
- format: fieldDef.format
1175
- };
1176
- } else {
1177
- fieldProps = {
1178
- ...baseProps,
1179
- fieldType,
1180
- value: baseProps.value ?? "",
1181
- onChange: baseProps.onChange
1182
- };
1183
- }
1184
- const componentProps = { field: fieldProps, spec };
1185
- return /* @__PURE__ */ jsx("div", { "data-field-path": fieldPath, children: /* @__PURE__ */ jsx(
1186
- FieldWrapper,
1187
- {
1188
- fieldPath,
1189
- field: fieldDef,
1190
- errors,
1191
- touched,
1192
- required,
1193
- showRequiredIndicator,
1194
- visible: isVisible,
1195
- children: React.createElement(
1196
- Component,
1197
- componentProps
1198
- )
1199
- }
1200
- ) }, fieldPath);
1201
- },
1202
- [
1203
- spec,
1204
- components,
1205
- FieldWrapper,
1206
- formaData,
1207
- formaComputed,
1208
- formaVisibility,
1209
- formaRequired,
1210
- formaEnabled,
1211
- formaReadonly,
1212
- formaOptionsVisibility,
1213
- formaTouched,
1214
- formaErrors,
1215
- setFieldValue,
1216
- setFieldTouched,
1217
- getArrayHelpers
1218
- ]
1219
- );
1220
- const renderedFields = useMemo2(
1221
- () => fieldsToRender.map(renderField),
1222
- [fieldsToRender, renderField]
1223
- );
1224
- const content = useMemo2(() => {
1225
- if (spec.pages && spec.pages.length > 0 && forma.wizard) {
1226
- const currentPage = forma.wizard.currentPage;
1227
- if (!currentPage) return null;
1228
- return /* @__PURE__ */ jsx(
1229
- PageWrapper,
1230
- {
1231
- title: currentPage.title,
1232
- description: currentPage.description,
1233
- pageIndex: forma.wizard.currentPageIndex,
1234
- totalPages: forma.wizard.pages.length,
1235
- children: renderedFields
1236
- }
1237
- );
1238
- }
1239
- return /* @__PURE__ */ jsx(Fragment, { children: renderedFields });
1240
- }, [spec.pages, forma.wizard, PageWrapper, renderedFields]);
1241
- return /* @__PURE__ */ jsx(FormaContext.Provider, { value: forma, children: /* @__PURE__ */ jsx(
1242
- Layout,
1243
- {
1244
- onSubmit: forma.submitForm,
1245
- isSubmitting: forma.isSubmitting,
1246
- isValid: forma.isValid,
1247
- children: content
1248
- }
1249
- ) });
1250
- }
1251
- );
1252
-
1253
- // src/FieldRenderer.tsx
1254
- import React2 from "react";
1255
- import { isAdornableField as isAdornableField3 } from "@fogpipe/forma-core";
1256
- import { jsx as jsx2 } from "react/jsx-runtime";
1257
- function getNumberConstraints2(schema) {
1258
- if (!schema) return {};
1259
- if (schema.type !== "number" && schema.type !== "integer") return {};
1260
- const min = "minimum" in schema && typeof schema.minimum === "number" ? schema.minimum : void 0;
1261
- const max = "maximum" in schema && typeof schema.maximum === "number" ? schema.maximum : void 0;
1262
- let step;
1263
- if ("multipleOf" in schema && typeof schema.multipleOf === "number") {
1264
- step = schema.multipleOf;
1265
- } else if (schema.type === "integer") {
1266
- step = 1;
1267
- }
1268
- return { min, max, step };
1269
- }
1270
- function createDefaultItem2(itemFields) {
1271
- const item = {};
1272
- for (const [fieldName, fieldDef] of Object.entries(itemFields)) {
1273
- if (fieldDef.defaultValue !== void 0) {
1274
- item[fieldName] = fieldDef.defaultValue;
1275
- } else if (fieldDef.type === "boolean") {
1276
- item[fieldName] = false;
1277
- } else if (fieldDef.type === "number" || fieldDef.type === "integer") {
1278
- item[fieldName] = null;
1279
- } else {
1280
- item[fieldName] = "";
1281
- }
1282
- }
1283
- return item;
1284
- }
1285
40
  function FieldRenderer({
1286
41
  fieldPath,
1287
42
  components,
@@ -1296,7 +51,7 @@ function FieldRenderer({
1296
51
  }
1297
52
  const isVisible = forma.visibility[fieldPath] !== false;
1298
53
  if (!isVisible) {
1299
- return /* @__PURE__ */ jsx2("div", { "data-field-path": fieldPath, hidden: true });
54
+ return /* @__PURE__ */ jsx("div", { "data-field-path": fieldPath, hidden: true });
1300
55
  }
1301
56
  const fieldType = fieldDef.type;
1302
57
  const componentKey = fieldType;
@@ -1307,6 +62,7 @@ function FieldRenderer({
1307
62
  }
1308
63
  const errors = forma.errors.filter((e) => e.field === fieldPath);
1309
64
  const touched = forma.touched[fieldPath] ?? false;
65
+ const visibleErrors = touched || forma.isSubmitted ? errors : [];
1310
66
  const required = forma.required[fieldPath] ?? false;
1311
67
  const disabled = forma.enabled[fieldPath] === false;
1312
68
  const schemaProperty = spec.schema.properties[fieldPath];
@@ -1319,6 +75,7 @@ function FieldRenderer({
1319
75
  required,
1320
76
  disabled,
1321
77
  errors,
78
+ visibleErrors,
1322
79
  onChange: (value) => forma.setFieldValue(fieldPath, value),
1323
80
  onBlur: () => forma.setFieldTouched(fieldPath),
1324
81
  // Convenience properties
@@ -1330,7 +87,7 @@ function FieldRenderer({
1330
87
  description: fieldDef.description,
1331
88
  placeholder: fieldDef.placeholder,
1332
89
  // Adorner properties (only for adornable field types)
1333
- ...isAdornableField3(fieldDef) && {
90
+ ...isAdornableField(fieldDef) && {
1334
91
  prefix: fieldDef.prefix,
1335
92
  suffix: fieldDef.suffix
1336
93
  },
@@ -1340,7 +97,7 @@ function FieldRenderer({
1340
97
  };
1341
98
  let fieldProps = baseProps;
1342
99
  if (fieldType === "number") {
1343
- const constraints = getNumberConstraints2(schemaProperty);
100
+ const constraints = getNumberConstraints(schemaProperty);
1344
101
  fieldProps = {
1345
102
  ...baseProps,
1346
103
  fieldType: "number",
@@ -1349,7 +106,7 @@ function FieldRenderer({
1349
106
  ...constraints
1350
107
  };
1351
108
  } else if (fieldType === "integer") {
1352
- const constraints = getNumberConstraints2(schemaProperty);
109
+ const constraints = getNumberConstraints(schemaProperty);
1353
110
  fieldProps = {
1354
111
  ...baseProps,
1355
112
  fieldType: "integer",
@@ -1384,7 +141,7 @@ function FieldRenderer({
1384
141
  const helpers = {
1385
142
  items: arrayValue,
1386
143
  push: (item) => {
1387
- const newItem = item ?? createDefaultItem2(itemFieldDefs);
144
+ const newItem = item ?? createDefaultItem(itemFieldDefs);
1388
145
  forma.setFieldValue(fieldPath, [...arrayValue, newItem]);
1389
146
  },
1390
147
  insert: (index, item) => {
@@ -1478,9 +235,13 @@ function FieldRenderer({
1478
235
  const sourceValue = fieldDef.source ? forma.data[fieldDef.source] ?? forma.computed[fieldDef.source] : void 0;
1479
236
  const {
1480
237
  onChange: _onChange,
238
+ // omit from display props
1481
239
  value: _value,
240
+ // omit from display props
1482
241
  ...displayBaseProps
1483
242
  } = baseProps;
243
+ void _onChange;
244
+ void _value;
1484
245
  fieldProps = {
1485
246
  ...displayBaseProps,
1486
247
  fieldType: "display",
@@ -1497,34 +258,34 @@ function FieldRenderer({
1497
258
  };
1498
259
  }
1499
260
  const componentProps = { field: fieldProps, spec };
1500
- const element = React2.createElement(
261
+ const element = React.createElement(
1501
262
  Component,
1502
263
  componentProps
1503
264
  );
1504
265
  if (className) {
1505
- return /* @__PURE__ */ jsx2("div", { "data-field-path": fieldPath, className, children: element });
266
+ return /* @__PURE__ */ jsx("div", { "data-field-path": fieldPath, className, children: element });
1506
267
  }
1507
- return /* @__PURE__ */ jsx2("div", { "data-field-path": fieldPath, children: element });
268
+ return /* @__PURE__ */ jsx("div", { "data-field-path": fieldPath, children: element });
1508
269
  }
1509
270
 
1510
271
  // src/ErrorBoundary.tsx
1511
- import React3 from "react";
1512
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
272
+ import React2 from "react";
273
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1513
274
  function DefaultErrorFallback({
1514
275
  error,
1515
276
  onReset
1516
277
  }) {
1517
- return /* @__PURE__ */ jsxs2("div", { className: "forma-error-boundary", role: "alert", children: [
1518
- /* @__PURE__ */ jsx3("h3", { children: "Something went wrong" }),
1519
- /* @__PURE__ */ jsx3("p", { children: "An error occurred while rendering the form." }),
1520
- /* @__PURE__ */ jsxs2("details", { children: [
1521
- /* @__PURE__ */ jsx3("summary", { children: "Error details" }),
1522
- /* @__PURE__ */ jsx3("pre", { children: error.message })
278
+ return /* @__PURE__ */ jsxs("div", { className: "forma-error-boundary", role: "alert", children: [
279
+ /* @__PURE__ */ jsx2("h3", { children: "Something went wrong" }),
280
+ /* @__PURE__ */ jsx2("p", { children: "An error occurred while rendering the form." }),
281
+ /* @__PURE__ */ jsxs("details", { children: [
282
+ /* @__PURE__ */ jsx2("summary", { children: "Error details" }),
283
+ /* @__PURE__ */ jsx2("pre", { children: error.message })
1523
284
  ] }),
1524
- /* @__PURE__ */ jsx3("button", { type: "button", onClick: onReset, children: "Try again" })
285
+ /* @__PURE__ */ jsx2("button", { type: "button", onClick: onReset, children: "Try again" })
1525
286
  ] });
1526
287
  }
1527
- var FormaErrorBoundary = class extends React3.Component {
288
+ var FormaErrorBoundary = class extends React2.Component {
1528
289
  constructor(props) {
1529
290
  super(props);
1530
291
  this.state = { hasError: false, error: null };
@@ -1553,7 +314,7 @@ var FormaErrorBoundary = class extends React3.Component {
1553
314
  if (fallback) {
1554
315
  return fallback;
1555
316
  }
1556
- return /* @__PURE__ */ jsx3(DefaultErrorFallback, { error: this.state.error, onReset: this.reset });
317
+ return /* @__PURE__ */ jsx2(DefaultErrorFallback, { error: this.state.error, onReset: this.reset });
1557
318
  }
1558
319
  return this.props.children;
1559
320
  }