@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.
@@ -6,10 +6,11 @@
6
6
  */
7
7
 
8
8
  import React, { forwardRef, useImperativeHandle, useRef, useMemo, useCallback } from "react";
9
- import type { Forma, FieldDefinition, ValidationResult, JSONSchemaProperty } from "@fogpipe/forma-core";
9
+ import type { Forma, FieldDefinition, ValidationResult, JSONSchemaProperty, SelectOption } from "@fogpipe/forma-core";
10
+ import { isAdornableField, isSelectionField } from "@fogpipe/forma-core";
10
11
  import { useForma } from "./useForma.js";
11
12
  import { FormaContext } from "./context.js";
12
- import type { ComponentMap, LayoutProps, FieldWrapperProps, PageWrapperProps, BaseFieldProps, TextFieldProps, NumberFieldProps, SelectFieldProps, ArrayFieldProps, ArrayHelpers } from "./types.js";
13
+ import type { ComponentMap, LayoutProps, FieldWrapperProps, PageWrapperProps, BaseFieldProps, TextFieldProps, NumberFieldProps, SelectFieldProps, ArrayFieldProps, ArrayHelpers, DisplayFieldProps } from "./types.js";
13
14
 
14
15
  /**
15
16
  * Props for FormRenderer component
@@ -247,8 +248,8 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
247
248
  const isVisible = forma.visibility[fieldPath] !== false;
248
249
  if (!isVisible) return null;
249
250
 
250
- // Infer field type
251
- const fieldType = fieldDef.type || (fieldDef.itemFields ? "array" : "text");
251
+ // Get field type (type is required on all field definitions)
252
+ const fieldType = fieldDef.type;
252
253
  const componentKey = fieldType as keyof ComponentMap;
253
254
  const Component = components[componentKey] || components.fallback;
254
255
 
@@ -273,6 +274,7 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
273
274
  const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
274
275
 
275
276
  // Base field props
277
+ const isReadonly = forma.readonly[fieldPath] ?? false;
276
278
  const baseProps: BaseFieldProps = {
277
279
  name: fieldPath,
278
280
  field: fieldDef,
@@ -286,13 +288,22 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
286
288
  // Convenience properties
287
289
  visible: true, // Always true since we already filtered for visibility
288
290
  enabled: !disabled,
291
+ readonly: isReadonly,
289
292
  label: fieldDef.label ?? fieldPath,
290
293
  description: fieldDef.description,
291
294
  placeholder: fieldDef.placeholder,
295
+ // Adorner properties (only for adornable field types)
296
+ ...(isAdornableField(fieldDef) && {
297
+ prefix: fieldDef.prefix,
298
+ suffix: fieldDef.suffix,
299
+ }),
300
+ // Presentation variant
301
+ variant: fieldDef.variant,
302
+ variantConfig: fieldDef.variantConfig,
292
303
  };
293
304
 
294
305
  // Build type-specific props
295
- let fieldProps: BaseFieldProps | TextFieldProps | NumberFieldProps | SelectFieldProps | ArrayFieldProps = baseProps;
306
+ let fieldProps: BaseFieldProps | TextFieldProps | NumberFieldProps | SelectFieldProps | ArrayFieldProps | DisplayFieldProps = baseProps;
296
307
 
297
308
  if (fieldType === "number" || fieldType === "integer") {
298
309
  const constraints = getNumberConstraints(schemaProperty);
@@ -304,14 +315,15 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
304
315
  ...constraints,
305
316
  } as NumberFieldProps;
306
317
  } else if (fieldType === "select" || fieldType === "multiselect") {
318
+ const selectOptions = isSelectionField(fieldDef) ? fieldDef.options : [];
307
319
  fieldProps = {
308
320
  ...baseProps,
309
321
  fieldType,
310
322
  value: baseProps.value as string | string[] | null,
311
323
  onChange: baseProps.onChange as (value: string | string[] | null) => void,
312
- options: fieldDef.options ?? [],
324
+ options: forma.optionsVisibility[fieldPath] ?? selectOptions ?? [],
313
325
  } as SelectFieldProps;
314
- } else if (fieldType === "array" && fieldDef.itemFields) {
326
+ } else if (fieldType === "array" && fieldDef.type === "array" && fieldDef.itemFields) {
315
327
  const arrayValue = Array.isArray(baseProps.value) ? baseProps.value : [];
316
328
  const minItems = fieldDef.minItems ?? 0;
317
329
  const maxItems = fieldDef.maxItems ?? Infinity;
@@ -330,11 +342,12 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
330
342
  const getItemFieldPropsExtended = (index: number, fieldName: string) => {
331
343
  const baseProps = baseHelpers.getItemFieldProps(index, fieldName);
332
344
  const itemFieldDef = itemFieldDefs[fieldName];
345
+ const itemPath = `${fieldPath}[${index}].${fieldName}`;
333
346
  return {
334
347
  ...baseProps,
335
348
  itemIndex: index,
336
349
  fieldName,
337
- options: itemFieldDef?.options,
350
+ options: (forma.optionsVisibility[itemPath] as SelectOption[] | undefined) ?? (itemFieldDef && isSelectionField(itemFieldDef) ? itemFieldDef.options : undefined),
338
351
  };
339
352
  };
340
353
 
@@ -361,6 +374,19 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
361
374
  minItems,
362
375
  maxItems,
363
376
  } as ArrayFieldProps;
377
+ } else if (fieldType === "display" && fieldDef.type === "display") {
378
+ // Display fields (read-only presentation content)
379
+ // Resolve source value if the display field has a source property
380
+ const sourceValue = fieldDef.source ? forma.data[fieldDef.source] ?? forma.computed[fieldDef.source] : undefined;
381
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
382
+ const { onChange: _onChange, value: _value, ...displayBaseProps } = baseProps;
383
+ fieldProps = {
384
+ ...displayBaseProps,
385
+ fieldType: "display",
386
+ content: fieldDef.content,
387
+ sourceValue,
388
+ format: fieldDef.format,
389
+ } as DisplayFieldProps;
364
390
  } else {
365
391
  // Text-based fields
366
392
  fieldProps = {
@@ -735,6 +735,192 @@ describe("FormRenderer", () => {
735
735
  });
736
736
  });
737
737
 
738
+ // ============================================================================
739
+ // Option Visibility (visibleWhen on select options)
740
+ // ============================================================================
741
+
742
+ describe("option visibility in FormRenderer", () => {
743
+ it("should filter select options based on visibleWhen expressions", async () => {
744
+ const user = userEvent.setup();
745
+ const spec = createTestSpec({
746
+ fields: {
747
+ department: {
748
+ type: "select",
749
+ label: "Department",
750
+ options: [
751
+ { value: "engineering", label: "Engineering" },
752
+ { value: "sales", label: "Sales" },
753
+ ],
754
+ },
755
+ position: {
756
+ type: "select",
757
+ label: "Position",
758
+ options: [
759
+ { value: "dev", label: "Developer", visibleWhen: 'department = "engineering"' },
760
+ { value: "qa", label: "QA Engineer", visibleWhen: 'department = "engineering"' },
761
+ { value: "rep", label: "Sales Rep", visibleWhen: 'department = "sales"' },
762
+ { value: "mgr", label: "Sales Manager", visibleWhen: 'department = "sales"' },
763
+ ],
764
+ },
765
+ },
766
+ });
767
+
768
+ render(
769
+ <FormRenderer
770
+ spec={spec}
771
+ components={createTestComponentMap()}
772
+ />
773
+ );
774
+
775
+ // Initially no department selected - no position options should show
776
+ const positionSelect = screen.getByTestId("field-position").querySelector("select")!;
777
+ // Only the placeholder "Select..." option should be present
778
+ expect(positionSelect.querySelectorAll("option")).toHaveLength(1);
779
+
780
+ // Select Engineering department
781
+ const departmentSelect = screen.getByTestId("field-department").querySelector("select")!;
782
+ await user.selectOptions(departmentSelect, "engineering");
783
+
784
+ // Now only engineering positions should show
785
+ await waitFor(() => {
786
+ const options = positionSelect.querySelectorAll("option");
787
+ // placeholder + 2 engineering options
788
+ expect(options).toHaveLength(3);
789
+ expect(screen.getByText("Developer")).toBeInTheDocument();
790
+ expect(screen.getByText("QA Engineer")).toBeInTheDocument();
791
+ expect(screen.queryByText("Sales Rep")).not.toBeInTheDocument();
792
+ expect(screen.queryByText("Sales Manager")).not.toBeInTheDocument();
793
+ });
794
+
795
+ // Switch to Sales department
796
+ await user.selectOptions(departmentSelect, "sales");
797
+
798
+ // Now only sales positions should show
799
+ await waitFor(() => {
800
+ const options = positionSelect.querySelectorAll("option");
801
+ // placeholder + 2 sales options
802
+ expect(options).toHaveLength(3);
803
+ expect(screen.getByText("Sales Rep")).toBeInTheDocument();
804
+ expect(screen.getByText("Sales Manager")).toBeInTheDocument();
805
+ expect(screen.queryByText("Developer")).not.toBeInTheDocument();
806
+ expect(screen.queryByText("QA Engineer")).not.toBeInTheDocument();
807
+ });
808
+ });
809
+
810
+ it("should show all options when none have visibleWhen", () => {
811
+ const spec = createTestSpec({
812
+ fields: {
813
+ color: {
814
+ type: "select",
815
+ label: "Color",
816
+ options: [
817
+ { value: "red", label: "Red" },
818
+ { value: "blue", label: "Blue" },
819
+ { value: "green", label: "Green" },
820
+ ],
821
+ },
822
+ },
823
+ });
824
+
825
+ render(
826
+ <FormRenderer
827
+ spec={spec}
828
+ components={createTestComponentMap()}
829
+ />
830
+ );
831
+
832
+ expect(screen.getByText("Red")).toBeInTheDocument();
833
+ expect(screen.getByText("Blue")).toBeInTheDocument();
834
+ expect(screen.getByText("Green")).toBeInTheDocument();
835
+ });
836
+
837
+ it("should filter multiselect options based on visibleWhen expressions", async () => {
838
+ const user = userEvent.setup();
839
+ const spec = createTestSpec({
840
+ fields: {
841
+ tier: {
842
+ type: "select",
843
+ label: "Tier",
844
+ options: [
845
+ { value: "basic", label: "Basic" },
846
+ { value: "premium", label: "Premium" },
847
+ ],
848
+ },
849
+ features: {
850
+ type: "multiselect",
851
+ label: "Features",
852
+ options: [
853
+ { value: "email", label: "Email Support" },
854
+ { value: "phone", label: "Phone Support", visibleWhen: 'tier = "premium"' },
855
+ { value: "priority", label: "Priority Queue", visibleWhen: 'tier = "premium"' },
856
+ ],
857
+ },
858
+ },
859
+ });
860
+
861
+ render(
862
+ <FormRenderer
863
+ spec={spec}
864
+ components={createTestComponentMap()}
865
+ />
866
+ );
867
+
868
+ // Initially no tier selected - only non-conditional option visible
869
+ const featuresSelect = screen.getByTestId("field-features").querySelector("select")!;
870
+ await waitFor(() => {
871
+ // placeholder + 1 option without visibleWhen
872
+ expect(featuresSelect.querySelectorAll("option")).toHaveLength(2);
873
+ expect(screen.getByText("Email Support")).toBeInTheDocument();
874
+ expect(screen.queryByText("Phone Support")).not.toBeInTheDocument();
875
+ });
876
+
877
+ // Select Premium tier
878
+ const tierSelect = screen.getByTestId("field-tier").querySelector("select")!;
879
+ await user.selectOptions(tierSelect, "premium");
880
+
881
+ // All options should now show
882
+ await waitFor(() => {
883
+ // placeholder + 3 options
884
+ expect(featuresSelect.querySelectorAll("option")).toHaveLength(4);
885
+ expect(screen.getByText("Email Support")).toBeInTheDocument();
886
+ expect(screen.getByText("Phone Support")).toBeInTheDocument();
887
+ expect(screen.getByText("Priority Queue")).toBeInTheDocument();
888
+ });
889
+ });
890
+
891
+ it("should return empty options when all are filtered out", async () => {
892
+ const spec = createTestSpec({
893
+ fields: {
894
+ category: {
895
+ type: "select",
896
+ label: "Category",
897
+ options: [
898
+ { value: "a", label: "Option A", visibleWhen: 'toggle = true' },
899
+ { value: "b", label: "Option B", visibleWhen: 'toggle = true' },
900
+ ],
901
+ },
902
+ toggle: {
903
+ type: "boolean",
904
+ label: "Toggle",
905
+ },
906
+ },
907
+ });
908
+
909
+ render(
910
+ <FormRenderer
911
+ spec={spec}
912
+ initialData={{ toggle: false }}
913
+ components={createTestComponentMap()}
914
+ />
915
+ );
916
+
917
+ // All options have visibleWhen that evaluates to false
918
+ const categorySelect = screen.getByTestId("field-category").querySelector("select")!;
919
+ // Only placeholder
920
+ expect(categorySelect.querySelectorAll("option")).toHaveLength(1);
921
+ });
922
+ });
923
+
738
924
  // ============================================================================
739
925
  // Array Field Interactions
740
926
  // ============================================================================
package/src/index.ts CHANGED
@@ -79,6 +79,8 @@ export type {
79
79
  ArrayComponentProps,
80
80
  ObjectComponentProps,
81
81
  ComputedComponentProps,
82
+ DisplayFieldProps,
83
+ DisplayComponentProps,
82
84
  ComponentMap,
83
85
 
84
86
  // Form state
package/src/types.ts CHANGED
@@ -31,12 +31,22 @@ export interface BaseFieldProps {
31
31
  visible: boolean;
32
32
  /** Whether field is enabled (inverse of disabled) */
