@fogpipe/forma-react 0.18.0 → 0.19.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/{FormRenderer-D_ZVK44t.d.ts → FormRenderer-B7qwG4to.d.ts} +9 -1
- package/dist/{chunk-5K4QITFH.js → chunk-CFX3T5WK.js} +25 -3
- package/dist/chunk-CFX3T5WK.js.map +1 -0
- package/dist/defaults/index.d.ts +1 -1
- package/dist/defaults/index.js +7 -3
- package/dist/defaults/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +26 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/FieldRenderer.tsx +33 -1
- package/src/FormRenderer.tsx +35 -1
- package/src/__tests__/defaults/components.test.tsx +256 -0
- package/src/__tests__/defaults/integration.test.tsx +132 -0
- package/src/__tests__/test-utils.tsx +4 -2
- package/src/defaults/components/ComputedDisplay.tsx +2 -1
- package/src/defaults/components/DisplayField.tsx +8 -1
- package/src/types.ts +7 -0
- package/dist/chunk-5K4QITFH.js.map +0 -1
package/src/FormRenderer.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
ValidationResult,
|
|
19
19
|
JSONSchemaProperty,
|
|
20
20
|
SelectOption,
|
|
21
|
+
FormatOptions,
|
|
21
22
|
} from "@fogpipe/forma-core";
|
|
22
23
|
import { isAdornableField, isSelectionField } from "@fogpipe/forma-core";
|
|
23
24
|
import { useForma } from "./useForma.js";
|
|
@@ -34,6 +35,7 @@ import type {
|
|
|
34
35
|
ArrayFieldProps,
|
|
35
36
|
ArrayHelpers,
|
|
36
37
|
DisplayFieldProps,
|
|
38
|
+
ComputedFieldProps,
|
|
37
39
|
MatrixFieldProps,
|
|
38
40
|
} from "./types.js";
|
|
39
41
|
|
|
@@ -64,6 +66,8 @@ export interface FormRendererProps {
|
|
|
64
66
|
validateOn?: "change" | "blur" | "submit";
|
|
65
67
|
/** Current page for controlled wizard */
|
|
66
68
|
page?: number;
|
|
69
|
+
/** Format options for number/currency/date display (overrides spec.meta.locale/currency) */
|
|
70
|
+
formatOptions?: FormatOptions;
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
/**
|
|
@@ -240,6 +244,13 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
|
|
|
240
244
|
validateOn = "blur",
|
|
241
245
|
} = props;
|
|
242
246
|
|
|
247
|
+
// Resolve format options: prop override > spec.meta > defaults
|
|
248
|
+
const resolvedFormatOptions = useMemo<FormatOptions>(() => ({
|
|
249
|
+
locale: props.formatOptions?.locale ?? spec.meta.locale,
|
|
250
|
+
currency: props.formatOptions?.currency ?? spec.meta.currency,
|
|
251
|
+
nullDisplay: props.formatOptions?.nullDisplay ?? "—",
|
|
252
|
+
}), [props.formatOptions, spec.meta.locale, spec.meta.currency]);
|
|
253
|
+
|
|
243
254
|
const forma = useForma({
|
|
244
255
|
spec,
|
|
245
256
|
initialData,
|
|
@@ -396,6 +407,7 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
|
|
|
396
407
|
| SelectFieldProps
|
|
397
408
|
| ArrayFieldProps
|
|
398
409
|
| DisplayFieldProps
|
|
410
|
+
| ComputedFieldProps
|
|
399
411
|
| MatrixFieldProps = baseProps;
|
|
400
412
|
|
|
401
413
|
if (fieldType === "number" || fieldType === "integer") {
|
|
@@ -515,6 +527,13 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
|
|
|
515
527
|
const sourceValue = fieldDef.source
|
|
516
528
|
? (formaData[fieldDef.source] ?? formaComputed[fieldDef.source])
|
|
517
529
|
: undefined;
|
|
530
|
+
// Resolve format: display field's own format takes priority,
|
|
531
|
+
// fall back to the source computed field's format
|
|
532
|
+
const format =
|
|
533
|
+
fieldDef.format ??
|
|
534
|
+
(fieldDef.source
|
|
535
|
+
? spec.computed?.[fieldDef.source]?.format
|
|
536
|
+
: undefined);
|
|
518
537
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
519
538
|
const {
|
|
520
539
|
onChange: _onChange,
|
|
@@ -526,8 +545,22 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
|
|
|
526
545
|
fieldType: "display",
|
|
527
546
|
content: fieldDef.content,
|
|
528
547
|
sourceValue,
|
|
529
|
-
format
|
|
548
|
+
format,
|
|
549
|
+
formatOptions: resolvedFormatOptions,
|
|
530
550
|
} as DisplayFieldProps;
|
|
551
|
+
} else if (fieldType === "computed" && fieldDef.type === "computed") {
|
|
552
|
+
// Computed fields (read-only calculated values)
|
|
553
|
+
const computedDef = spec.computed?.[fieldPath];
|
|
554
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
555
|
+
const { onChange: _onChangeC, ...computedBaseProps } = baseProps;
|
|
556
|
+
fieldProps = {
|
|
557
|
+
...computedBaseProps,
|
|
558
|
+
fieldType: "computed",
|
|
559
|
+
value: formaComputed[fieldPath],
|
|
560
|
+
expression: computedDef?.expression ?? "",
|
|
561
|
+
format: computedDef?.format,
|
|
562
|
+
formatOptions: resolvedFormatOptions,
|
|
563
|
+
} as ComputedFieldProps;
|
|
531
564
|
} else {
|
|
532
565
|
// Text-based fields
|
|
533
566
|
fieldProps = {
|
|
@@ -581,6 +614,7 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
|
|
|
581
614
|
formaErrors,
|
|
582
615
|
formaIsSubmitted,
|
|
583
616
|
validateOn,
|
|
617
|
+
resolvedFormatOptions,
|
|
584
618
|
setFieldValue,
|
|
585
619
|
setFieldTouched,
|
|
586
620
|
getArrayHelpers,
|
|
@@ -671,6 +671,120 @@ describe("ComputedDisplay", () => {
|
|
|
671
671
|
);
|
|
672
672
|
expect(screen.getByText('{"a":1}')).toBeInTheDocument();
|
|
673
673
|
});
|
|
674
|
+
|
|
675
|
+
it("formats value with currency format", () => {
|
|
676
|
+
render(
|
|
677
|
+
<ComputedDisplay
|
|
678
|
+
field={makeComputedProps({ value: 1234.56, format: "currency" })}
|
|
679
|
+
spec={mockSpec}
|
|
680
|
+
/>,
|
|
681
|
+
);
|
|
682
|
+
expect(screen.getByText("$1,234.56")).toBeInTheDocument();
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("formats value with percent format", () => {
|
|
686
|
+
render(
|
|
687
|
+
<ComputedDisplay
|
|
688
|
+
field={makeComputedProps({ value: 0.5, format: "percent" })}
|
|
689
|
+
spec={mockSpec}
|
|
690
|
+
/>,
|
|
691
|
+
);
|
|
692
|
+
expect(screen.getByText("50%")).toBeInTheDocument();
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("formats value with decimal format", () => {
|
|
696
|
+
render(
|
|
697
|
+
<ComputedDisplay
|
|
698
|
+
field={makeComputedProps({ value: 99.999, format: "decimal(2)" })}
|
|
699
|
+
spec={mockSpec}
|
|
700
|
+
/>,
|
|
701
|
+
);
|
|
702
|
+
expect(screen.getByText("100.00")).toBeInTheDocument();
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it("still shows em-dash for null even with format", () => {
|
|
706
|
+
render(
|
|
707
|
+
<ComputedDisplay
|
|
708
|
+
field={makeComputedProps({ value: null, format: "currency" })}
|
|
709
|
+
spec={mockSpec}
|
|
710
|
+
/>,
|
|
711
|
+
);
|
|
712
|
+
expect(screen.getByText("\u2014")).toBeInTheDocument();
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it("still shows em-dash for undefined even with format", () => {
|
|
716
|
+
render(
|
|
717
|
+
<ComputedDisplay
|
|
718
|
+
field={makeComputedProps({ value: undefined, format: "currency" })}
|
|
719
|
+
spec={mockSpec}
|
|
720
|
+
/>,
|
|
721
|
+
);
|
|
722
|
+
expect(screen.getByText("\u2014")).toBeInTheDocument();
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("renders plain string without format", () => {
|
|
726
|
+
render(
|
|
727
|
+
<ComputedDisplay
|
|
728
|
+
field={makeComputedProps({ value: 42 })}
|
|
729
|
+
spec={mockSpec}
|
|
730
|
+
/>,
|
|
731
|
+
);
|
|
732
|
+
expect(screen.getByText("42")).toBeInTheDocument();
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it("formats zero with currency (does not show em-dash)", () => {
|
|
736
|
+
render(
|
|
737
|
+
<ComputedDisplay
|
|
738
|
+
field={makeComputedProps({ value: 0, format: "currency" })}
|
|
739
|
+
spec={mockSpec}
|
|
740
|
+
/>,
|
|
741
|
+
);
|
|
742
|
+
expect(screen.getByText("$0.00")).toBeInTheDocument();
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it("formats boolean without format as string", () => {
|
|
746
|
+
render(
|
|
747
|
+
<ComputedDisplay
|
|
748
|
+
field={makeComputedProps({ value: false })}
|
|
749
|
+
spec={mockSpec}
|
|
750
|
+
/>,
|
|
751
|
+
);
|
|
752
|
+
expect(screen.getByText("false")).toBeInTheDocument();
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it("formats currency with locale/currency from formatOptions", () => {
|
|
756
|
+
render(
|
|
757
|
+
<ComputedDisplay
|
|
758
|
+
field={makeComputedProps({
|
|
759
|
+
value: 25444.40,
|
|
760
|
+
format: "currency",
|
|
761
|
+
formatOptions: { locale: "sv-SE", currency: "SEK" },
|
|
762
|
+
})}
|
|
763
|
+
spec={mockSpec}
|
|
764
|
+
/>,
|
|
765
|
+
);
|
|
766
|
+
const text = screen.getByText((content) =>
|
|
767
|
+
content.includes("25") && content.includes("444") && content.includes("kr"),
|
|
768
|
+
);
|
|
769
|
+
expect(text).toBeInTheDocument();
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it("formats currency with EUR locale from formatOptions", () => {
|
|
773
|
+
render(
|
|
774
|
+
<ComputedDisplay
|
|
775
|
+
field={makeComputedProps({
|
|
776
|
+
value: 1000,
|
|
777
|
+
format: "currency",
|
|
778
|
+
formatOptions: { locale: "de-DE", currency: "EUR" },
|
|
779
|
+
})}
|
|
780
|
+
spec={mockSpec}
|
|
781
|
+
/>,
|
|
782
|
+
);
|
|
783
|
+
const text = screen.getByText((content) =>
|
|
784
|
+
content.includes("1.000") && content.includes("€"),
|
|
785
|
+
);
|
|
786
|
+
expect(text).toBeInTheDocument();
|
|
787
|
+
});
|
|
674
788
|
});
|
|
675
789
|
|
|
676
790
|
// ============================================================================
|
|
@@ -707,6 +821,148 @@ describe("DisplayField", () => {
|
|
|
707
821
|
);
|
|
708
822
|
expect(screen.getByText("Dynamic value")).toBeInTheDocument();
|
|
709
823
|
});
|
|
824
|
+
|
|
825
|
+
it("formats sourceValue with currency format", () => {
|
|
826
|
+
render(
|
|
827
|
+
<DisplayField
|
|
828
|
+
field={makeDisplayProps({
|
|
829
|
+
sourceValue: 1234.56,
|
|
830
|
+
format: "currency",
|
|
831
|
+
content: "Fallback",
|
|
832
|
+
})}
|
|
833
|
+
spec={mockSpec}
|
|
834
|
+
/>,
|
|
835
|
+
);
|
|
836
|
+
expect(screen.getByText("$1,234.56")).toBeInTheDocument();
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it("formats sourceValue with percent format", () => {
|
|
840
|
+
render(
|
|
841
|
+
<DisplayField
|
|
842
|
+
field={makeDisplayProps({
|
|
843
|
+
sourceValue: 0.75,
|
|
844
|
+
format: "percent",
|
|
845
|
+
content: "Fallback",
|
|
846
|
+
})}
|
|
847
|
+
spec={mockSpec}
|
|
848
|
+
/>,
|
|
849
|
+
);
|
|
850
|
+
expect(screen.getByText("75%")).toBeInTheDocument();
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it("formats sourceValue with decimal format", () => {
|
|
854
|
+
render(
|
|
855
|
+
<DisplayField
|
|
856
|
+
field={makeDisplayProps({
|
|
857
|
+
sourceValue: 123.456,
|
|
858
|
+
format: "decimal(1)",
|
|
859
|
+
content: "Fallback",
|
|
860
|
+
})}
|
|
861
|
+
spec={mockSpec}
|
|
862
|
+
/>,
|
|
863
|
+
);
|
|
864
|
+
expect(screen.getByText("123.5")).toBeInTheDocument();
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it("shows em-dash for null sourceValue with format", () => {
|
|
868
|
+
render(
|
|
869
|
+
<DisplayField
|
|
870
|
+
field={makeDisplayProps({
|
|
871
|
+
sourceValue: null,
|
|
872
|
+
format: "currency",
|
|
873
|
+
content: "Fallback",
|
|
874
|
+
})}
|
|
875
|
+
spec={mockSpec}
|
|
876
|
+
/>,
|
|
877
|
+
);
|
|
878
|
+
expect(screen.getByText("\u2014")).toBeInTheDocument();
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it("shows em-dash for undefined sourceValue with format", () => {
|
|
882
|
+
render(
|
|
883
|
+
<DisplayField
|
|
884
|
+
field={makeDisplayProps({
|
|
885
|
+
sourceValue: undefined,
|
|
886
|
+
format: "currency",
|
|
887
|
+
})}
|
|
888
|
+
spec={mockSpec}
|
|
889
|
+
/>,
|
|
890
|
+
);
|
|
891
|
+
// sourceValue is undefined → falls through to content
|
|
892
|
+
expect(screen.getByText("Hello world")).toBeInTheDocument();
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it("falls back to string for non-numeric sourceValue with currency format", () => {
|
|
896
|
+
render(
|
|
897
|
+
<DisplayField
|
|
898
|
+
field={makeDisplayProps({
|
|
899
|
+
sourceValue: "not a number",
|
|
900
|
+
format: "currency",
|
|
901
|
+
})}
|
|
902
|
+
spec={mockSpec}
|
|
903
|
+
/>,
|
|
904
|
+
);
|
|
905
|
+
expect(screen.getByText("not a number")).toBeInTheDocument();
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it("formats sourceValue without format as plain string", () => {
|
|
909
|
+
render(
|
|
910
|
+
<DisplayField
|
|
911
|
+
field={makeDisplayProps({
|
|
912
|
+
sourceValue: 42,
|
|
913
|
+
})}
|
|
914
|
+
spec={mockSpec}
|
|
915
|
+
/>,
|
|
916
|
+
);
|
|
917
|
+
expect(screen.getByText("42")).toBeInTheDocument();
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it("formats zero sourceValue with currency (does not skip rendering)", () => {
|
|
921
|
+
render(
|
|
922
|
+
<DisplayField
|
|
923
|
+
field={makeDisplayProps({
|
|
924
|
+
sourceValue: 0,
|
|
925
|
+
format: "currency",
|
|
926
|
+
})}
|
|
927
|
+
spec={mockSpec}
|
|
928
|
+
/>,
|
|
929
|
+
);
|
|
930
|
+
expect(screen.getByText("$0.00")).toBeInTheDocument();
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
it("formats currency with locale/currency from formatOptions", () => {
|
|
934
|
+
render(
|
|
935
|
+
<DisplayField
|
|
936
|
+
field={makeDisplayProps({
|
|
937
|
+
sourceValue: 209550,
|
|
938
|
+
format: "currency",
|
|
939
|
+
formatOptions: { locale: "sv-SE", currency: "SEK" },
|
|
940
|
+
})}
|
|
941
|
+
spec={mockSpec}
|
|
942
|
+
/>,
|
|
943
|
+
);
|
|
944
|
+
const text = screen.getByText((content) =>
|
|
945
|
+
content.includes("209") && content.includes("550") && content.includes("kr"),
|
|
946
|
+
);
|
|
947
|
+
expect(text).toBeInTheDocument();
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it("formats currency with EUR locale", () => {
|
|
951
|
+
render(
|
|
952
|
+
<DisplayField
|
|
953
|
+
field={makeDisplayProps({
|
|
954
|
+
sourceValue: 1234.56,
|
|
955
|
+
format: "currency",
|
|
956
|
+
formatOptions: { locale: "de-DE", currency: "EUR" },
|
|
957
|
+
})}
|
|
958
|
+
spec={mockSpec}
|
|
959
|
+
/>,
|
|
960
|
+
);
|
|
961
|
+
const text = screen.getByText((content) =>
|
|
962
|
+
content.includes("1.234,56") || (content.includes("1234") && content.includes("€")),
|
|
963
|
+
);
|
|
964
|
+
expect(text).toBeInTheDocument();
|
|
965
|
+
});
|
|
710
966
|
});
|
|
711
967
|
|
|
712
968
|
// ============================================================================
|
|
@@ -491,4 +491,136 @@ describe("Disabled field integration", () => {
|
|
|
491
491
|
|
|
492
492
|
expect(screen.getByLabelText("Name")).toBeDisabled();
|
|
493
493
|
});
|
|
494
|
+
|
|
495
|
+
it("display field inherits format from source computed field", () => {
|
|
496
|
+
const spec = createTestSpec({
|
|
497
|
+
fields: {
|
|
498
|
+
totalDisplay: {
|
|
499
|
+
type: "display",
|
|
500
|
+
label: "Total",
|
|
501
|
+
source: "total",
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
computed: {
|
|
505
|
+
total: {
|
|
506
|
+
expression: "42",
|
|
507
|
+
format: "currency",
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// Provide initial data with the computed value already resolved
|
|
513
|
+
render(
|
|
514
|
+
<DefaultFormRenderer
|
|
515
|
+
spec={spec}
|
|
516
|
+
onSubmit={vi.fn()}
|
|
517
|
+
initialData={{ total: 1234.56 }}
|
|
518
|
+
/>,
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
// The display field should inherit "currency" format from computed.total
|
|
522
|
+
expect(screen.getByText("$1,234.56")).toBeInTheDocument();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("display field's own format takes priority over computed source format", () => {
|
|
526
|
+
const spec = createTestSpec({
|
|
527
|
+
fields: {
|
|
528
|
+
totalDisplay: {
|
|
529
|
+
type: "display",
|
|
530
|
+
label: "Total",
|
|
531
|
+
source: "total",
|
|
532
|
+
format: "percent",
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
computed: {
|
|
536
|
+
total: {
|
|
537
|
+
expression: "42",
|
|
538
|
+
format: "currency",
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
render(
|
|
544
|
+
<DefaultFormRenderer
|
|
545
|
+
spec={spec}
|
|
546
|
+
onSubmit={vi.fn()}
|
|
547
|
+
initialData={{ total: 0.75 }}
|
|
548
|
+
/>,
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// Display field's own format "percent" should win over computed "currency"
|
|
552
|
+
expect(screen.getByText("75%")).toBeInTheDocument();
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// ============================================================================
|
|
557
|
+
// Spec-Level Locale/Currency Integration
|
|
558
|
+
// ============================================================================
|
|
559
|
+
describe("Spec-level locale/currency", () => {
|
|
560
|
+
it("display field uses spec.meta.locale and spec.meta.currency for formatting", () => {
|
|
561
|
+
const spec = createTestSpec({
|
|
562
|
+
meta: { locale: "sv-SE", currency: "SEK" },
|
|
563
|
+
fields: {
|
|
564
|
+
totalDisplay: {
|
|
565
|
+
type: "display",
|
|
566
|
+
label: "Total",
|
|
567
|
+
source: "total",
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
computed: {
|
|
571
|
+
total: {
|
|
572
|
+
expression: "42",
|
|
573
|
+
format: "currency",
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
render(
|
|
579
|
+
<DefaultFormRenderer
|
|
580
|
+
spec={spec}
|
|
581
|
+
onSubmit={vi.fn()}
|
|
582
|
+
initialData={{ total: 209550 }}
|
|
583
|
+
/>,
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
// Should render with Swedish locale and SEK currency, not USD
|
|
587
|
+
const text = screen.getByText((content) =>
|
|
588
|
+
content.includes("209") && content.includes("550") && content.includes("kr"),
|
|
589
|
+
);
|
|
590
|
+
expect(text).toBeInTheDocument();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("formatOptions prop overrides spec.meta locale/currency", () => {
|
|
594
|
+
const spec = createTestSpec({
|
|
595
|
+
meta: { locale: "sv-SE", currency: "SEK" },
|
|
596
|
+
fields: {
|
|
597
|
+
totalDisplay: {
|
|
598
|
+
type: "display",
|
|
599
|
+
label: "Total",
|
|
600
|
+
source: "total",
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
computed: {
|
|
604
|
+
total: {
|
|
605
|
+
expression: "42",
|
|
606
|
+
format: "currency",
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
render(
|
|
612
|
+
<DefaultFormRenderer
|
|
613
|
+
spec={spec}
|
|
614
|
+
onSubmit={vi.fn()}
|
|
615
|
+
initialData={{ total: 1000 }}
|
|
616
|
+
formatOptions={{ locale: "de-DE", currency: "EUR" }}
|
|
617
|
+
/>,
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
// Should render with German locale and EUR, overriding spec meta
|
|
621
|
+
const text = screen.getByText((content) =>
|
|
622
|
+
content.includes("1.000") && content.includes("€"),
|
|
623
|
+
);
|
|
624
|
+
expect(text).toBeInTheDocument();
|
|
625
|
+
});
|
|
494
626
|
});
|
|
@@ -25,12 +25,13 @@ export function createTestSpec(
|
|
|
25
25
|
options: {
|
|
26
26
|
fields?: Record<string, { type: string; [key: string]: unknown }>;
|
|
27
27
|
fieldOrder?: string[];
|
|
28
|
-
computed?: Record<string, { expression: string }>;
|
|
28
|
+
computed?: Record<string, { expression: string; format?: string }>;
|
|
29
29
|
pages?: PageDefinition[];
|
|
30
30
|
referenceData?: Record<string, unknown>;
|
|
31
|
+
meta?: Partial<Forma["meta"]>;
|
|
31
32
|
} = {},
|
|
32
33
|
): Forma {
|
|
33
|
-
const { fields = {}, fieldOrder, computed, pages, referenceData } = options;
|
|
34
|
+
const { fields = {}, fieldOrder, computed, pages, referenceData, meta } = options;
|
|
34
35
|
|
|
35
36
|
// Build schema from fields
|
|
36
37
|
const schemaProperties: Record<string, unknown> = {};
|
|
@@ -113,6 +114,7 @@ export function createTestSpec(
|
|
|
113
114
|
meta: {
|
|
114
115
|
id: "test-form",
|
|
115
116
|
title: "Test Form",
|
|
117
|
+
...meta,
|
|
116
118
|
},
|
|
117
119
|
schema: {
|
|
118
120
|
type: "object",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { formatValue } from "@fogpipe/forma-core";
|
|
2
3
|
import type { ComputedComponentProps } from "../../types.js";
|
|
3
4
|
|
|
4
5
|
export function ComputedDisplay({ field }: ComputedComponentProps) {
|
|
@@ -12,7 +13,7 @@ export function ComputedDisplay({ field }: ComputedComponentProps) {
|
|
|
12
13
|
displayValue = String(field.value);
|
|
13
14
|
}
|
|
14
15
|
} else {
|
|
15
|
-
displayValue =
|
|
16
|
+
displayValue = formatValue(field.value, field.format, field.formatOptions);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
return (
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import type { FormatOptions } from "@fogpipe/forma-core";
|
|
3
|
+
import { formatValue } from "@fogpipe/forma-core";
|
|
2
4
|
import type { DisplayComponentProps } from "../../types.js";
|
|
3
5
|
|
|
6
|
+
const FORMAT_DEFAULTS: FormatOptions = { nullDisplay: "\u2014" };
|
|
7
|
+
|
|
4
8
|
export function DisplayField({ field }: DisplayComponentProps) {
|
|
9
|
+
const options = field.formatOptions
|
|
10
|
+
? { ...FORMAT_DEFAULTS, ...field.formatOptions }
|
|
11
|
+
: FORMAT_DEFAULTS;
|
|
5
12
|
const content =
|
|
6
13
|
field.sourceValue !== undefined
|
|
7
|
-
?
|
|
14
|
+
? formatValue(field.sourceValue, field.format, options)
|
|
8
15
|
: field.content;
|
|
9
16
|
|
|
10
17
|
return (
|
package/src/types.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
Forma,
|
|
7
7
|
FieldDefinition,
|
|
8
8
|
FieldError,
|
|
9
|
+
FormatOptions,
|
|
9
10
|
MatrixColumn,
|
|
10
11
|
SelectOption,
|
|
11
12
|
} from "@fogpipe/forma-core";
|
|
@@ -276,6 +277,10 @@ export interface ComputedFieldProps extends Omit<BaseFieldProps, "onChange"> {
|
|
|
276
277
|
fieldType: "computed";
|
|
277
278
|
value: unknown;
|
|
278
279
|
expression: string;
|
|
280
|
+
/** Display format string (e.g., "currency", "percent", "decimal(2)") */
|
|
281
|
+
format?: string;
|
|
282
|
+
/** Resolved format options (locale, currency) for number/date formatting */
|
|
283
|
+
formatOptions?: FormatOptions;
|
|
279
284
|
onChange?: never;
|
|
280
285
|
}
|
|
281
286
|
|
|
@@ -312,6 +317,8 @@ export interface DisplayFieldProps extends Omit<
|
|
|
312
317
|
sourceValue?: unknown;
|
|
313
318
|
/** Display format string */
|
|
314
319
|
format?: string;
|
|
320
|
+
/** Resolved format options (locale, currency) for number/date formatting */
|
|
321
|
+
formatOptions?: FormatOptions;
|
|
315
322
|
/** No onChange - display fields are read-only */
|
|
316
323
|
onChange?: never;
|
|
317
324
|
value?: never;
|