@fogpipe/forma-react 0.12.0-alpha.1 → 0.12.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/dist/index.js CHANGED
@@ -1,5 +1,12 @@
1
1
  // src/useForma.ts
2
- import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useReducer,
7
+ useRef,
8
+ useState
9
+ } from "react";
3
10
  import { isAdornableField } from "@fogpipe/forma-core";
4
11
  import {
5
12
  getVisibility,
@@ -66,7 +73,15 @@ function getDefaultBooleanValues(spec) {
66
73
  return defaults;
67
74
  }
68
75
  function useForma(options) {
69
- const { spec: inputSpec, initialData = {}, onSubmit, onChange, validateOn = "blur", referenceData, validationDebounceMs = 0 } = options;
76
+ const {
77
+ spec: inputSpec,
78
+ initialData = {},
79
+ onSubmit,
80
+ onChange,
81
+ validateOn = "blur",
82
+ referenceData,
83
+ validationDebounceMs = 0
84
+ } = options;
70
85
  const spec = useMemo(() => {
71
86
  if (!referenceData) return inputSpec;
72
87
  return {
@@ -136,33 +151,39 @@ function useForma(options) {
136
151
  hasInitialized.current = true;
137
152
  }
138
153
  }, [state.data, computed, onChange]);
139
- const setNestedValue = useCallback((path, value) => {
140
- const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
141
- if (parts.length === 1) {
142
- dispatch({ type: "SET_FIELD_VALUE", field: path, value });
143
- return;
144
- }
145
- const buildNestedObject = (data, pathParts, val) => {
146
- const result = { ...data };
147
- let current = result;
148
- for (let i = 0; i < pathParts.length - 1; i++) {
149
- const part = pathParts[i];
150
- const nextPart = pathParts[i + 1];
151
- const isNextArrayIndex = /^\d+$/.test(nextPart);
152
- if (current[part] === void 0) {
153
- current[part] = isNextArrayIndex ? [] : {};
154
- } else if (Array.isArray(current[part])) {
155
- current[part] = [...current[part]];
156
- } else {
157
- current[part] = { ...current[part] };
158
- }
159
- current = current[part];
154
+ const setNestedValue = useCallback(
155
+ (path, value) => {
156
+ const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
157
+ if (parts.length === 1) {
158
+ dispatch({ type: "SET_FIELD_VALUE", field: path, value });
159
+ return;
160
160
  }
161
- current[pathParts[pathParts.length - 1]] = val;
162
- return result;
163
- };
164
- dispatch({ type: "SET_VALUES", values: buildNestedObject(state.data, parts, value) });
165
- }, [state.data]);
161
+ const buildNestedObject = (data, pathParts, val) => {
162
+ const result = { ...data };
163
+ let current = result;
164
+ for (let i = 0; i < pathParts.length - 1; i++) {
165
+ const part = pathParts[i];
166
+ const nextPart = pathParts[i + 1];
167
+ const isNextArrayIndex = /^\d+$/.test(nextPart);
168
+ if (current[part] === void 0) {
169
+ current[part] = isNextArrayIndex ? [] : {};
170
+ } else if (Array.isArray(current[part])) {
171
+ current[part] = [...current[part]];
172
+ } else {
173
+ current[part] = { ...current[part] };
174
+ }
175
+ current = current[part];
176
+ }
177
+ current[pathParts[pathParts.length - 1]] = val;
178
+ return result;
179
+ };
180
+ dispatch({
181
+ type: "SET_VALUES",
182
+ values: buildNestedObject(state.data, parts, value)
183
+ });
184
+ },
185
+ [state.data]
186
+ );
166
187
  const setFieldValue = useCallback(
167
188
  (path, value) => {
168
189
  setNestedValue(path, value);
@@ -213,7 +234,10 @@ function useForma(options) {
213
234
  }));
214
235
  const visiblePages = pages.filter((p) => p.visible);
215
236
  const maxPageIndex = Math.max(0, visiblePages.length - 1);
216
- const clampedPageIndex = Math.min(Math.max(0, state.currentPage), maxPageIndex);
237
+ const clampedPageIndex = Math.min(
238
+ Math.max(0, state.currentPage),
239
+ maxPageIndex
240
+ );
217
241
  if (clampedPageIndex !== state.currentPage && visiblePages.length > 0) {
218
242
  dispatch({ type: "SET_PAGE", page: clampedPageIndex });
219
243
  }
@@ -321,193 +345,271 @@ function useForma(options) {
321
345
  }
322
346
  }
323
347
  }, [spec]);
324
- const getFieldHandlers = useCallback((path) => {
325
- if (!fieldHandlers.current.has(path)) {
326
- fieldHandlers.current.set(path, {
327
- onChange: (value) => setValueAtPath(path, value),
328
- onBlur: () => setFieldTouched(path)
329
- });
330
- }
331
- return fieldHandlers.current.get(path);
332
- }, [setValueAtPath, setFieldTouched]);
333
- const getFieldProps = useCallback((path) => {
334
- var _a;
335
- const fieldDef = spec.fields[path];
336
- const handlers = getFieldHandlers(path);
337
- let fieldType = (fieldDef == null ? void 0 : fieldDef.type) || "text";
338
- if (!fieldType || fieldType === "computed") {
339
- const schemaProperty2 = spec.schema.properties[path];
340
- if (schemaProperty2) {
341
- if (schemaProperty2.type === "number") fieldType = "number";
342
- else if (schemaProperty2.type === "integer") fieldType = "integer";
343
- else if (schemaProperty2.type === "boolean") fieldType = "boolean";
344
- else if (schemaProperty2.type === "array") fieldType = "array";
345
- else if (schemaProperty2.type === "object") fieldType = "object";
346
- else if ("enum" in schemaProperty2 && schemaProperty2.enum) fieldType = "select";
347
- else if ("format" in schemaProperty2) {
348
- if (schemaProperty2.format === "date") fieldType = "date";
349
- else if (schemaProperty2.format === "date-time") fieldType = "datetime";
350
- else if (schemaProperty2.format === "email") fieldType = "email";
351
- else if (schemaProperty2.format === "uri") fieldType = "url";
352
- }
348
+ const getFieldHandlers = useCallback(
349
+ (path) => {
350
+ if (!fieldHandlers.current.has(path)) {
351
+ fieldHandlers.current.set(path, {
352
+ onChange: (value) => setValueAtPath(path, value),
353
+ onBlur: () => setFieldTouched(path)
354
+ });
353
355
  }
354
- }
355
- const fieldErrors = validation.errors.filter((e) => e.field === path);
356
- const isTouched = state.touched[path] ?? false;
357
- const showErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
358
- const displayedErrors = showErrors ? fieldErrors : [];
359
- const hasErrors = displayedErrors.length > 0;
360
- const isRequired = required[path] ?? false;
361
- const schemaProperty = spec.schema.properties[path];
362
- const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
363
- const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
364
- const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);
365
- const adornerProps = fieldDef && isAdornableField(fieldDef) ? { prefix: fieldDef.prefix, suffix: fieldDef.suffix } : {};
366
- return {
367
- name: path,
368
- value: getValueAtPath(path),
369
- type: fieldType,
370
- label: (fieldDef == null ? void 0 : fieldDef.label) || path.charAt(0).toUpperCase() + path.slice(1),
371
- description: fieldDef == null ? void 0 : fieldDef.description,
372
- placeholder: fieldDef == null ? void 0 : fieldDef.placeholder,
373
- visible: visibility[path] !== false,
374
- enabled: enabled[path] !== false,
375
- readonly: readonly[path] ?? false,
376
- required: isRequired,
377
- showRequiredIndicator,
378
- touched: isTouched,
379
- errors: displayedErrors,
380
- onChange: handlers.onChange,
381
- onBlur: handlers.onBlur,
382
- // ARIA accessibility attributes
383
- "aria-invalid": hasErrors || void 0,
384
- "aria-describedby": hasErrors ? `${path}-error` : void 0,
385
- "aria-required": isRequired || void 0,
386
- // Adorner props (only for adornable field types)
387
- ...adornerProps,
388
- // Presentation variant
389
- variant: fieldDef == null ? void 0 : fieldDef.variant,
390
- variantConfig: fieldDef == null ? void 0 : fieldDef.variantConfig
391
- };
392
- }, [spec, state.touched, state.isSubmitted, visibility, enabled, readonly, required, validation.errors, validateOn, getValueAtPath, getFieldHandlers]);
393
- const getSelectFieldProps = useCallback((path) => {
394
- const baseProps = getFieldProps(path);
395
- const visibleOptions = optionsVisibility[path] ?? [];
396
- return {
397
- ...baseProps,
398
- options: visibleOptions
399
- };
400
- }, [getFieldProps, optionsVisibility]);
401
- const getArrayHelpers = useCallback((path) => {
402
- const fieldDef = spec.fields[path];
403
- const currentValue = getValueAtPath(path) ?? [];
404
- const arrayDef = (fieldDef == null ? void 0 : fieldDef.type) === "array" ? fieldDef : void 0;
405
- const minItems = (arrayDef == null ? void 0 : arrayDef.minItems) ?? 0;
406
- const maxItems = (arrayDef == null ? void 0 : arrayDef.maxItems) ?? Infinity;
407
- const canAdd = currentValue.length < maxItems;
408
- const canRemove = currentValue.length > minItems;
409
- const getItemFieldProps = (index, fieldName) => {
356
+ return fieldHandlers.current.get(path);
357
+ },
358
+ [setValueAtPath, setFieldTouched]
359
+ );
360
+ const getFieldProps = useCallback(
361
+ (path) => {
410
362
  var _a;
411
- const itemPath = `${path}[${index}].${fieldName}`;
412
- const itemFieldDef = (_a = arrayDef == null ? void 0 : arrayDef.itemFields) == null ? void 0 : _a[fieldName];
413
- const handlers = getFieldHandlers(itemPath);
414
- const item = currentValue[index] ?? {};
415
- const itemValue = item[fieldName];
416
- const fieldErrors = validation.errors.filter((e) => e.field === itemPath);
417
- const isTouched = state.touched[itemPath] ?? false;
363
+ const fieldDef = spec.fields[path];
364
+ const handlers = getFieldHandlers(path);
365
+ let fieldType = (fieldDef == null ? void 0 : fieldDef.type) || "text";
366
+ if (!fieldType || fieldType === "computed") {
367
+ const schemaProperty2 = spec.schema.properties[path];
368
+ if (schemaProperty2) {
369
+ if (schemaProperty2.type === "number") fieldType = "number";
370
+ else if (schemaProperty2.type === "integer") fieldType = "integer";
371
+ else if (schemaProperty2.type === "boolean") fieldType = "boolean";
372
+ else if (schemaProperty2.type === "array") fieldType = "array";
373
+ else if (schemaProperty2.type === "object") fieldType = "object";
374
+ else if ("enum" in schemaProperty2 && schemaProperty2.enum)
375
+ fieldType = "select";
376
+ else if ("format" in schemaProperty2) {
377
+ if (schemaProperty2.format === "date") fieldType = "date";
378
+ else if (schemaProperty2.format === "date-time")
379
+ fieldType = "datetime";
380
+ else if (schemaProperty2.format === "email") fieldType = "email";
381
+ else if (schemaProperty2.format === "uri") fieldType = "url";
382
+ }
383
+ }
384
+ }
385
+ const fieldErrors = validation.errors.filter((e) => e.field === path);
386
+ const isTouched = state.touched[path] ?? false;
418
387
  const showErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
419
- const visibleOptions = optionsVisibility[itemPath];
388
+ const displayedErrors = showErrors ? fieldErrors : [];
389
+ const hasErrors = displayedErrors.length > 0;
390
+ const isRequired = required[path] ?? false;
391
+ const schemaProperty = spec.schema.properties[path];
392
+ const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
393
+ const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
394
+ const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);
395
+ const adornerProps = fieldDef && isAdornableField(fieldDef) ? { prefix: fieldDef.prefix, suffix: fieldDef.suffix } : {};
420
396
  return {
421
- name: itemPath,
422
- value: itemValue,
423
- type: (itemFieldDef == null ? void 0 : itemFieldDef.type) || "text",
424
- label: (itemFieldDef == null ? void 0 : itemFieldDef.label) || fieldName.charAt(0).toUpperCase() + fieldName.slice(1),
425
- description: itemFieldDef == null ? void 0 : itemFieldDef.description,
426
- placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
427
- visible: true,
397
+ name: path,
398
+ value: getValueAtPath(path),
399
+ type: fieldType,
400
+ label: (fieldDef == null ? void 0 : fieldDef.label) || path.charAt(0).toUpperCase() + path.slice(1),
401
+ description: fieldDef == null ? void 0 : fieldDef.description,
402
+ placeholder: fieldDef == null ? void 0 : fieldDef.placeholder,
403
+ visible: visibility[path] !== false,
428
404
  enabled: enabled[path] !== false,
429
- readonly: readonly[itemPath] ?? false,
430
- required: false,
431
- // TODO: Evaluate item field required
432
- showRequiredIndicator: false,
433
- // Item fields don't show required indicator
405
+ readonly: readonly[path] ?? false,
406
+ required: isRequired,
407
+ showRequiredIndicator,
434
408
  touched: isTouched,
435
- errors: showErrors ? fieldErrors : [],
409
+ errors: displayedErrors,
436
410
  onChange: handlers.onChange,
437
411
  onBlur: handlers.onBlur,
412
+ // ARIA accessibility attributes
413
+ "aria-invalid": hasErrors || void 0,
414
+ "aria-describedby": hasErrors ? `${path}-error` : void 0,
415
+ "aria-required": isRequired || void 0,
416
+ // Adorner props (only for adornable field types)
417
+ ...adornerProps,
418
+ // Presentation variant
419
+ variant: fieldDef == null ? void 0 : fieldDef.variant,
420
+ variantConfig: fieldDef == null ? void 0 : fieldDef.variantConfig
421
+ };
422
+ },
423
+ [
424
+ spec,
425
+ state.touched,
426
+ state.isSubmitted,
427
+ visibility,
428
+ enabled,
429
+ readonly,
430
+ required,
431
+ validation.errors,
432
+ validateOn,
433
+ getValueAtPath,
434
+ getFieldHandlers
435
+ ]
436
+ );
437
+ const getSelectFieldProps = useCallback(
438
+ (path) => {
439
+ const baseProps = getFieldProps(path);
440
+ const visibleOptions = optionsVisibility[path] ?? [];
441
+ return {
442
+ ...baseProps,
438
443
  options: visibleOptions
439
444
  };
440
- };
441
- return {
442
- items: currentValue,
443
- push: (item) => {
444
- if (canAdd) {
445
- setValueAtPath(path, [...currentValue, item]);
446
- }
447
- },
448
- remove: (index) => {
449
- if (canRemove) {
445
+ },
446
+ [getFieldProps, optionsVisibility]
447
+ );
448
+ const getArrayHelpers = useCallback(
449
+ (path) => {
450
+ const fieldDef = spec.fields[path];
451
+ const currentValue = getValueAtPath(path) ?? [];
452
+ const arrayDef = (fieldDef == null ? void 0 : fieldDef.type) === "array" ? fieldDef : void 0;
453
+ const minItems = (arrayDef == null ? void 0 : arrayDef.minItems) ?? 0;
454
+ const maxItems = (arrayDef == null ? void 0 : arrayDef.maxItems) ?? Infinity;
455
+ const canAdd = currentValue.length < maxItems;
456
+ const canRemove = currentValue.length > minItems;
457
+ const getItemFieldProps = (index, fieldName) => {
458
+ var _a;
459
+ const itemPath = `${path}[${index}].${fieldName}`;
460
+ const itemFieldDef = (_a = arrayDef == null ? void 0 : arrayDef.itemFields) == null ? void 0 : _a[fieldName];
461
+ const handlers = getFieldHandlers(itemPath);
462
+ const item = currentValue[index] ?? {};
463
+ const itemValue = item[fieldName];
464
+ const fieldErrors = validation.errors.filter(
465
+ (e) => e.field === itemPath
466
+ );
467
+ const isTouched = state.touched[itemPath] ?? false;
468
+ const showErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
469
+ const visibleOptions = optionsVisibility[itemPath];
470
+ return {
471
+ name: itemPath,
472
+ value: itemValue,
473
+ type: (itemFieldDef == null ? void 0 : itemFieldDef.type) || "text",
474
+ label: (itemFieldDef == null ? void 0 : itemFieldDef.label) || fieldName.charAt(0).toUpperCase() + fieldName.slice(1),
475
+ description: itemFieldDef == null ? void 0 : itemFieldDef.description,
476
+ placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
477
+ visible: true,
478
+ enabled: enabled[path] !== false,
479
+ readonly: readonly[itemPath] ?? false,
480
+ required: false,
481
+ // TODO: Evaluate item field required
482
+ showRequiredIndicator: false,
483
+ // Item fields don't show required indicator
484
+ touched: isTouched,
485
+ errors: showErrors ? fieldErrors : [],
486
+ onChange: handlers.onChange,
487
+ onBlur: handlers.onBlur,
488
+ options: visibleOptions
489
+ };
490
+ };
491
+ return {
492
+ items: currentValue,
493
+ push: (item) => {
494
+ if (canAdd) {
495
+ setValueAtPath(path, [...currentValue, item]);
496
+ }
497
+ },
498
+ remove: (index) => {
499
+ if (canRemove) {
500
+ const newArray = [...currentValue];
501
+ newArray.splice(index, 1);
502
+ setValueAtPath(path, newArray);
503
+ }
504
+ },
505
+ move: (from, to) => {
450
506
  const newArray = [...currentValue];
451
- newArray.splice(index, 1);
507
+ const [item] = newArray.splice(from, 1);
508
+ newArray.splice(to, 0, item);
452
509
  setValueAtPath(path, newArray);
453
- }
454
- },
455
- move: (from, to) => {
456
- const newArray = [...currentValue];
457
- const [item] = newArray.splice(from, 1);
458
- newArray.splice(to, 0, item);
459
- setValueAtPath(path, newArray);
460
- },
461
- swap: (indexA, indexB) => {
462
- const newArray = [...currentValue];
463
- [newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];
464
- setValueAtPath(path, newArray);
465
- },
466
- insert: (index, item) => {
467
- if (canAdd) {
510
+ },
511
+ swap: (indexA, indexB) => {
468
512
  const newArray = [...currentValue];
469
- newArray.splice(index, 0, item);
513
+ [newArray[indexA], newArray[indexB]] = [
514
+ newArray[indexB],
515
+ newArray[indexA]
516
+ ];
470
517
  setValueAtPath(path, newArray);
471
- }
472
- },
473
- getItemFieldProps,
474
- minItems,
475
- maxItems,
476
- canAdd,
477
- canRemove
478
- };
479
- }, [spec.fields, getValueAtPath, setValueAtPath, getFieldHandlers, enabled, readonly, state.touched, state.isSubmitted, validation.errors, validateOn, optionsVisibility]);
480
- return {
481
- data: state.data,
482
- computed,
483
- visibility,
484
- required,
485
- enabled,
486
- readonly,
487
- optionsVisibility,
488
- touched: state.touched,
489
- errors: validation.errors,
490
- isValid: validation.valid,
491
- isSubmitting: state.isSubmitting,
492
- isSubmitted: state.isSubmitted,
493
- isDirty: state.isDirty,
494
- spec,
495
- wizard,
496
- setFieldValue,
497
- setFieldTouched,
498
- setValues,
499
- validateField,
500
- validateForm,
501
- submitForm,
502
- resetForm,
503
- getFieldProps,
504
- getSelectFieldProps,
505
- getArrayHelpers
506
- };
518
+ },
519
+ insert: (index, item) => {
520
+ if (canAdd) {
521
+ const newArray = [...currentValue];
522
+ newArray.splice(index, 0, item);
523
+ setValueAtPath(path, newArray);
524
+ }
525
+ },
526
+ getItemFieldProps,
527
+ minItems,
528
+ maxItems,
529
+ canAdd,
530
+ canRemove
531
+ };
532
+ },
533
+ [
534
+ spec.fields,
535
+ getValueAtPath,
536
+ setValueAtPath,
537
+ getFieldHandlers,
538
+ enabled,
539
+ readonly,
540
+ state.touched,
541
+ state.isSubmitted,
542
+ validation.errors,
543
+ validateOn,
544
+ optionsVisibility
545
+ ]
546
+ );
547
+ return useMemo(
548
+ () => ({
549
+ data: state.data,
550
+ computed,
551
+ visibility,
552
+ required,
553
+ enabled,
554
+ readonly,
555
+ optionsVisibility,
556
+ touched: state.touched,
557
+ errors: validation.errors,
558
+ isValid: validation.valid,
559
+ isSubmitting: state.isSubmitting,
560
+ isSubmitted: state.isSubmitted,
561
+ isDirty: state.isDirty,
562
+ spec,
563
+ wizard,
564
+ setFieldValue,
565
+ setFieldTouched,
566
+ setValues,
567
+ validateField,
568
+ validateForm,
569
+ submitForm,
570
+ resetForm,
571
+ getFieldProps,
572
+ getSelectFieldProps,
573
+ getArrayHelpers
574
+ }),
575
+ [
576
+ state.data,
577
+ state.touched,
578
+ state.isSubmitting,
579
+ state.isSubmitted,
580
+ state.isDirty,
581
+ computed,
582
+ visibility,
583
+ required,
584
+ enabled,
585
+ readonly,
586
+ optionsVisibility,
587
+ validation.errors,
588
+ validation.valid,
589
+ spec,
590
+ wizard,
591
+ setFieldValue,
592
+ setFieldTouched,
593
+ setValues,
594
+ validateField,
595
+ validateForm,
596
+ submitForm,
597
+ resetForm,
598
+ getFieldProps,
599
+ getSelectFieldProps,
600
+ getArrayHelpers
601
+ ]
602
+ );
507
603
  }