33
33
  enabled: boolean;
34
+ /** Whether field is readonly (visible, not editable, value still submitted) */
35
+ readonly: boolean;
34
36
  /** Display label from field definition */
35
37
  label: string;
36
38
  /** Help text or description from field definition */
37
39
  description?: string;
38
40
  /** Placeholder text from field definition */
39
41
  placeholder?: string;
42
+ /** Prefix adorner text (e.g., "$") - only for adornable field types */
43
+ prefix?: string;
44
+ /** Suffix adorner text (e.g., "kg") - only for adornable field types */
45
+ suffix?: string;
46
+ /** Presentation variant hint (e.g., "slider", "radio", "nps") */
47
+ variant?: string;
48
+ /** Variant-specific configuration */
49
+ variantConfig?: Record<string, unknown>;
40
50
  }
41
51
 
42
52
  /**
@@ -239,6 +249,22 @@ export interface ArrayItemFieldProps extends Omit<BaseFieldProps, "value" | "onC
239
249
  fieldName: string;
240
250
  }
241
251
 
252
+ /**
253
+ * Props for display fields (read-only presentation content)
254
+ */
255
+ export interface DisplayFieldProps extends Omit<BaseFieldProps, "value" | "onChange"> {
256
+ fieldType: "display";
257
+ /** Static content (markdown/text) */
258
+ content?: string;
259
+ /** Computed source value (resolved by useForma from display field's source property) */
260
+ sourceValue?: unknown;
261
+ /** Display format string */
262
+ format?: string;
263
+ /** No onChange - display fields are read-only */
264
+ onChange?: never;
265
+ value?: never;
266
+ }
267
+
242
268
  /**
243
269
  * Union of all field prop types
244
270
  */
