@fogpipe/forma-react 0.12.0-alpha.1 → 0.12.0-alpha.2
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 +1 -1
- package/dist/index.js +521 -351
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/FieldRenderer.tsx +82 -20
- package/src/FormRenderer.tsx +320 -181
- package/src/__tests__/FieldRenderer.test.tsx +136 -20
- package/src/__tests__/FormRenderer.test.tsx +264 -85
- package/src/useForma.ts +382 -249
package/src/FieldRenderer.tsx
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import React from "react";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
FieldDefinition,
|
|
11
|
+
JSONSchemaProperty,
|
|
12
|
+
SelectOption,
|
|
13
|
+
} from "@fogpipe/forma-core";
|
|
10
14
|
import { isAdornableField } from "@fogpipe/forma-core";
|
|
11
15
|
import { useFormaContext } from "./context.js";
|
|
12
16
|
import type {
|
|
@@ -37,13 +41,23 @@ export interface FieldRendererProps {
|
|
|
37
41
|
/**
|
|
38
42
|
* Extract numeric constraints from JSON Schema property
|
|
39
43
|
*/
|
|
40
|
-
function getNumberConstraints(schema?: JSONSchemaProperty): {
|
|
44
|
+
function getNumberConstraints(schema?: JSONSchemaProperty): {
|
|
45
|
+
min?: number;
|
|
46
|
+
max?: number;
|
|
47
|
+
step?: number;
|
|
48
|
+
} {
|
|
41
49
|
if (!schema) return {};
|
|
42
50
|
if (schema.type !== "number" && schema.type !== "integer") return {};
|
|
43
51
|
|
|
44
52
|
// Extract min/max from schema
|
|
45
|
-
const min =
|
|
46
|
-
|
|
53
|
+
const min =
|
|
54
|
+
"minimum" in schema && typeof schema.minimum === "number"
|
|
55
|
+
? schema.minimum
|
|
56
|
+
: undefined;
|
|
57
|
+
const max =
|
|
58
|
+
"maximum" in schema && typeof schema.maximum === "number"
|
|
59
|
+
? schema.maximum
|
|
60
|
+
: undefined;
|
|
47
61
|
|
|
48
62
|
// Use multipleOf for step if defined, otherwise default to 1 for integers
|
|
49
63
|
let step: number | undefined;
|
|
@@ -59,7 +73,9 @@ function getNumberConstraints(schema?: JSONSchemaProperty): { min?: number; max?
|
|
|
59
73
|
/**
|
|
60
74
|
* Create a default item for an array field based on item field definitions
|
|
61
75
|
*/
|
|
62
|
-
function createDefaultItem(
|
|
76
|
+
function createDefaultItem(
|
|
77
|
+
itemFields: Record<string, FieldDefinition>,
|
|
78
|
+
): Record<string, unknown> {
|
|
63
79
|
const item: Record<string, unknown> = {};
|
|
64
80
|
for (const [fieldName, fieldDef] of Object.entries(itemFields)) {
|
|
65
81
|
if (fieldDef.type === "boolean") {
|
|
@@ -82,7 +98,11 @@ function createDefaultItem(itemFields: Record<string, FieldDefinition>): Record<
|
|
|
82
98
|
* <FieldRenderer fieldPath="email" components={componentMap} />
|
|
83
99
|
* ```
|
|
84
100
|
*/
|
|
85
|
-
export function FieldRenderer({
|
|
101
|
+
export function FieldRenderer({
|
|
102
|
+
fieldPath,
|
|
103
|
+
components,
|
|
104
|
+
className,
|
|
105
|
+
}: FieldRendererProps) {
|
|
86
106
|
const forma = useFormaContext();
|
|
87
107
|
const { spec } = forma;
|
|
88
108
|
|
|
@@ -93,7 +113,9 @@ export function FieldRenderer({ fieldPath, components, className }: FieldRendere
|
|
|
93
113
|
}
|
|
94
114
|
|
|
95
115
|
const isVisible = forma.visibility[fieldPath] !== false;
|
|
96
|
-
if (!isVisible)
|
|
116
|
+
if (!isVisible) {
|
|
117
|
+
return <div data-field-path={fieldPath} hidden />;
|
|
118
|
+
}
|
|
97
119
|
|
|
98
120
|
// Get field type (type is required on all field definitions)
|
|
99
121
|
const fieldType = fieldDef.type;
|
|
@@ -143,7 +165,15 @@ export function FieldRenderer({ fieldPath, components, className }: FieldRendere
|
|
|
143
165
|
};
|
|
144
166
|
|
|
145
167
|
// Build type-specific props
|
|
146
|
-
let fieldProps:
|
|
168
|
+
let fieldProps:
|
|
169
|
+
| BaseFieldProps
|
|
170
|
+
| TextFieldProps
|
|
171
|
+
| NumberFieldProps
|
|
172
|
+
| IntegerFieldProps
|
|
173
|
+
| SelectFieldProps
|
|
174
|
+
| MultiSelectFieldProps
|
|
175
|
+
| ArrayFieldProps
|
|
176
|
+
| DisplayFieldProps = baseProps;
|
|
147
177
|
|
|
148
178
|
if (fieldType === "number") {
|
|
149
179
|
const constraints = getNumberConstraints(schemaProperty);
|
|
@@ -166,7 +196,8 @@ export function FieldRenderer({ fieldPath, components, className }: FieldRendere
|
|
|
166
196
|
} as IntegerFieldProps;
|
|
167
197
|
} else if (fieldType === "select") {
|
|
168
198
|
// Use pre-computed visible options from memoized map
|
|
169
|
-
const visibleOptions = (forma.optionsVisibility[fieldPath] ??
|
|
199
|
+
const visibleOptions = (forma.optionsVisibility[fieldPath] ??
|
|
200
|
+
[]) as SelectOption[];
|
|
170
201
|
fieldProps = {
|
|
171
202
|
...baseProps,
|
|
172
203
|
fieldType: "select",
|
|
@@ -176,7 +207,8 @@ export function FieldRenderer({ fieldPath, components, className }: FieldRendere
|
|
|
176
207
|
} as SelectFieldProps;
|
|
177
208
|
} else if (fieldType === "multiselect") {
|
|
178
209
|
// Use pre-computed visible options from memoized map
|
|
179
|
-
const visibleOptions = (forma.optionsVisibility[fieldPath] ??
|
|
210
|
+
const visibleOptions = (forma.optionsVisibility[fieldPath] ??
|
|
211
|
+
[]) as SelectOption[];
|
|
180
212
|
fieldProps = {
|
|
181
213
|
...baseProps,
|
|
182
214
|
fieldType: "multiselect",
|
|
@@ -184,7 +216,11 @@ export function FieldRenderer({ fieldPath, components, className }: FieldRendere
|
|
|
184
216
|
onChange: baseProps.onChange as (value: string[]) => void,
|
|
185
217
|
options: visibleOptions,
|
|
186
218
|
} as MultiSelectFieldProps;
|
|
187
|
-
} else if (
|
|
219
|
+
} else if (
|
|
220
|
+
fieldType === "array" &&
|
|
221
|
+
fieldDef.type === "array" &&
|
|
222
|
+
fieldDef.itemFields
|
|
223
|
+
) {
|
|
188
224
|
const arrayValue = (baseProps.value as unknown[] | undefined) ?? [];
|
|
189
225
|
const minItems = fieldDef.minItems ?? 0;
|
|
190
226
|
const maxItems = fieldDef.maxItems ?? Infinity;
|
|
@@ -214,7 +250,10 @@ export function FieldRenderer({ fieldPath, components, className }: FieldRendere
|
|
|
214
250
|
},
|
|
215
251
|
swap: (indexA: number, indexB: number) => {
|
|
216
252
|
const newArray = [...arrayValue];
|
|
217
|
-
[newArray[indexA], newArray[indexB]] = [
|
|
253
|
+
[newArray[indexA], newArray[indexB]] = [
|
|
254
|
+
newArray[indexB],
|
|
255
|
+
newArray[indexA],
|
|
256
|
+
];
|
|
218
257
|
forma.setFieldValue(fieldPath, newArray);
|
|
219
258
|
},
|
|
220
259
|
getItemFieldProps: (index: number, fieldName: string) => {
|
|
@@ -224,7 +263,9 @@ export function FieldRenderer({ fieldPath, components, className }: FieldRendere
|
|
|
224
263
|
const itemValue = item[fieldName];
|
|
225
264
|
|
|
226
265
|
// Use pre-computed visible options from memoized map
|
|
227
|
-
const visibleOptions = forma.optionsVisibility[itemPath] as
|
|
266
|
+
const visibleOptions = forma.optionsVisibility[itemPath] as
|
|
267
|
+
| SelectOption[]
|
|
268
|
+
| undefined;
|
|
228
269
|
|
|
229
270
|
return {
|
|
230
271
|
name: itemPath,
|
|
@@ -241,7 +282,10 @@ export function FieldRenderer({ fieldPath, components, className }: FieldRendere
|
|
|
241
282
|
errors: forma.errors.filter((e) => e.field === itemPath),
|
|
242
283
|
onChange: (value: unknown) => {
|
|
243
284
|
const newArray = [...arrayValue];
|
|
244
|
-
const existingItem = (newArray[index] ?? {}) as Record<
|
|
285
|
+
const existingItem = (newArray[index] ?? {}) as Record<
|
|
286
|
+
string,
|
|
287
|
+
unknown
|
|
288
|
+
>;
|
|
245
289
|
newArray[index] = { ...existingItem, [fieldName]: value };
|
|
246
290
|
forma.setFieldValue(fieldPath, newArray);
|
|
247
291
|
},
|
|
@@ -268,9 +312,15 @@ export function FieldRenderer({ fieldPath, components, className }: FieldRendere
|
|
|
268
312
|
} as ArrayFieldProps;
|
|
269
313
|
} else if (fieldType === "display" && fieldDef.type === "display") {
|
|
270
314
|
// Display fields (read-only presentation content)
|
|
271
|
-
const sourceValue = fieldDef.source
|
|
315
|
+
const sourceValue = fieldDef.source
|
|
316
|
+
? (forma.data[fieldDef.source] ?? forma.computed[fieldDef.source])
|
|
317
|
+
: undefined;
|
|
272
318
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
273
|
-
const {
|
|
319
|
+
const {
|
|
320
|
+
onChange: _onChange,
|
|
321
|
+
value: _value,
|
|
322
|
+
...displayBaseProps
|
|
323
|
+
} = baseProps;
|
|
274
324
|
fieldProps = {
|
|
275
325
|
...displayBaseProps,
|
|
276
326
|
fieldType: "display",
|
|
@@ -282,7 +332,12 @@ export function FieldRenderer({ fieldPath, components, className }: FieldRendere
|
|
|
282
332
|
// Text-based fields
|
|
283
333
|
fieldProps = {
|
|
284
334
|
...baseProps,
|
|
285
|
-
fieldType: fieldType as
|
|
335
|
+
fieldType: fieldType as
|
|
336
|
+
| "text"
|
|
337
|
+
| "email"
|
|
338
|
+
| "password"
|
|
339
|
+
| "url"
|
|
340
|
+
| "textarea",
|
|
286
341
|
value: (baseProps.value as string) ?? "",
|
|
287
342
|
onChange: baseProps.onChange as (value: string) => void,
|
|
288
343
|
};
|
|
@@ -290,11 +345,18 @@ export function FieldRenderer({ fieldPath, components, className }: FieldRendere
|
|
|
290
345
|
|
|
291
346
|
// Wrap props in { field, spec } structure for components
|
|
292
347
|
const componentProps = { field: fieldProps, spec };
|
|
293
|
-
const element = React.createElement(
|
|
348
|
+
const element = React.createElement(
|
|
349
|
+
Component as React.ComponentType<typeof componentProps>,
|
|
350
|
+
componentProps,
|
|
351
|
+
);
|
|
294
352
|
|
|
295
353
|
if (className) {
|
|
296
|
-
return
|
|
354
|
+
return (
|
|
355
|
+
<div data-field-path={fieldPath} className={className}>
|
|
356
|
+
{element}
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
297
359
|
}
|
|
298
360
|
|
|
299
|
-
return element
|
|
361
|
+
return <div data-field-path={fieldPath}>{element}</div>;
|
|
300
362
|
}
|