@fogpipe/forma-react 0.11.1 → 0.12.0-alpha.1

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.d.ts CHANGED
@@ -34,12 +34,22 @@ interface BaseFieldProps {
34
34
  visible: boolean;
35
35
  /** Whether field is enabled (inverse of disabled) */
36
36
  enabled: boolean;
37
+ /** Whether field is readonly (visible, not editable, value still submitted) */
38
+ readonly: boolean;
37
39
  /** Display label from field definition */
38
40
  label: string;
39
41
  /** Help text or description from field definition */
40
42
  description?: string;
41
43
  /** Placeholder text from field definition */
42
44
  placeholder?: string;
45
+ /** Prefix adorner text (e.g., "$") - only for adornable field types */
46
+ prefix?: string;
47
+ /** Suffix adorner text (e.g., "kg") - only for adornable field types */
48
+ suffix?: string;
49
+ /** Presentation variant hint (e.g., "slider", "radio", "nps") */
50
+ variant?: string;
51
+ /** Variant-specific configuration */
52
+ variantConfig?: Record<string, unknown>;
43
53
  }
44
54
  /**
45
55
  * Props for text-based fields (text, email, password, url, textarea)
@@ -226,10 +236,25 @@ interface ArrayItemFieldProps extends Omit<BaseFieldProps, "value" | "onChange">
226
236
  /** Field name within the item */
227
237
  fieldName: string;
228
238
  }
239
+ /**
240
+ * Props for display fields (read-only presentation content)
241
+ */
242
+ interface DisplayFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
243
+ fieldType: "display";
244
+ /** Static content (markdown/text) */
245
+ content?: string;
246
+ /** Computed source value (resolved by useForma from display field's source property) */
247
+ sourceValue?: unknown;
248
+ /** Display format string */
249
+ format?: string;
250
+ /** No onChange - display fields are read-only */
251
+ onChange?: never;
252
+ value?: never;
253
+ }
229
254
  /**
230
255
  * Union of all field prop types
231
256
  */
232
- type FieldProps = TextFieldProps | NumberFieldProps | IntegerFieldProps | BooleanFieldProps | DateFieldProps | DateTimeFieldProps | SelectFieldProps | MultiSelectFieldProps | ArrayFieldProps | ObjectFieldProps | ComputedFieldProps;
257
+ type FieldProps = TextFieldProps | NumberFieldProps | IntegerFieldProps | BooleanFieldProps | DateFieldProps | DateTimeFieldProps | SelectFieldProps | MultiSelectFieldProps | ArrayFieldProps | ObjectFieldProps | ComputedFieldProps | DisplayFieldProps;
233
258
  /**
234
259
  * Map of field types to React components
235
260
  * Components receive wrapper props with { field, spec } structure
@@ -250,6 +275,7 @@ interface ComponentMap {
250
275
  array?: React.ComponentType<ArrayComponentProps>;
251
276
  object?: React.ComponentType<ObjectComponentProps>;
252
277
  computed?: React.ComponentType<ComputedComponentProps>;
278
+ display?: React.ComponentType<DisplayComponentProps>;
253
279
  fallback?: React.ComponentType<FieldComponentProps>;
254
280
  }
255
281
  /**
@@ -338,6 +364,10 @@ interface ComputedComponentProps {
338
364
  field: ComputedFieldProps;
339
365
  spec: Forma;
340
366
  }
367
+ interface DisplayComponentProps {
368
+ field: DisplayFieldProps;
369
+ spec: Forma;
370
+ }
341
371
  /**
342
372
  * Generic field component props (for fallback/dynamic components)
343
373
  */
@@ -367,6 +397,8 @@ interface GetFieldPropsResult {
367
397
  visible: boolean;
368
398
  /** Whether field is enabled (not disabled) */
369
399
  enabled: boolean;
400
+ /** Whether field is readonly (visible, not editable, value still submitted) */
401
+ readonly: boolean;
370
402
  /** Whether field is required (for validation) */
371
403
  required: boolean;
372
404
  /**
@@ -390,6 +422,14 @@ interface GetFieldPropsResult {
390
422
  "aria-required"?: boolean;
391
423
  /** Options for select/multiselect fields (filtered by visibleWhen) */
392
424
  options?: SelectOption[];
425
+ /** Prefix adorner text (e.g., "$") */
426
+ prefix?: string;
427
+ /** Suffix adorner text (e.g., "kg") */
428
+ suffix?: string;
429
+ /** Presentation variant hint */
430
+ variant?: string;
431
+ /** Variant-specific configuration */
432
+ variantConfig?: Record<string, unknown>;
393
433
  }
394
434
  /**
395
435
  * Select field props returned by getSelectFieldProps()
@@ -501,6 +541,8 @@ interface UseFormaReturn {
501
541
  required: Record<string, boolean>;
502
542
  /** Field enabled state map */