@@ -253,7 +279,8 @@ export type FieldProps =
253
279
  | MultiSelectFieldProps
254
280
  | ArrayFieldProps
255
281
  | ObjectFieldProps
256
- | ComputedFieldProps;
282
+ | ComputedFieldProps
283
+ | DisplayFieldProps;
257
284
 
258
285
  /**
259
286
  * Map of field types to React components
@@ -275,6 +302,7 @@ export interface ComponentMap {
275
302
  array?: React.ComponentType<ArrayComponentProps>;
276
303
  object?: React.ComponentType<ObjectComponentProps>;
277
304
  computed?: React.ComponentType<ComputedComponentProps>;
305
+ display?: React.ComponentType<DisplayComponentProps>;
278
306
  fallback?: React.ComponentType<FieldComponentProps>;
279
307
  }
280
308
 
@@ -382,6 +410,11 @@ export interface ComputedComponentProps {
382
410
  spec: Forma;
383
411
  }
384
412
 
413
+ export interface DisplayComponentProps {
414
+ field: DisplayFieldProps;
415
+ spec: Forma;
416
+ }
417
+
385
418
  /**
386
419
  * Generic field component props (for fallback/dynamic components)
387
420
  */
@@ -427,6 +460,8 @@ export interface GetFieldPropsResult {
427
460
  visible: boolean;
428
461
  /** Whether field is enabled (not disabled) */
429
462
  enabled: boolean;
463
+ /** Whether field is readonly (visible, not editable, value still submitted) */
464
+ readonly: boolean;
430
465
  /** Whether field is required (for validation) */
431
466
  required: boolean;
432
467
  /**
@@ -451,6 +486,14 @@ export interface GetFieldPropsResult {
451
486
  "aria-required"?: boolean;
452
487
  /** Options for select/multiselect fields (filtered by visibleWhen) */
453
488
  options?: SelectOption[];
489
+ /** Prefix adorner text (e.g., "$") */
490
+ prefix?: string;
491
+ /** Suffix adorner text (e.g., "kg") */
492
+ suffix?: string;
493
+ /** Presentation variant hint */
494
+ variant?: string;
495
+ /** Variant-specific configuration */
496
+ variantConfig?: Record<string, unknown>;
454
497
  }