508
604
 
509
605
  // src/FormRenderer.tsx
510
- import React, { forwardRef, useImperativeHandle, useRef as useRef2, useMemo as useMemo2, useCallback as useCallback2 } from "react";
606
+ import React, {
607
+ forwardRef,
608
+ useImperativeHandle,
609
+ useRef as useRef2,
610
+ useMemo as useMemo2,
611
+ useCallback as useCallback2
612
+ } from "react";
511
613
  import { isAdornableField as isAdornableField2, isSelectionField } from "@fogpipe/forma-core";
512
614
 
513
615
  // src/context.ts
@@ -538,7 +640,14 @@ function DefaultLayout({ children, onSubmit, isSubmitting }) {
538
640
  }
539
641
  );
540
642
  }
541
- function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredIndicator, visible }) {
643
+ function DefaultFieldWrapper({
644
+ fieldPath,
645
+ field,
646
+ children,
647
+ errors,
648
+ showRequiredIndicator,
649
+ visible
650
+ }) {
542
651
  if (!visible) return null;
543
652
  const errorId = `${fieldPath}-error`;
544
653
  const descriptionId = field.description ? `${fieldPath}-description` : void 0;
@@ -563,7 +672,11 @@ function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredI
563
672
  field.description && /* @__PURE__ */ jsx("p", { id: descriptionId, className: "field-description", children: field.description })
564
673
  ] });
