@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.
@@ -6,7 +6,11 @@
6
6
  */
7
7
 
8
8
  import React from "react";
9
- import type { FieldDefinition, JSONSchemaProperty, SelectOption } from "@fogpipe/forma-core";
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): { min?: number; max?: number; step?: number } {
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 = "minimum" in schema && typeof schema.minimum === "number" ? schema.minimum : undefined;
46
- const max = "maximum" in schema && typeof schema.maximum === "number" ? schema.maximum : undefined;
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(itemFields: Record<string, FieldDefinition>): Record<string, unknown> {
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({ fieldPath, components, className }: FieldRendererProps) {
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) return null;
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: BaseFieldProps | TextFieldProps | NumberFieldProps | IntegerFieldProps | SelectFieldProps | MultiSelectFieldProps | ArrayFieldProps | DisplayFieldProps = baseProps;
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] ?? []) as SelectOption[];
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] ?? []) as SelectOption[];
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 (fieldType === "array" && fieldDef.type === "array" && fieldDef.itemFields) {
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]] = [newArray[indexB], newArray[indexA]];
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 SelectOption[] | undefined;
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<string, unknown>;
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 ? forma.data[fieldDef.source] ?? forma.computed[fieldDef.source] : undefined;
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 { onChange: _onChange, value: _value, ...displayBaseProps } = baseProps;
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 "text" | "email" | "password" | "url" | "textarea",
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(Component as React.ComponentType<typeof componentProps>, componentProps);
348
+ const element = React.createElement(
349
+ Component as React.ComponentType<typeof componentProps>,
350
+ componentProps,
351
+ );
294
352
 
295
353
  if (className) {
296
- return <div className={className}>{element}</div>;
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
  }