455
498
 
456
499
  /**
package/src/useForma.ts CHANGED
@@ -7,11 +7,13 @@
7
7
 
8
8
  import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
9
9
  import type { Forma, FieldError, ValidationResult, SelectOption } from "@fogpipe/forma-core";
10
+ import { isAdornableField } from "@fogpipe/forma-core";
10
11
  import type { GetFieldPropsResult, GetSelectFieldPropsResult, GetArrayHelpersResult } from "./types.js";
11
12
  import {
12
13
  getVisibility,
13
14
  getRequired,
14
15
  getEnabled,
16
+ getReadonly,
15
17
  validate,
16
18
  calculate,
17
19
  getPageVisibility,
@@ -110,6 +112,8 @@ export interface UseFormaReturn {
110
112
  required: Record<string, boolean>;
111
113
  /** Field enabled state map */
112
114
  enabled: Record<string, boolean>;
115
+ /** Field readonly state map */
116
+ readonly: Record<string, boolean>;
113
117
  /** Visible options for select/multiselect fields, keyed by field path */
114
118
  optionsVisibility: OptionsVisibilityResult;
115
119
  /** Field touched state map */
@@ -272,6 +276,12 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
272
276
  [state.data, spec, computed]
273
277
  );
274
278
 
279
+ // Calculate readonly state
280
+ const readonly = useMemo(
281
+ () => getReadonly(state.data, spec, { computed }),
282
+ [state.data, spec, computed]
283
+ );
284
+
275
285
  // Calculate visible options for all select/multiselect fields (memoized)
