@fogpipe/forma-react 0.17.1 → 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.
Files changed (40) hide show
  1. package/README.md +82 -0
  2. package/dist/FormRenderer-B7qwG4to.d.ts +566 -0
  3. package/dist/chunk-CFX3T5WK.js +1298 -0
  4. package/dist/chunk-CFX3T5WK.js.map +1 -0
  5. package/dist/defaults/index.d.ts +56 -0
  6. package/dist/defaults/index.js +899 -0
  7. package/dist/defaults/index.js.map +1 -0
  8. package/dist/defaults/styles/forma-defaults.css +696 -0
  9. package/dist/index.d.ts +7 -559
  10. package/dist/index.js +53 -1293
  11. package/dist/index.js.map +1 -1
  12. package/package.json +17 -3
  13. package/src/FieldRenderer.tsx +33 -1
  14. package/src/FormRenderer.tsx +35 -1
  15. package/src/__tests__/defaults/components.test.tsx +1074 -0
  16. package/src/__tests__/defaults/integration.test.tsx +626 -0
  17. package/src/__tests__/defaults/layout.test.tsx +298 -0
  18. package/src/__tests__/test-utils.tsx +4 -2
  19. package/src/defaults/DefaultFormRenderer.tsx +43 -0
  20. package/src/defaults/componentMap.ts +45 -0
  21. package/src/defaults/components/ArrayField.tsx +183 -0
  22. package/src/defaults/components/BooleanInput.tsx +32 -0
  23. package/src/defaults/components/ComputedDisplay.tsx +27 -0
  24. package/src/defaults/components/DateInput.tsx +59 -0
  25. package/src/defaults/components/DisplayField.tsx +22 -0
  26. package/src/defaults/components/FallbackField.tsx +35 -0
  27. package/src/defaults/components/MatrixField.tsx +98 -0
  28. package/src/defaults/components/MultiSelectInput.tsx +51 -0
  29. package/src/defaults/components/NumberInput.tsx +73 -0
  30. package/src/defaults/components/ObjectField.tsx +22 -0
  31. package/src/defaults/components/SelectInput.tsx +44 -0
  32. package/src/defaults/components/TextInput.tsx +48 -0
  33. package/src/defaults/components/TextareaInput.tsx +46 -0
  34. package/src/defaults/index.ts +33 -0
  35. package/src/defaults/layout/FieldWrapper.tsx +83 -0
  36. package/src/defaults/layout/FormLayout.tsx +34 -0
  37. package/src/defaults/layout/PageWrapper.tsx +18 -0
  38. package/src/defaults/layout/WizardLayout.tsx +130 -0
  39. package/src/defaults/styles/forma-defaults.css +696 -0
  40. package/src/types.ts +7 -0