503
543
  enabled: Record<string, boolean>;
544
+ /** Field readonly state map */
545
+ readonly: Record<string, boolean>;
504
546
  /** Visible options for select/multiselect fields, keyed by field path */
505
547
  optionsVisibility: OptionsVisibilityResult;
506
548
  /** Field touched state map */
@@ -680,4 +722,4 @@ declare const FormaContext: React$1.Context<UseFormaReturn | null>;
680
722
  */
681
723
  declare function useFormaContext(): UseFormaReturn;
682
724
 
683
- 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 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 };
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 };
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  // src/useForma.ts
2
2
  import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
3
+ import { isAdornableField } from "@fogpipe/forma-core";
3
4
  import {
4
5
  getVisibility,
5
6
  getRequired,
6
7
  getEnabled,
8
+ getReadonly,
7
9
  validate,
8
10
  calculate,
9
11
  getPageVisibility,
@@ -103,6 +105,10 @@ function useForma(options) {
103
105
  () => getEnabled(state.data, spec, { computed }),
104
106
  [state.data, spec, computed]
105
107
  );
108
+ const readonly = useMemo(
109
+ () => getReadonly(state.data, spec, { computed }),
110
+ [state.data, spec, computed]
111
+ );
106
112
  const optionsVisibility = useMemo(
107
113
  () => getOptionsVisibility(state.data, spec, { computed }),
108
114
  [state.data, spec, computed]
@@ -300,7 +306,7 @@ function useForma(options) {
300
306
  const validFields = new Set(spec.fieldOrder);
301
307
  for (const fieldId of spec.fieldOrder) {
302
308
  const fieldDef = spec.fields[fieldId];
303
- if (fieldDef == null ? void 0 : fieldDef.itemFields) {
309
+ if ((fieldDef == null ? void 0 : fieldDef.type) === "array" && fieldDef.itemFields) {
304
310
  for (const key of fieldHandlers.current.keys()) {
305
311
  if (key.startsWith(`${fieldId}[`)) {
306
312
  validFields.add(key);
@@ -356,6 +362,7 @@ function useForma(options) {
356
362
  const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
357
363
  const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
358
364
  const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);
365
+ const adornerProps = fieldDef && isAdornableField(fieldDef) ? { prefix: fieldDef.prefix, suffix: fieldDef.suffix } : {};
359
366
  return {
360
367
  name: path,
361
368
  value: getValueAtPath(path),
@@ -365,6 +372,7 @@ function useForma(options) {
365
372
  placeholder: fieldDef == null ? void 0 : fieldDef.placeholder,
366
373
  visible: visibility[path] !== false,
367
374
  enabled: enabled[path] !== false,
375
+ readonly: readonly[path] ?? false,
368
376
  required: isRequired,
369
377
  showRequiredIndicator,
370
378
  touched: isTouched,
@@ -374,9 +382,14 @@ function useForma(options) {
374
382
  // ARIA accessibility attributes
375
383
  "aria-invalid": hasErrors || void 0,
376
384
  "aria-describedby": hasErrors ? `${path}-error` : void 0,
377
- "aria-required": isRequired || 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
378
391
  };
379
- }, [spec, state.touched, state.isSubmitted, visibility, enabled, required, validation.errors, validateOn, getValueAtPath, getFieldHandlers]);
392
+ }, [spec, state.touched, state.isSubmitted, visibility, enabled, readonly, required, validation.errors, validateOn, getValueAtPath, getFieldHandlers]);
380
393
  const getSelectFieldProps = useCallback((path) => {
381
394
  const baseProps = getFieldProps(path);
382
395
  const visibleOptions = optionsVisibility[path] ?? [];
@@ -388,14 +401,15 @@ function useForma(options) {
388
401
  const getArrayHelpers = useCallback((path) => {
389
402
  const fieldDef = spec.fields[path];
390
403
  const currentValue = getValueAtPath(path) ?? [];
391
- const minItems = (fieldDef == null ? void 0 : fieldDef.minItems) ?? 0;
392
- const maxItems = (fieldDef == null ? void 0 : fieldDef.maxItems) ?? Infinity;
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;
393
407
  const canAdd = currentValue.length < maxItems;
394
408
  const canRemove = currentValue.length > minItems;
395
409
  const getItemFieldProps = (index, fieldName) => {
396
410
  var _a;
397
411
  const itemPath = `${path}[${index}].${fieldName}`;
398
- const itemFieldDef = (_a = fieldDef == null ? void 0 : fieldDef.itemFields) == null ? void 0 : _a[fieldName];
412
+ const itemFieldDef = (_a = arrayDef == null ? void 0 : arrayDef.itemFields) == null ? void 0 : _a[fieldName];
399
413
  const handlers = getFieldHandlers(itemPath);
400
414
  const item = currentValue[index] ?? {};
401
415
  const itemValue = item[fieldName];
@@ -412,6 +426,7 @@ function useForma(options) {
412
426
  placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
413
427
  visible: true,
414
428
  enabled: enabled[path] !== false,
429
+ readonly: readonly[itemPath] ?? false,
415
430
  required: false,
416
431
  // TODO: Evaluate item field required
417
432
  showRequiredIndicator: false,
@@ -461,13 +476,14 @@ function useForma(options) {
461
476
  canAdd,
462
477
  canRemove
463
478
  };
464
- }, [spec.fields, getValueAtPath, setValueAtPath, getFieldHandlers, enabled, state.touched, state.isSubmitted, validation.errors, validateOn, optionsVisibility]);
479
+ }, [spec.fields, getValueAtPath, setValueAtPath, getFieldHandlers, enabled, readonly, state.touched, state.isSubmitted, validation.errors, validateOn, optionsVisibility]);
465
480
  return {
466
481
  data: state.data,
467
482
  computed,
468
483
  visibility,
469
484
  required,
470
485
  enabled,
486
+ readonly,
471
487
  optionsVisibility,
472
488
  touched: state.touched,
473
489
  errors: validation.errors,
@@ -492,6 +508,7 @@ function useForma(options) {
492
508
 
493
509
  // src/FormRenderer.tsx
494
510
  import React, { forwardRef, useImperativeHandle, useRef as useRef2, useMemo as useMemo2, useCallback as useCallback2 } from "react";
511
+ import { isAdornableField as isAdornableField2, isSelectionField } from "@fogpipe/forma-core";
495
512
 
496
513
  // src/context.ts
497
514
  import { createContext, useContext } from "react";
@@ -642,7 +659,7 @@ var FormRenderer = forwardRef(
642
659
  if (!fieldDef) return null;
643
660
  const isVisible = forma.visibility[fieldPath] !== false;
644
661
  if (!isVisible) return null;
645
- const fieldType = fieldDef.type || (fieldDef.itemFields ? "array" : "text");
662
+ const fieldType = fieldDef.type;
646
663
  const componentKey = fieldType;
647
664
  const Component = components[componentKey] || components.fallback;
648
665
  if (!Component) {
@@ -657,6 +674,7 @@ var FormRenderer = forwardRef(
657
674
  const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
658
675
  const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
659
676
  const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
677
+ const isReadonly = forma.readonly[fieldPath] ?? false;
660
678
  const baseProps = {
661
679
  name: fieldPath,
662
680
  field: fieldDef,
@@ -671,9 +689,18 @@ var FormRenderer = forwardRef(
671
689
  visible: true,
672
690
  // Always true since we already filtered for visibility
673
691
  enabled: !disabled,
692
+ readonly: isReadonly,
674
693
  label: fieldDef.label ?? fieldPath,
675
694
  description: fieldDef.description,
676
- placeholder: fieldDef.placeholder
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
677
704
  };
678
705
  let fieldProps = baseProps;
679
706
  if (fieldType === "number" || fieldType === "integer") {
@@ -686,14 +713,15 @@ var FormRenderer = forwardRef(
686
713
  ...constraints
687
714
  };
688
715
  } else if (fieldType === "select" || fieldType === "multiselect") {
716
+ const selectOptions = isSelectionField(fieldDef) ? fieldDef.options : [];
689
717
  fieldProps = {
690
718
  ...baseProps,
691
719
  fieldType,
692
720
  value: baseProps.value,
693
721
  onChange: baseProps.onChange,
694
- options: fieldDef.options ?? []
722
+ options: forma.optionsVisibility[fieldPath] ?? selectOptions ?? []
695
723
  };
696
- } else if (fieldType === "array" && fieldDef.itemFields) {
724
+ } else if (fieldType === "array" && fieldDef.type === "array" && fieldDef.itemFields) {
697
725
  const arrayValue = Array.isArray(baseProps.value) ? baseProps.value : [];
698
726
  const minItems = fieldDef.minItems ?? 0;
699
727
  const maxItems = fieldDef.maxItems ?? Infinity;
@@ -706,11 +734,12 @@ var FormRenderer = forwardRef(
706
734
  const getItemFieldPropsExtended = (index, fieldName) => {
707
735
  const baseProps2 = baseHelpers.getItemFieldProps(index, fieldName);
708
736
  const itemFieldDef = itemFieldDefs[fieldName];
737
+ const itemPath = `${fieldPath}[${index}].${fieldName}`;
709
738
  return {
710
739
  ...baseProps2,
711
740
  itemIndex: index,
712
741
  fieldName,
713
- options: itemFieldDef == null ? void 0 : itemFieldDef.options
742
+ options: forma.optionsVisibility[itemPath] ?? (itemFieldDef && isSelectionField(itemFieldDef) ? itemFieldDef.options : void 0)
714
743
  };
715
744
  };
716
745
  const helpers = {
@@ -736,6 +765,16 @@ var FormRenderer = forwardRef(
736
765
  minItems,
737
766
  maxItems
738
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
+ };
739
778
  } else {
740
779
  fieldProps = {
741
780
  ...baseProps,
@@ -795,6 +834,7 @@ var FormRenderer = forwardRef(
795
834
 
796
835
  // src/FieldRenderer.tsx
797
836
  import React2 from "react";
837
+ import { isAdornableField as isAdornableField3 } from "@fogpipe/forma-core";
798
838
  import { jsx as jsx2 } from "react/jsx-runtime";
799
839
  function getNumberConstraints2(schema) {
800
840
  if (!schema) return {};
@@ -832,7 +872,7 @@ function FieldRenderer({ fieldPath, components, className }) {
832
872
  }
833
873
  const isVisible = forma.visibility[fieldPath] !== false;
834
874
  if (!isVisible) return null;
835
- const fieldType = fieldDef.type || (fieldDef.itemFields ? "array" : "text");
875
+ const fieldType = fieldDef.type;
836
876
  const componentKey = fieldType;
837
877
  const Component = components[componentKey] || components.fallback;
838
878
  if (!Component) {
@@ -844,6 +884,7 @@ function FieldRenderer({ fieldPath, components, className }) {
844
884
  const required = forma.required[fieldPath] ?? false;
845
885
  const disabled = forma.enabled[fieldPath] === false;
846
886
  const schemaProperty = spec.schema.properties[fieldPath];
887
+ const isReadonly = forma.readonly[fieldPath] ?? false;
847
888
  const baseProps = {
848
889
  name: fieldPath,
849
890
  field: fieldDef,
@@ -858,9 +899,18 @@ function FieldRenderer({ fieldPath, components, className }) {
858
899
  visible: true,
859
900
  // Always true since we already filtered for visibility
860
901
  enabled: !disabled,
902
+ readonly: isReadonly,
861
903
  label: fieldDef.label ?? fieldPath,
862
904
  description: fieldDef.description,
863
- placeholder: fieldDef.placeholder
905
+ placeholder: fieldDef.placeholder,
906
+ // Adorner properties (only for adornable field types)
907
+ ...isAdornableField3(fieldDef) && {
908
+ prefix: fieldDef.prefix,
909
+ suffix: fieldDef.suffix
910
+ },
911
+ // Presentation variant
912
+ variant: fieldDef.variant,
913
+ variantConfig: fieldDef.variantConfig
864
914
  };
865
915
  let fieldProps = baseProps;
866
916
  if (fieldType === "number") {
@@ -900,7 +950,7 @@ function FieldRenderer({ fieldPath, components, className }) {
900
950
  onChange: baseProps.onChange,
901
951
  options: visibleOptions
902
952
  };
903
- } else if (fieldType === "array" && fieldDef.itemFields) {
953
+ } else if (fieldType === "array" && fieldDef.type === "array" && fieldDef.itemFields) {
904
954
  const arrayValue = baseProps.value ?? [];
905
955
  const minItems = fieldDef.minItems ?? 0;
906
956
  const maxItems = fieldDef.maxItems ?? Infinity;
@@ -947,6 +997,7 @@ function FieldRenderer({ fieldPath, components, className }) {
947
997
  placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
948
998
  visible: true,
949
999
  enabled: !disabled,
1000
+ readonly: forma.readonly[itemPath] ?? false,
950
1001
  required: (itemFieldDef == null ? void 0 : itemFieldDef.requiredWhen) === "true",
951
1002
  touched: forma.touched[itemPath] ?? false,
952
1003
  errors: forma.errors.filter((e) => e.field === itemPath),
@@ -977,6 +1028,16 @@ function FieldRenderer({ fieldPath, components, className }) {
977
1028
  minItems,
978
1029
  maxItems
979
1030
  };
1031
+ } else if (fieldType === "display" && fieldDef.type === "display") {
1032
+ const sourceValue = fieldDef.source ? forma.data[fieldDef.source] ?? forma.computed[fieldDef.source] : void 0;
1033
+ const { onChange: _onChange, value: _value, ...displayBaseProps } = baseProps;
1034
+ fieldProps = {
1035
+ ...displayBaseProps,
1036
+ fieldType: "display",
1037
+ content: fieldDef.content,
1038
+ sourceValue,
1039
+ format: fieldDef.format
1040
+ };
980
1041
  } else {
981
1042
  fieldProps = {
982
1043
  ...baseProps,