276
286
  const optionsVisibility = useMemo(
277
287
  () => getOptionsVisibility(state.data, spec, { computed }),
@@ -543,7 +553,7 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
543
553
  // Also include array item field patterns
544
554
  for (const fieldId of spec.fieldOrder) {
545
555
  const fieldDef = spec.fields[fieldId];
546
- if (fieldDef?.itemFields) {
556
+ if (fieldDef?.type === "array" && fieldDef.itemFields) {
547
557
  for (const key of fieldHandlers.current.keys()) {
548
558
  if (key.startsWith(`${fieldId}[`)) {
549
559
  validFields.add(key);
@@ -610,6 +620,11 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
610
620
  const hasValidationRules = (fieldDef?.validations?.length ?? 0) > 0;
611
621
  const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);
612
622
 
623
+ // Pass through adorner props for adornable field types
624
+ const adornerProps = fieldDef && isAdornableField(fieldDef)
625
+ ? { prefix: fieldDef.prefix, suffix: fieldDef.suffix }
626
+ : {};
627
+
613
628
  return {
614
629
  name: path,
615
630
  value: getValueAtPath(path),
@@ -619,6 +634,7 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
619
634
  placeholder: fieldDef?.placeholder,
620
635
  visible: visibility[path] !== false,
621
636
  enabled: enabled[path] !== false,
637
+ readonly: readonly[path] ?? false,
622
638
  required: isRequired,
623
639
  showRequiredIndicator,
624
640
  touched: isTouched,
@@ -629,8 +645,13 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
629
645
  "aria-invalid": hasErrors || undefined,
630
646
  "aria-describedby": hasErrors ? `${path}-error` : undefined,
631
647
  "aria-required": isRequired || undefined,
648
+ // Adorner props (only for adornable field types)
649
+ ...adornerProps,
650
+ // Presentation variant
651
+ variant: fieldDef?.variant,
652
+ variantConfig: fieldDef?.variantConfig,
632
653
  };
633
- }, [spec, state.touched, state.isSubmitted, visibility, enabled, required, validation.errors, validateOn, getValueAtPath, getFieldHandlers]);
654
+ }, [spec, state.touched, state.isSubmitted, visibility, enabled, readonly, required, validation.errors, validateOn, getValueAtPath, getFieldHandlers]);
634
655
 
635
656
  // Get select field props - uses pre-computed optionsVisibility map
636
657
  const getSelectFieldProps = useCallback((path: string): GetSelectFieldPropsResult => {
@@ -649,15 +670,16 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
649
670
  const getArrayHelpers = useCallback((path: string): GetArrayHelpersResult => {
650
671
  const fieldDef = spec.fields[path];
651
672
  const currentValue = (getValueAtPath(path) as unknown[]) ?? [];
652
- const minItems = fieldDef?.minItems ?? 0;
653
- const maxItems = fieldDef?.maxItems ?? Infinity;
673
+ const arrayDef = fieldDef?.type === "array" ? fieldDef : undefined;
674
+ const minItems = arrayDef?.minItems ?? 0;
675
+ const maxItems = arrayDef?.maxItems ?? Infinity;
654
676
 
655
677
  const canAdd = currentValue.length < maxItems;
656
678
  const canRemove = currentValue.length > minItems;
657
679
 
658
680
  const getItemFieldProps = (index: number, fieldName: string): GetFieldPropsResult => {
659
681
  const itemPath = `${path}[${index}].${fieldName}`;
660
- const itemFieldDef = fieldDef?.itemFields?.[fieldName];
682
+ const itemFieldDef = arrayDef?.itemFields?.[fieldName];
661
683
  const handlers = getFieldHandlers(itemPath);
662
684
 
663
685
  // Get item value
@@ -680,6 +702,7 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
680
702
  placeholder: itemFieldDef?.placeholder,
681
703
  visible: true,
682
704
  enabled: enabled[path] !== false,
705
+ readonly: readonly[itemPath] ?? false,
683
706
  required: false, // TODO: Evaluate item field required
684
707
  showRequiredIndicator: false, // Item fields don't show required indicator
685
708
  touched: isTouched,
@@ -728,7 +751,7 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
728
751
  canAdd,
729
752
  canRemove,
730
753
  };
731
- }, [spec.fields, getValueAtPath, setValueAtPath, getFieldHandlers, enabled, state.touched, state.isSubmitted, validation.errors, validateOn, optionsVisibility]);
754
+ }, [spec.fields, getValueAtPath, setValueAtPath, getFieldHandlers, enabled, readonly, state.touched, state.isSubmitted, validation.errors, validateOn, optionsVisibility]);
732
755
 
733
756
  return {
734
757
  data: state.data,
@@ -736,6 +759,7 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
736
759
  visibility,
737
760
  required,
738
761
  enabled,
762
+ readonly,
739
763
  optionsVisibility,
740
764
  touched: state.touched,
741
765
  errors: validation.errors,