@@ -0,0 +1,298 @@
1
+ /// <reference types="@testing-library/jest-dom" />
2
+ /**
3
+ * Tests for default layout components
4
+ */
5
+ import React from "react";
6
+ import { render, screen, fireEvent } from "@testing-library/react";
7
+ import { describe, it, expect, vi } from "vitest";
8
+ import type { FieldError } from "@fogpipe/forma-core";
9
+ import type { FieldWrapperProps, PageWrapperProps } from "../../types.js";
10
+ import { FieldWrapper } from "../../defaults/layout/FieldWrapper.js";
11
+ import { FormLayout } from "../../defaults/layout/FormLayout.js";
12
+ import { PageWrapper } from "../../defaults/layout/PageWrapper.js";
13
+ import { FormaContext } from "../../context.js";
14
+ import type { UseFormaReturn } from "../../useForma.js";
15
+
16
+ function mockFormaContext(
17
+ overrides: Partial<UseFormaReturn> = {},
18
+ ): UseFormaReturn {
19
+ return {
20
+ data: {},
21
+ computed: {},
22
+ visibility: {},
23
+ required: {},
24
+ enabled: {},
25
+ readonly: {},
26
+ optionsVisibility: {},
27
+ touched: {},
28
+ errors: [],
29
+ isValid: true,
30
+ isSubmitting: false,
31
+ isSubmitted: false,
32
+ isDirty: false,
33
+ spec: {} as UseFormaReturn["spec"],
34
+ wizard: null,
35
+ setFieldValue: vi.fn(),
36
+ setFieldTouched: vi.fn(),
37
+ setValues: vi.fn(),
38
+ validateField: vi.fn(),
39
+ validateForm: vi.fn(),
40
+ submitForm: vi.fn(),
41
+ resetForm: vi.fn(),
42
+ getFieldProps: vi.fn(),
43
+ getSelectFieldProps: vi.fn(),
44
+ getArrayHelpers: vi.fn(),
45
+ on: vi.fn(),
46
+ ...overrides,
47
+ } as unknown as UseFormaReturn;
48
+ }
49
+
50
+ function renderWithContext(
51
+ ui: React.ReactElement,
52
+ contextOverrides: Partial<UseFormaReturn> = {},
53
+ ) {
54
+ const ctx = mockFormaContext(contextOverrides);
55
+ return render(
56
+ <FormaContext.Provider value={ctx}>{ui}</FormaContext.Provider>,
57
+ );
58
+ }
59
+
60
+ // ============================================================================
61
+ // FieldWrapper
62
+ // ============================================================================
63
+ describe("FieldWrapper", () => {
64
+ function makeProps(
65
+ overrides: Partial<FieldWrapperProps> = {},
66
+ ): FieldWrapperProps {
67
+ return {
68
+ fieldPath: "testField",
69
+ field: {
70
+ type: "text",
71
+ label: "Test Field",
72
+ } as FieldWrapperProps["field"],
73
+ children: <input type="text" />,
74
+ errors: [],
75
+ touched: false,
76
+ required: false,
77
+ showRequiredIndicator: false,
78
+ visible: true,
79
+ ...overrides,
80
+ };
81
+ }
82
+
83
+ it("renders label with field name", () => {
84
+ renderWithContext(<FieldWrapper {...makeProps()} />);
85
+ expect(screen.getByText("Test Field")).toBeInTheDocument();
86
+ });
87
+
88
+ it("shows required indicator when showRequiredIndicator is true", () => {
89
+ renderWithContext(
90
+ <FieldWrapper {...makeProps({ showRequiredIndicator: true })} />,
91
+ );
92
+ expect(screen.getByText("*")).toBeInTheDocument();
93
+ });
94
+
95
+ it("hides required indicator when showRequiredIndicator is false", () => {
96
+ renderWithContext(
97
+ <FieldWrapper {...makeProps({ showRequiredIndicator: false })} />,
98
+ );
99
+ expect(screen.queryByText("*")).not.toBeInTheDocument();
100
+ });
101
+
102
+ it("renders description when provided", () => {
103
+ renderWithContext(
104
+ <FieldWrapper
105
+ {...makeProps({
106
+ field: {
107
+ type: "text",
108
+ label: "Test Field",
109
+ description: "Help text here",
110
+ } as FieldWrapperProps["field"],
111
+ })}
112
+ />,
113
+ );
114
+ expect(screen.getByText("Help text here")).toBeInTheDocument();
115
+ });
116
+
117
+ it("renders children", () => {
118
+ renderWithContext(
119
+ <FieldWrapper
120
+ {...makeProps({ children: <span>Child content</span> })}
121
+ />,
122
+ );
123
+ expect(screen.getByText("Child content")).toBeInTheDocument();
124
+ });
125
+
126
+ it("does NOT render errors when touched is false and not submitted", () => {
127
+ const errors: FieldError[] = [
128
+ { field: "testField", message: "Required", severity: "error" },
129
+ ];
130
+ renderWithContext(
131
+ <FieldWrapper {...makeProps({ errors, touched: false })} />,
132
+ );
133
+ expect(screen.queryByText("Required")).not.toBeInTheDocument();
134
+ });
135
+
136
+ it("renders errors when touched is true and errors exist", () => {
137
+ const errors: FieldError[] = [
138
+ { field: "testField", message: "Required", severity: "error" },
139
+ ];
140
+ renderWithContext(
141
+ <FieldWrapper {...makeProps({ errors, touched: true })} />,
142
+ );
143
+ expect(screen.getByText("Required")).toBeInTheDocument();
144
+ });
145
+
146
+ it("renders errors when isSubmitted is true even if not touched", () => {
147
+ const errors: FieldError[] = [
148
+ { field: "testField", message: "Required", severity: "error" },
149
+ ];
150
+ renderWithContext(
151
+ <FieldWrapper {...makeProps({ errors, touched: false })} />,
152
+ { isSubmitted: true },
153
+ );
154
+ expect(screen.getByText("Required")).toBeInTheDocument();
155
+ });
156
+
157
+ it("error container has role=alert", () => {
158
+ const errors: FieldError[] = [
159
+ { field: "testField", message: "Required", severity: "error" },
160
+ ];
161
+ renderWithContext(
162
+ <FieldWrapper {...makeProps({ errors, touched: true })} />,
163
+ );
164
+ expect(screen.getByRole("alert")).toBeInTheDocument();
165
+ });
166
+
167
+ it("returns null when visible is false", () => {
168
+ const { container } = renderWithContext(
169
+ <FieldWrapper {...makeProps({ visible: false })} />,
170
+ );
171
+ expect(container.firstChild).toBeNull();
172
+ });
173
+
174
+ it("applies forma-field--error class when has visible errors", () => {
175
+ const errors: FieldError[] = [
176
+ { field: "testField", message: "Required", severity: "error" },
177
+ ];
178
+ const { container } = renderWithContext(
179
+ <FieldWrapper {...makeProps({ errors, touched: true })} />,
180
+ );
181
+ expect(container.firstChild).toHaveClass("forma-field--error");
182
+ });
183
+
184
+ it("applies forma-field--required class when showRequiredIndicator", () => {
185
+ const { container } = renderWithContext(
186
+ <FieldWrapper {...makeProps({ showRequiredIndicator: true })} />,
187
+ );
188
+ expect(container.firstChild).toHaveClass("forma-field--required");
189
+ });
190
+ });
191
+
192
+ // ============================================================================
193
+ // FormLayout
194
+ // ============================================================================
195
+ describe("FormLayout", () => {
196
+ it("renders as form element", () => {
197
+ const { container } = render(
198
+ <FormLayout onSubmit={vi.fn()} isSubmitting={false} isValid={true}>
199
+ <div>Content</div>
200
+ </FormLayout>,
201
+ );
202
+ expect(container.querySelector("form")).toBeInTheDocument();
203
+ });
204
+
205
+ it("calls onSubmit on form submit", () => {
206
+ const onSubmit = vi.fn();
207
+ const { container } = render(
208
+ <FormLayout onSubmit={onSubmit} isSubmitting={false} isValid={true}>
209
+ <div>Content</div>
210
+ </FormLayout>,
211
+ );
212
+ fireEvent.submit(container.querySelector("form")!);
213
+ expect(onSubmit).toHaveBeenCalled();
214
+ });
215
+
216
+ it("renders submit button with Submit text", () => {
217
+ render(
218
+ <FormLayout onSubmit={vi.fn()} isSubmitting={false} isValid={true}>
219
+ <div>Content</div>
220
+ </FormLayout>,
221
+ );
222
+ expect(screen.getByRole("button")).toHaveTextContent("Submit");
223
+ });
224
+
225
+ it("shows Submitting... and disables button when isSubmitting", () => {
226
+ render(
227
+ <FormLayout onSubmit={vi.fn()} isSubmitting={true} isValid={true}>
228
+ <div>Content</div>
229
+ </FormLayout>,
230
+ );
231
+ const button = screen.getByRole("button");
232
+ expect(button).toHaveTextContent("Submitting...");
233
+ expect(button).toBeDisabled();
234
+ });
235
+
236
+ it("sets aria-busy on button when submitting", () => {
237
+ render(
238
+ <FormLayout onSubmit={vi.fn()} isSubmitting={true} isValid={true}>
239
+ <div>Content</div>
240
+ </FormLayout>,
241
+ );
242
+ expect(screen.getByRole("button")).toHaveAttribute("aria-busy", "true");
243
+ });
244
+
245
+ it("renders children inside form", () => {
246
+ render(
247
+ <FormLayout onSubmit={vi.fn()} isSubmitting={false} isValid={true}>
248
+ <div>Form content</div>
249
+ </FormLayout>,
250
+ );
251
+ expect(screen.getByText("Form content")).toBeInTheDocument();
252
+ });
253
+ });
254
+
255
+ // ============================================================================
256
+ // PageWrapper
257
+ // ============================================================================
258
+ describe("PageWrapper", () => {
259
+ function makePageProps(
260
+ overrides: Partial<PageWrapperProps> = {},
261
+ ): PageWrapperProps {
262
+ return {
263
+ title: "Page Title",
264
+ children: <div>Page content</div>,
265
+ pageIndex: 0,
266
+ totalPages: 3,
267
+ ...overrides,
268
+ };
269
+ }
270
+
271
+ it("renders title as h2", () => {
272
+ render(<PageWrapper {...makePageProps()} />);
273
+ expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
274
+ "Page Title",
275
+ );
276
+ });
277
+
278
+ it("renders description when provided", () => {
279
+ render(
280
+ <PageWrapper
281
+ {...makePageProps({ description: "Some description" })}
282
+ />,
283
+ );
284
+ expect(screen.getByText("Some description")).toBeInTheDocument();
285
+ });
286
+
287
+ it("omits description element when not provided", () => {
288
+ const { container } = render(<PageWrapper {...makePageProps()} />);
289
+ expect(
290
+ container.querySelector(".forma-page__description"),
291
+ ).not.toBeInTheDocument();
292
+ });
293
+
294
+ it("renders children", () => {
295
+ render(<PageWrapper {...makePageProps()} />);
296
+ expect(screen.getByText("Page content")).toBeInTheDocument();
297
+ });
298
+ });
@@ -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",
@@ -0,0 +1,43 @@
1
+ import React, { forwardRef } from "react";
2
+ import { FormRenderer } from "../FormRenderer.js";
3
+ import type { FormRendererProps, FormRendererHandle } from "../FormRenderer.js";
4
+ import {
5
+ defaultComponentMap,
6
+ defaultFieldWrapper,
7
+ defaultLayout,
8
+ defaultWizardLayout,
9
+ defaultPageWrapper,
10
+ } from "./componentMap.js";
11
+
12
+ export interface DefaultFormRendererProps
13
+ extends Omit<FormRendererProps, "components"> {
14
+ /** Component map (defaults to defaultComponentMap if not provided) */
15
+ components?: FormRendererProps["components"];
16
+ /** Use wizard layout for multi-page forms */
17
+ wizardLayout?: boolean;
18
+ }
19
+
20
+ export const DefaultFormRenderer = forwardRef<
21
+ FormRendererHandle,
22
+ DefaultFormRendererProps
23
+ >(function DefaultFormRenderer(props, ref) {
24
+ const {
25
+ components,
26
+ wizardLayout,
27
+ layout,
28
+ fieldWrapper,
29
+ pageWrapper,
30
+ ...rest
31
+ } = props;
32
+
33
+ return (
34
+ <FormRenderer
35
+ ref={ref}
36
+ components={components ?? defaultComponentMap}
37
+ fieldWrapper={fieldWrapper ?? defaultFieldWrapper}
38
+ layout={layout ?? (wizardLayout ? defaultWizardLayout : defaultLayout)}
39
+ pageWrapper={pageWrapper ?? defaultPageWrapper}
40
+ {...rest}
41
+ />
42
+ );
43
+ });
@@ -0,0 +1,45 @@
1
+ import type { ComponentMap } from "../types.js";
2
+ import { TextInput } from "./components/TextInput.js";
3
+ import { TextareaInput } from "./components/TextareaInput.js";
4
+ import { NumberInput, IntegerInput } from "./components/NumberInput.js";
5
+ import { BooleanInput } from "./components/BooleanInput.js";
6
+ import { DateInput, DateTimeInput } from "./components/DateInput.js";
7
+ import { SelectInput } from "./components/SelectInput.js";
8
+ import { MultiSelectInput } from "./components/MultiSelectInput.js";
9
+ import { ArrayField } from "./components/ArrayField.js";
10
+ import { ObjectField } from "./components/ObjectField.js";
11
+ import { ComputedDisplay } from "./components/ComputedDisplay.js";
12
+ import { DisplayField } from "./components/DisplayField.js";
13
+ import { MatrixField } from "./components/MatrixField.js";
14
+ import { FallbackField } from "./components/FallbackField.js";
15
+ import { FieldWrapper } from "./layout/FieldWrapper.js";
16
+ import { FormLayout } from "./layout/FormLayout.js";
17
+ import { WizardLayout } from "./layout/WizardLayout.js";
18
+ import { PageWrapper } from "./layout/PageWrapper.js";
19
+
20
+ export const defaultComponentMap: ComponentMap = {
21
+ text: TextInput,
22
+ email: TextInput,
23
+ phone: TextInput,
24
+ url: TextInput,
25
+ password: TextInput,
26
+ textarea: TextareaInput,
27
+ number: NumberInput,
28
+ integer: IntegerInput,
29
+ boolean: BooleanInput,
30
+ date: DateInput,
31
+ datetime: DateTimeInput,
32
+ select: SelectInput,
33
+ multiselect: MultiSelectInput,
34
+ array: ArrayField,
35
+ object: ObjectField,
36
+ computed: ComputedDisplay,
37
+ display: DisplayField,
38
+ matrix: MatrixField,
39
+ fallback: FallbackField,
40
+ };
41
+
42
+ export const defaultFieldWrapper = FieldWrapper;
43
+ export const defaultLayout = FormLayout;
44
+ export const defaultWizardLayout = WizardLayout;
45
+ export const defaultPageWrapper = PageWrapper;
@@ -0,0 +1,183 @@
1
+ import React, { useRef } from "react";
2
+ import type { ArrayComponentProps } from "../../types.js";
3
+
4
+ export function ArrayField({ field }: ArrayComponentProps) {
5
+ const hasErrors = field.visibleErrors.length > 0;
6
+
7
+ // Stable keys via ref-based sequential counter
8
+ const itemKeysRef = useRef<string[]>([]);
9
+ const nextKeyRef = useRef(0);
10
+
11
+ const currentLength = field.helpers.items.length;
12
+ const keysLength = itemKeysRef.current.length;
13
+
14
+ if (currentLength > keysLength) {
15
+ for (let i = keysLength; i < currentLength; i++) {
16
+ itemKeysRef.current.push(`item-${nextKeyRef.current++}`);
17
+ }
18
+ } else if (currentLength < keysLength) {
19
+ itemKeysRef.current.length = currentLength;
20
+ }
21
+
22
+ const fieldOrder =
23
+ field.itemFieldOrder ?? Object.keys(field.itemFields);
24
+
25
+ return (
26
+ <div
27
+ className="forma-array"
28
+ aria-invalid={hasErrors || undefined}
29
+ >
30
+ {field.helpers.items.length === 0 && (
31
+ <p className="forma-array__empty">No items</p>
32
+ )}
33
+ {field.helpers.items.map((_, index) => (
34
+ <div
35
+ key={itemKeysRef.current[index]}
36
+ className="forma-array__item"
37
+ >
38
+ <div className="forma-array__item-fields">
39
+ {fieldOrder.map((fieldName) => {
40
+ const itemProps = field.helpers.getItemFieldProps(
41
+ index,
42
+ fieldName,
43
+ );
44
+ return (
45
+ <div key={fieldName} className="forma-field">
46
+ <label
47
+ htmlFor={itemProps.name}
48
+ className="forma-label"
49
+ >
50
+ {itemProps.label}
51
+ </label>
52
+ {renderItemField(itemProps)}
53
+ {itemProps.errors.length > 0 && itemProps.touched && (
54
+ <div className="forma-field__errors" role="alert">
55
+ {itemProps.errors.map((err, i) => (
56
+ <span key={i} className="forma-field__error">
57
+ {err.message}
58
+ </span>
59
+ ))}
60
+ </div>
61
+ )}
62
+ </div>
63
+ );
64
+ })}
65
+ </div>
66
+ <button
67
+ type="button"
68
+ className="forma-button forma-button--danger forma-array__remove"
69
+ onClick={() => field.helpers.remove(index)}
70
+ disabled={!field.helpers.canRemove || field.disabled}
71
+ >
72
+ Remove
73
+ </button>
74
+ </div>
75
+ ))}
76
+ <button
77
+ type="button"
78
+ className="forma-button forma-button--secondary forma-array__add"
79
+ onClick={() => field.helpers.push()}
80
+ disabled={!field.helpers.canAdd || field.disabled}
81
+ >
82
+ + Add Item
83
+ </button>
84
+ </div>
85
+ );
86
+ }
87
+
88
+ function renderItemField(
89
+ itemProps: ReturnType<
90
+ ArrayComponentProps["field"]["helpers"]["getItemFieldProps"]
91
+ >,
92
+ ) {
93
+ const type = itemProps.type;
94
+
95
+ if (type === "select" && itemProps.options) {
96
+ return (
97
+ <select
98
+ id={itemProps.name}
99
+ className="forma-select"
100
+ value={String(itemProps.value ?? "")}
101
+ onChange={(e) => {
102
+ const value = e.target.value;
103
+ if (!value) {
104
+ itemProps.onChange(null);
105
+ } else {
106
+ const option = itemProps.options?.find(
107
+ (opt) => String(opt.value) === value,
108
+ );
109
+ itemProps.onChange(option ? option.value : value);
110
+ }
111
+ }}
112
+ onBlur={itemProps.onBlur}
113
+ disabled={!itemProps.enabled}
114
+ >
115
+ <option value="">Select...</option>
116
+ {itemProps.options.map((opt) => (
117
+ <option key={String(opt.value)} value={String(opt.value)}>
118
+ {opt.label}
119
+ </option>
120
+ ))}
121
+ </select>
122
+ );
123
+ }
124
+
125
+ if (type === "number" || type === "integer") {
126
+ return (
127
+ <input
128
+ id={itemProps.name}
129
+ type="number"
130
+ className="forma-input"
131
+ value={itemProps.value != null ? String(itemProps.value) : ""}
132
+ onChange={(e) => {
133
+ const val = e.target.value;
134
+ if (val === "") {
135
+ itemProps.onChange(null);
136
+ } else {
137
+ const num =
138
+ type === "integer"
139
+ ? parseInt(val, 10)
140
+ : parseFloat(val);
141
+ itemProps.onChange(isNaN(num) ? null : num);
142
+ }
143
+ }}
144
+ onBlur={itemProps.onBlur}
145
+ disabled={!itemProps.enabled}
146
+ step={type === "integer" ? 1 : "any"}
147
+ />
148
+ );
149
+ }
150
+
151
+ if (type === "boolean") {
152
+ return (
153
+ <div className="forma-checkbox">
154
+ <input
155
+ id={itemProps.name}
156
+ type="checkbox"
157
+ className="forma-checkbox__input"
158
+ checked={Boolean(itemProps.value)}
159
+ onChange={(e) => itemProps.onChange(e.target.checked)}
160
+ onBlur={itemProps.onBlur}
161
+ disabled={!itemProps.enabled}
162
+ />
163
+ <label htmlFor={itemProps.name} className="forma-checkbox__label">
164
+ {itemProps.label}
165
+ </label>
166
+ </div>
167
+ );
168
+ }
169
+
170
+ // Default: text input
171
+ return (
172
+ <input
173
+ id={itemProps.name}
174
+ type="text"
175
+ className="forma-input"
176
+ value={String(itemProps.value ?? "")}
177
+ onChange={(e) => itemProps.onChange(e.target.value)}
178
+ onBlur={itemProps.onBlur}
179
+ disabled={!itemProps.enabled}
180
+ placeholder={itemProps.placeholder}
181
+ />
182
+ );
183
+ }
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import type { BooleanComponentProps } from "../../types.js";
3
+
4
+ export function BooleanInput({ field }: BooleanComponentProps) {
5
+ const hasErrors = field.visibleErrors.length > 0;
6
+ const describedBy = [
7
+ field.description ? `${field.name}-description` : null,
8
+ hasErrors ? `${field.name}-errors` : null,
9
+ ]
10
+ .filter(Boolean)
11
+ .join(" ");
12
+
13
+ return (
14
+ <div className="forma-checkbox">
15
+ <input
16
+ id={field.name}
17
+ name={field.name}
18
+ type="checkbox"
19
+ className="forma-checkbox__input"
20
+ checked={field.value ?? false}
21
+ onChange={(e) => field.onChange(e.target.checked)}
22
+ onBlur={field.onBlur}
23
+ disabled={field.disabled}
24
+ aria-invalid={hasErrors || undefined}
25
+ aria-describedby={describedBy || undefined}
26
+ />
27
+ <label htmlFor={field.name} className="forma-checkbox__label">
28
+ {field.label}
29
+ </label>
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import { formatValue } from "@fogpipe/forma-core";
3
+ import type { ComputedComponentProps } from "../../types.js";
4
+
5
+ export function ComputedDisplay({ field }: ComputedComponentProps) {
6
+ let displayValue: string;
7
+ if (field.value === null || field.value === undefined) {
8
+ displayValue = "\u2014";
9
+ } else if (typeof field.value === "object") {
10
+ try {
11
+ displayValue = JSON.stringify(field.value);
12
+ } catch {
13
+ displayValue = String(field.value);
14
+ }
15
+ } else {
16
+ displayValue = formatValue(field.value, field.format, field.formatOptions);
17
+ }
18
+
19
+ return (
20
+ <output
21
+ id={field.name}
22
+ className="forma-computed"
23
+ >
24
+ {displayValue}
25
+ </output>
26
+ );
27
+ }