565
674
  }
566
- function DefaultPageWrapper({ title, description, children }) {
675
+ function DefaultPageWrapper({
676
+ title,
677
+ description,
678
+ children
679
+ }) {
567
680
  return /* @__PURE__ */ jsxs("div", { className: "page-wrapper", children: [
568
681
  /* @__PURE__ */ jsx("h2", { children: title }),
569
682
  description && /* @__PURE__ */ jsx("p", { children: description }),
@@ -642,6 +755,20 @@ var FormRenderer = forwardRef(
642
755
  }),
643
756
  [forma, focusField, focusFirstError]
644
757
  );
758
+ const {
759
+ data: formaData,
760
+ computed: formaComputed,
761
+ visibility: formaVisibility,
762
+ required: formaRequired,
763
+ enabled: formaEnabled,
764
+ readonly: formaReadonly,
765
+ optionsVisibility: formaOptionsVisibility,
766
+ touched: formaTouched,
767
+ errors: formaErrors,
768
+ setFieldValue,
769
+ setFieldTouched,
770
+ getArrayHelpers
771
+ } = forma;
645
772
  const fieldsToRender = useMemo2(() => {
646
773
  var _a;
647
774
  if (spec.pages && spec.pages.length > 0 && forma.wizard) {
@@ -653,152 +780,179 @@ var FormRenderer = forwardRef(
653
780
  }
654
781
  return spec.fieldOrder;
655
782
  }, [spec.pages, spec.fieldOrder, forma.wizard]);
656
- const renderField = useCallback2((fieldPath) => {
657
- var _a;
658
- const fieldDef = spec.fields[fieldPath];
659
- if (!fieldDef) return null;
660
- const isVisible = forma.visibility[fieldPath] !== false;
661
- if (!isVisible) return null;
662
- const fieldType = fieldDef.type;
663
- const componentKey = fieldType;
664
- const Component = components[componentKey] || components.fallback;
665
- if (!Component) {
666
- console.warn(`No component found for field type: ${fieldType}`);
667
- return null;
668
- }
669
- const errors = forma.errors.filter((e) => e.field === fieldPath);
670
- const touched = forma.touched[fieldPath] ?? false;
671
- const required = forma.required[fieldPath] ?? false;
672
- const disabled = forma.enabled[fieldPath] === false;
673
- const schemaProperty = spec.schema.properties[fieldPath];
674
- const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
675
- const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
676
- const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
677
- const isReadonly = forma.readonly[fieldPath] ?? false;
678
- const baseProps = {
679
- name: fieldPath,
680
- field: fieldDef,
681
- value: forma.data[fieldPath],
682
- touched,
683
- required,
684
- disabled,
685
- errors,
686
- onChange: (value) => forma.setFieldValue(fieldPath, value),
687
- onBlur: () => forma.setFieldTouched(fieldPath),
688
- // Convenience properties
689
- visible: true,
690
- // Always true since we already filtered for visibility
691
- enabled: !disabled,
692
- readonly: isReadonly,
693
- label: fieldDef.label ?? fieldPath,
694
- description: fieldDef.description,
695
- placeholder: fieldDef.placeholder,
696
- // Adorner properties (only for adornable field types)
697
- ...isAdornableField2(fieldDef) && {
698
- prefix: fieldDef.prefix,
699
- suffix: fieldDef.suffix
700
- },
701
- // Presentation variant
702
- variant: fieldDef.variant,
703
- variantConfig: fieldDef.variantConfig
704
- };
705
- let fieldProps = baseProps;
706
- if (fieldType === "number" || fieldType === "integer") {
707
- const constraints = getNumberConstraints(schemaProperty);
708
- fieldProps = {
709
- ...baseProps,
710
- fieldType,
711
- value: baseProps.value,
712
- onChange: baseProps.onChange,
713
- ...constraints
714
- };
715
- } else if (fieldType === "select" || fieldType === "multiselect") {
716
- const selectOptions = isSelectionField(fieldDef) ? fieldDef.options : [];
717
- fieldProps = {
718
- ...baseProps,
719
- fieldType,
720
- value: baseProps.value,
721
- onChange: baseProps.onChange,
722
- options: forma.optionsVisibility[fieldPath] ?? selectOptions ?? []
723
- };
724
- } else if (fieldType === "array" && fieldDef.type === "array" && fieldDef.itemFields) {
725
- const arrayValue = Array.isArray(baseProps.value) ? baseProps.value : [];
726
- const minItems = fieldDef.minItems ?? 0;
727
- const maxItems = fieldDef.maxItems ?? Infinity;
728
- const itemFieldDefs = fieldDef.itemFields;
729
- const baseHelpers = forma.getArrayHelpers(fieldPath);
730
- const pushWithDefault = (item) => {
731
- const newItem = item ?? createDefaultItem(itemFieldDefs);
732
- baseHelpers.push(newItem);
733
- };
734
- const getItemFieldPropsExtended = (index, fieldName) => {
735
- const baseProps2 = baseHelpers.getItemFieldProps(index, fieldName);
736
- const itemFieldDef = itemFieldDefs[fieldName];
737
- const itemPath = `${fieldPath}[${index}].${fieldName}`;
738
- return {
739
- ...baseProps2,
740
- itemIndex: index,
741
- fieldName,
742
- options: forma.optionsVisibility[itemPath] ?? (itemFieldDef && isSelectionField(itemFieldDef) ? itemFieldDef.options : void 0)
743
- };
744
- };
745
- const helpers = {
746
- items: arrayValue,
747
- push: pushWithDefault,
748
- insert: baseHelpers.insert,
749
- remove: baseHelpers.remove,
750
- move: baseHelpers.move,
751
- swap: baseHelpers.swap,
752
- getItemFieldProps: getItemFieldPropsExtended,
753
- minItems,
754
- maxItems,
755
- canAdd: arrayValue.length < maxItems,
756
- canRemove: arrayValue.length > minItems
757
- };
758
- fieldProps = {
759
- ...baseProps,
760
- fieldType: "array",
761
- value: arrayValue,
762
- onChange: baseProps.onChange,
763
- helpers,
764
- itemFields: itemFieldDefs,
765
- minItems,
766
- maxItems
767
- };
768
- } else if (fieldType === "display" && fieldDef.type === "display") {
769
- const sourceValue = fieldDef.source ? forma.data[fieldDef.source] ?? forma.computed[fieldDef.source] : void 0;
770
- const { onChange: _onChange, value: _value, ...displayBaseProps } = baseProps;
771
- fieldProps = {
772
- ...displayBaseProps,
773
- fieldType: "display",
774
- content: fieldDef.content,
775
- sourceValue,
776
- format: fieldDef.format
777
- };
778
- } else {
779
- fieldProps = {
780
- ...baseProps,
781
- fieldType,
782
- value: baseProps.value ?? "",
783
- onChange: baseProps.onChange
784
- };
785
- }
786
- const componentProps = { field: fieldProps, spec };
787
- return /* @__PURE__ */ jsx(
788
- FieldWrapper,
789
- {
790
- fieldPath,
783
+ const renderField = useCallback2(
784
+ (fieldPath) => {
785
+ var _a;
786
+ const fieldDef = spec.fields[fieldPath];
787
+ if (!fieldDef) return null;
788
+ const isVisible = formaVisibility[fieldPath] !== false;
789
+ if (!isVisible) {
790
+ return /* @__PURE__ */ jsx("div", { "data-field-path": fieldPath, hidden: true }, fieldPath);
791
+ }
792
+ const fieldType = fieldDef.type;
793
+ const componentKey = fieldType;
794
+ const Component = components[componentKey] || components.fallback;
795
+ if (!Component) {
796
+ console.warn(`No component found for field type: ${fieldType}`);
797
+ return null;
798
+ }
799
+ const errors = formaErrors.filter((e) => e.field === fieldPath);
800
+ const touched = formaTouched[fieldPath] ?? false;
801
+ const required = formaRequired[fieldPath] ?? false;
802
+ const disabled = formaEnabled[fieldPath] === false;
803
+ const schemaProperty = spec.schema.properties[fieldPath];
804
+ const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
805
+ const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
806
+ const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
807
+ const isReadonly = formaReadonly[fieldPath] ?? false;
808
+ const baseProps = {
809
+ name: fieldPath,
791
810
  field: fieldDef,
792
- errors,
811
+ value: formaData[fieldPath],
793
812
  touched,
794
813
  required,
795
- showRequiredIndicator,
796
- visible: isVisible,
797
- children: React.createElement(Component, componentProps)
798
- },
799
- fieldPath
800
- );
801
- }, [spec, forma, components, FieldWrapper]);
814
+ disabled,
815
+ errors,
816
+ onChange: (value) => setFieldValue(fieldPath, value),
817
+ onBlur: () => setFieldTouched(fieldPath),
818
+ // Convenience properties
819
+ visible: true,
820
+ // Always true since we already filtered for visibility
821
+ enabled: !disabled,
822
+ readonly: isReadonly,
823
+ label: fieldDef.label ?? fieldPath,
824
+ description: fieldDef.description,
825
+ placeholder: fieldDef.placeholder,
826
+ // Adorner properties (only for adornable field types)
827
+ ...isAdornableField2(fieldDef) && {
828
+ prefix: fieldDef.prefix,
829
+ suffix: fieldDef.suffix
830
+ },
831
+ // Presentation variant
832
+ variant: fieldDef.variant,
833
+ variantConfig: fieldDef.variantConfig
834
+ };
835
+ let fieldProps = baseProps;
836
+ if (fieldType === "number" || fieldType === "integer") {
837
+ const constraints = getNumberConstraints(schemaProperty);
838
+ fieldProps = {
839
+ ...baseProps,
840
+ fieldType,
841
+ value: baseProps.value,
842
+ onChange: baseProps.onChange,
843
+ ...constraints
844
+ };
845
+ } else if (fieldType === "select" || fieldType === "multiselect") {
846
+ const selectOptions = isSelectionField(fieldDef) ? fieldDef.options : [];
847
+ fieldProps = {
848
+ ...baseProps,
849
+ fieldType,
850
+ value: baseProps.value,
851
+ onChange: baseProps.onChange,
852
+ options: formaOptionsVisibility[fieldPath] ?? selectOptions ?? []
853
+ };
854
+ } else if (fieldType === "array" && fieldDef.type === "array" && fieldDef.itemFields) {
855
+ const arrayValue = Array.isArray(baseProps.value) ? baseProps.value : [];
856
+ const minItems = fieldDef.minItems ?? 0;
857
+ const maxItems = fieldDef.maxItems ?? Infinity;
858
+ const itemFieldDefs = fieldDef.itemFields;
859
+ const baseHelpers = getArrayHelpers(fieldPath);
860
+ const pushWithDefault = (item) => {
861
+ const newItem = item ?? createDefaultItem(itemFieldDefs);
862
+ baseHelpers.push(newItem);
863
+ };
864
+ const getItemFieldPropsExtended = (index, fieldName) => {
865
+ const baseProps2 = baseHelpers.getItemFieldProps(index, fieldName);
866
+ const itemFieldDef = itemFieldDefs[fieldName];
867
+ const itemPath = `${fieldPath}[${index}].${fieldName}`;
868
+ return {
869
+ ...baseProps2,
870
+ itemIndex: index,
871
+ fieldName,
872
+ options: formaOptionsVisibility[itemPath] ?? (itemFieldDef && isSelectionField(itemFieldDef) ? itemFieldDef.options : void 0)
873
+ };
874
+ };
875
+ const helpers = {
876
+ items: arrayValue,
877
+ push: pushWithDefault,
878
+ insert: baseHelpers.insert,
879
+ remove: baseHelpers.remove,
880
+ move: baseHelpers.move,
881
+ swap: baseHelpers.swap,
882
+ getItemFieldProps: getItemFieldPropsExtended,
883
+ minItems,
884
+ maxItems,
885
+ canAdd: arrayValue.length < maxItems,
886
+ canRemove: arrayValue.length > minItems
887
+ };
888
+ fieldProps = {
889
+ ...baseProps,
890
+ fieldType: "array",
891
+ value: arrayValue,
892
+ onChange: baseProps.onChange,
893
+ helpers,
894
+ itemFields: itemFieldDefs,
895
+ minItems,
896
+ maxItems
897
+ };
898
+ } else if (fieldType === "display" && fieldDef.type === "display") {
899
+ const sourceValue = fieldDef.source ? formaData[fieldDef.source] ?? formaComputed[fieldDef.source] : void 0;
900
+ const {
901
+ onChange: _onChange,
902
+ value: _value,
903
+ ...displayBaseProps
904
+ } = baseProps;
905
+ fieldProps = {
906
+ ...displayBaseProps,
907
+ fieldType: "display",
908
+ content: fieldDef.content,
909
+ sourceValue,
910
+ format: fieldDef.format
911
+ };
912
+ } else {
913
+ fieldProps = {
914
+ ...baseProps,
915
+ fieldType,
916
+ value: baseProps.value ?? "",
917
+ onChange: baseProps.onChange
918
+ };
919
+ }
920
+ const componentProps = { field: fieldProps, spec };
921
+ return /* @__PURE__ */ jsx("div", { "data-field-path": fieldPath, children: /* @__PURE__ */ jsx(
922
+ FieldWrapper,
923
+ {
924
+ fieldPath,
925
+ field: fieldDef,
926
+ errors,
927
+ touched,
928
+ required,
929
+ showRequiredIndicator,
930
+ visible: isVisible,
931
+ children: React.createElement(
932
+ Component,
933
+ componentProps
934
+ )
935
+ }
936
+ ) }, fieldPath);
937
+ },
938
+ [
939
+ spec,
940
+ components,
941
+ FieldWrapper,
942
+ formaData,
943
+ formaComputed,
944
+ formaVisibility,
945
+ formaRequired,
946
+ formaEnabled,
947
+ formaReadonly,
948
+ formaOptionsVisibility,
949
+ formaTouched,
950
+ formaErrors,
951
+ setFieldValue,
952
+ setFieldTouched,
953
+ getArrayHelpers
954
+ ]
955
+ );
802
956
  const renderedFields = useMemo2(
803
957
  () => fieldsToRender.map(renderField),
804
958
  [fieldsToRender, renderField]
@@ -862,7 +1016,11 @@ function createDefaultItem2(itemFields) {
862
1016
  }
863
1017
  return item;
864
1018
  }
865
- function FieldRenderer({ fieldPath, components, className }) {
1019
+ function FieldRenderer({
1020
+ fieldPath,
1021
+ components,
1022
+ className
1023
+ }) {
866
1024
  const forma = useFormaContext();
867
1025
  const { spec } = forma;
868
1026
  const fieldDef = spec.fields[fieldPath];
@@ -871,7 +1029,9 @@ function FieldRenderer({ fieldPath, components, className }) {
871
1029
  return null;
872
1030
  }
873
1031
  const isVisible = forma.visibility[fieldPath] !== false;
874
- if (!isVisible) return null;
1032
+ if (!isVisible) {
1033
+ return /* @__PURE__ */ jsx2("div", { "data-field-path": fieldPath, hidden: true });
1034
+ }
875
1035
  const fieldType = fieldDef.type;
876
1036
  const componentKey = fieldType;
877
1037
  const Component = components[componentKey] || components.fallback;
@@ -979,7 +1139,10 @@ function FieldRenderer({ fieldPath, components, className }) {
979
1139
  },
980
1140
  swap: (indexA, indexB) => {
981
1141
  const newArray = [...arrayValue];
982
- [newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];
1142
+ [newArray[indexA], newArray[indexB]] = [
1143
+ newArray[indexB],
1144
+ newArray[indexA]
1145
+ ];
983
1146
  forma.setFieldValue(fieldPath, newArray);
984
1147
  },
985
1148
  getItemFieldProps: (index, fieldName) => {
@@ -1030,7 +1193,11 @@ function FieldRenderer({ fieldPath, components, className }) {
1030
1193
  };
1031
1194
  } else if (fieldType === "display" && fieldDef.type === "display") {
1032
1195
  const sourceValue = fieldDef.source ? forma.data[fieldDef.source] ?? forma.computed[fieldDef.source] : void 0;
1033
- const { onChange: _onChange, value: _value, ...displayBaseProps } = baseProps;
1196
+ const {
1197
+ onChange: _onChange,
1198
+ value: _value,
1199
+ ...displayBaseProps
1200
+ } = baseProps;
1034
1201
  fieldProps = {
1035
1202
  ...displayBaseProps,
1036
1203
  fieldType: "display",
@@ -1047,11 +1214,14 @@ function FieldRenderer({ fieldPath, components, className }) {
1047
1214
  };
1048
1215
  }
1049
1216
  const componentProps = { field: fieldProps, spec };
1050
- const element = React2.createElement(Component, componentProps);
1217
+ const element = React2.createElement(
1218
+ Component,
1219
+ componentProps
1220
+ );
1051
1221
  if (className) {
1052
- return /* @__PURE__ */ jsx2("div", { className, children: element });
1222
+ return /* @__PURE__ */ jsx2("div", { "data-field-path": fieldPath, className, children: element });
1053
1223
  }
1054
- return element;
1224
+ return /* @__PURE__ */ jsx2("div", { "data-field-path": fieldPath, children: element });
1055
1225
  }
1056
1226
 
1057
1227
  // src/ErrorBoundary.tsx