@fogpipe/forma-react 0.6.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/README.md +277 -0
- package/dist/index.d.ts +668 -0
- package/dist/index.js +1039 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
- package/src/ErrorBoundary.tsx +115 -0
- package/src/FieldRenderer.tsx +258 -0
- package/src/FormRenderer.tsx +470 -0
- package/src/__tests__/FormRenderer.test.tsx +803 -0
- package/src/__tests__/test-utils.tsx +297 -0
- package/src/__tests__/useForma.test.ts +1103 -0
- package/src/context.ts +23 -0
- package/src/index.ts +91 -0
- package/src/types.ts +482 -0
- package/src/useForma.ts +681 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
// src/useForma.ts
|
|
2
|
+
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
getVisibility,
|
|
5
|
+
getRequired,
|
|
6
|
+
getEnabled,
|
|
7
|
+
validate,
|
|
8
|
+
calculate,
|
|
9
|
+
getPageVisibility
|
|
10
|
+
} from "@fogpipe/forma-core";
|
|
11
|
+
function formReducer(state, action) {
|
|
12
|
+
switch (action.type) {
|
|
13
|
+
case "SET_FIELD_VALUE":
|
|
14
|
+
return {
|
|
15
|
+
...state,
|
|
16
|
+
data: { ...state.data, [action.field]: action.value },
|
|
17
|
+
isDirty: true,
|
|
18
|
+
isSubmitted: false
|
|
19
|
+
// Clear on data change
|
|
20
|
+
};
|
|
21
|
+
case "SET_FIELD_TOUCHED":
|
|
22
|
+
return {
|
|
23
|
+
...state,
|
|
24
|
+
touched: { ...state.touched, [action.field]: action.touched }
|
|
25
|
+
};
|
|
26
|
+
case "SET_VALUES":
|
|
27
|
+
return {
|
|
28
|
+
...state,
|
|
29
|
+
data: { ...state.data, ...action.values },
|
|
30
|
+
isDirty: true,
|
|
31
|
+
isSubmitted: false
|
|
32
|
+
// Clear on data change
|
|
33
|
+
};
|
|
34
|
+
case "SET_SUBMITTING":
|
|
35
|
+
return { ...state, isSubmitting: action.isSubmitting };
|
|
36
|
+
case "SET_SUBMITTED":
|
|
37
|
+
return { ...state, isSubmitted: action.isSubmitted };
|
|
38
|
+
case "SET_PAGE":
|
|
39
|
+
return { ...state, currentPage: action.page };
|
|
40
|
+
case "RESET":
|
|
41
|
+
return {
|
|
42
|
+
data: action.initialData,
|
|
43
|
+
touched: {},
|
|
44
|
+
isSubmitting: false,
|
|
45
|
+
isSubmitted: false,
|
|
46
|
+
isDirty: false,
|
|
47
|
+
currentPage: 0
|
|
48
|
+
};
|
|
49
|
+
default:
|
|
50
|
+
return state;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function useForma(options) {
|
|
54
|
+
const { spec: inputSpec, initialData = {}, onSubmit, onChange, validateOn = "blur", referenceData, validationDebounceMs = 0 } = options;
|
|
55
|
+
const spec = useMemo(() => {
|
|
56
|
+
if (!referenceData) return inputSpec;
|
|
57
|
+
return {
|
|
58
|
+
...inputSpec,
|
|
59
|
+
referenceData: {
|
|
60
|
+
...inputSpec.referenceData,
|
|
61
|
+
...referenceData
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}, [inputSpec, referenceData]);
|
|
65
|
+
const [state, dispatch] = useReducer(formReducer, {
|
|
66
|
+
data: initialData,
|
|
67
|
+
touched: {},
|
|
68
|
+
isSubmitting: false,
|
|
69
|
+
isSubmitted: false,
|
|
70
|
+
isDirty: false,
|
|
71
|
+
currentPage: 0
|
|
72
|
+
});
|
|
73
|
+
const hasInitialized = useRef(false);
|
|
74
|
+
const computed = useMemo(
|
|
75
|
+
() => calculate(state.data, spec),
|
|
76
|
+
[state.data, spec]
|
|
77
|
+
);
|
|
78
|
+
const visibility = useMemo(
|
|
79
|
+
() => getVisibility(state.data, spec, { computed }),
|
|
80
|
+
[state.data, spec, computed]
|
|
81
|
+
);
|
|
82
|
+
const required = useMemo(
|
|
83
|
+
() => getRequired(state.data, spec, { computed }),
|
|
84
|
+
[state.data, spec, computed]
|
|
85
|
+
);
|
|
86
|
+
const enabled = useMemo(
|
|
87
|
+
() => getEnabled(state.data, spec, { computed }),
|
|
88
|
+
[state.data, spec, computed]
|
|
89
|
+
);
|
|
90
|
+
const immediateValidation = useMemo(
|
|
91
|
+
() => validate(state.data, spec, { computed, onlyVisible: true }),
|
|
92
|
+
[state.data, spec, computed]
|
|
93
|
+
);
|
|
94
|
+
const [debouncedValidation, setDebouncedValidation] = useState(immediateValidation);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (validationDebounceMs <= 0) {
|
|
97
|
+
setDebouncedValidation(immediateValidation);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const timeoutId = setTimeout(() => {
|
|
101
|
+
setDebouncedValidation(immediateValidation);
|
|
102
|
+
}, validationDebounceMs);
|
|
103
|
+
return () => clearTimeout(timeoutId);
|
|
104
|
+
}, [immediateValidation, validationDebounceMs]);
|
|
105
|
+
const validation = validationDebounceMs > 0 ? debouncedValidation : immediateValidation;
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (hasInitialized.current) {
|
|
108
|
+
onChange == null ? void 0 : onChange(state.data, computed);
|
|
109
|
+
} else {
|
|
110
|
+
hasInitialized.current = true;
|
|
111
|
+
}
|
|
112
|
+
}, [state.data, computed, onChange]);
|
|
113
|
+
const setNestedValue = useCallback((path, value) => {
|
|
114
|
+
const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
115
|
+
if (parts.length === 1) {
|
|
116
|
+
dispatch({ type: "SET_FIELD_VALUE", field: path, value });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const buildNestedObject = (data, pathParts, val) => {
|
|
120
|
+
const result = { ...data };
|
|
121
|
+
let current = result;
|
|
122
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
123
|
+
const part = pathParts[i];
|
|
124
|
+
const nextPart = pathParts[i + 1];
|
|
125
|
+
const isNextArrayIndex = /^\d+$/.test(nextPart);
|
|
126
|
+
if (current[part] === void 0) {
|
|
127
|
+
current[part] = isNextArrayIndex ? [] : {};
|
|
128
|
+
} else if (Array.isArray(current[part])) {
|
|
129
|
+
current[part] = [...current[part]];
|
|
130
|
+
} else {
|
|
131
|
+
current[part] = { ...current[part] };
|
|
132
|
+
}
|
|
133
|
+
current = current[part];
|
|
134
|
+
}
|
|
135
|
+
current[pathParts[pathParts.length - 1]] = val;
|
|
136
|
+
return result;
|
|
137
|
+
};
|
|
138
|
+
dispatch({ type: "SET_VALUES", values: buildNestedObject(state.data, parts, value) });
|
|
139
|
+
}, [state.data]);
|
|
140
|
+
const setFieldValue = useCallback(
|
|
141
|
+
(path, value) => {
|
|
142
|
+
setNestedValue(path, value);
|
|
143
|
+
if (validateOn === "change") {
|
|
144
|
+
dispatch({ type: "SET_FIELD_TOUCHED", field: path, touched: true });
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
[validateOn, setNestedValue]
|
|
148
|
+
);
|
|
149
|
+
const setFieldTouched = useCallback((path, touched = true) => {
|
|
150
|
+
dispatch({ type: "SET_FIELD_TOUCHED", field: path, touched });
|
|
151
|
+
}, []);
|
|
152
|
+
const setValues = useCallback((values) => {
|
|
153
|
+
dispatch({ type: "SET_VALUES", values });
|
|
154
|
+
}, []);
|
|
155
|
+
const validateField = useCallback(
|
|
156
|
+
(path) => {
|
|
157
|
+
return validation.errors.filter((e) => e.field === path);
|
|
158
|
+
},
|
|
159
|
+
[validation]
|
|
160
|
+
);
|
|
161
|
+
const validateForm = useCallback(() => {
|
|
162
|
+
return validation;
|
|
163
|
+
}, [validation]);
|
|
164
|
+
const submitForm = useCallback(async () => {
|
|
165
|
+
dispatch({ type: "SET_SUBMITTING", isSubmitting: true });
|
|
166
|
+
try {
|
|
167
|
+
if (immediateValidation.valid && onSubmit) {
|
|
168
|
+
await onSubmit(state.data);
|
|
169
|
+
}
|
|
170
|
+
dispatch({ type: "SET_SUBMITTED", isSubmitted: true });
|
|
171
|
+
} finally {
|
|
172
|
+
dispatch({ type: "SET_SUBMITTING", isSubmitting: false });
|
|
173
|
+
}
|
|
174
|
+
}, [immediateValidation, onSubmit, state.data]);
|
|
175
|
+
const resetForm = useCallback(() => {
|
|
176
|
+
dispatch({ type: "RESET", initialData });
|
|
177
|
+
}, [initialData]);
|
|
178
|
+
const wizard = useMemo(() => {
|
|
179
|
+
if (!spec.pages || spec.pages.length === 0) return null;
|
|
180
|
+
const pageVisibility = getPageVisibility(state.data, spec, { computed });
|
|
181
|
+
const pages = spec.pages.map((p) => ({
|
|
182
|
+
id: p.id,
|
|
183
|
+
title: p.title,
|
|
184
|
+
description: p.description,
|
|
185
|
+
visible: pageVisibility[p.id] !== false,
|
|
186
|
+
fields: p.fields
|
|
187
|
+
}));
|
|
188
|
+
const visiblePages = pages.filter((p) => p.visible);
|
|
189
|
+
const currentPage = visiblePages[state.currentPage] || null;
|
|
190
|
+
const hasNextPage = state.currentPage < visiblePages.length - 1;
|
|
191
|
+
const hasPreviousPage = state.currentPage > 0;
|
|
192
|
+
const isLastPage = state.currentPage === visiblePages.length - 1;
|
|
193
|
+
return {
|
|
194
|
+
pages,
|
|
195
|
+
currentPageIndex: state.currentPage,
|
|
196
|
+
currentPage,
|
|
197
|
+
goToPage: (index) => dispatch({ type: "SET_PAGE", page: index }),
|
|
198
|
+
nextPage: () => {
|
|
199
|
+
if (hasNextPage) {
|
|
200
|
+
dispatch({ type: "SET_PAGE", page: state.currentPage + 1 });
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
previousPage: () => {
|
|
204
|
+
if (hasPreviousPage) {
|
|
205
|
+
dispatch({ type: "SET_PAGE", page: state.currentPage - 1 });
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
hasNextPage,
|
|
209
|
+
hasPreviousPage,
|
|
210
|
+
canProceed: true,
|
|
211
|
+
// TODO: Validate current page
|
|
212
|
+
isLastPage,
|
|
213
|
+
touchCurrentPageFields: () => {
|
|
214
|
+
if (currentPage) {
|
|
215
|
+
currentPage.fields.forEach((field) => {
|
|
216
|
+
dispatch({ type: "SET_FIELD_TOUCHED", field, touched: true });
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
validateCurrentPage: () => {
|
|
221
|
+
if (!currentPage) return true;
|
|
222
|
+
const pageErrors = validation.errors.filter(
|
|
223
|
+
(e) => currentPage.fields.includes(e.field)
|
|
224
|
+
);
|
|
225
|
+
return pageErrors.length === 0;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}, [spec, state.data, state.currentPage, computed, validation]);
|
|
229
|
+
const getValueAtPath = useCallback((path) => {
|
|
230
|
+
const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
231
|
+
let value = state.data;
|
|
232
|
+
for (const part of parts) {
|
|
233
|
+
if (value === null || value === void 0) return void 0;
|
|
234
|
+
value = value[part];
|
|
235
|
+
}
|
|
236
|
+
return value;
|
|
237
|
+
}, [state.data]);
|
|
238
|
+
const setValueAtPath = useCallback((path, value) => {
|
|
239
|
+
const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
240
|
+
if (parts.length === 1) {
|
|
241
|
+
dispatch({ type: "SET_FIELD_VALUE", field: path, value });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const newData = { ...state.data };
|
|
245
|
+
let current = newData;
|
|
246
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
247
|
+
const part = parts[i];
|
|
248
|
+
const nextPart = parts[i + 1];
|
|
249
|
+
const isNextArrayIndex = /^\d+$/.test(nextPart);
|
|
250
|
+
if (current[part] === void 0) {
|
|
251
|
+
current[part] = isNextArrayIndex ? [] : {};
|
|
252
|
+
} else if (Array.isArray(current[part])) {
|
|
253
|
+
current[part] = [...current[part]];
|
|
254
|
+
} else {
|
|
255
|
+
current[part] = { ...current[part] };
|
|
256
|
+
}
|
|
257
|
+
current = current[part];
|
|
258
|
+
}
|
|
259
|
+
current[parts[parts.length - 1]] = value;
|
|
260
|
+
dispatch({ type: "SET_VALUES", values: newData });
|
|
261
|
+
}, [state.data]);
|
|
262
|
+
const fieldHandlers = useRef(/* @__PURE__ */ new Map());
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
const validFields = new Set(spec.fieldOrder);
|
|
265
|
+
for (const fieldId of spec.fieldOrder) {
|
|
266
|
+
const fieldDef = spec.fields[fieldId];
|
|
267
|
+
if (fieldDef == null ? void 0 : fieldDef.itemFields) {
|
|
268
|
+
for (const key of fieldHandlers.current.keys()) {
|
|
269
|
+
if (key.startsWith(`${fieldId}[`)) {
|
|
270
|
+
validFields.add(key);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const key of fieldHandlers.current.keys()) {
|
|
276
|
+
const baseField = key.split("[")[0];
|
|
277
|
+
if (!validFields.has(key) && !validFields.has(baseField)) {
|
|
278
|
+
fieldHandlers.current.delete(key);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}, [spec]);
|
|
282
|
+
const getFieldHandlers = useCallback((path) => {
|
|
283
|
+
if (!fieldHandlers.current.has(path)) {
|
|
284
|
+
fieldHandlers.current.set(path, {
|
|
285
|
+
onChange: (value) => setValueAtPath(path, value),
|
|
286
|
+
onBlur: () => setFieldTouched(path)
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return fieldHandlers.current.get(path);
|
|
290
|
+
}, [setValueAtPath, setFieldTouched]);
|
|
291
|
+
const getFieldProps = useCallback((path) => {
|
|
292
|
+
const fieldDef = spec.fields[path];
|
|
293
|
+
const handlers = getFieldHandlers(path);
|
|
294
|
+
let fieldType = (fieldDef == null ? void 0 : fieldDef.type) || "text";
|
|
295
|
+
if (!fieldType || fieldType === "computed") {
|
|
296
|
+
const schemaProperty = spec.schema.properties[path];
|
|
297
|
+
if (schemaProperty) {
|
|
298
|
+
if (schemaProperty.type === "number") fieldType = "number";
|
|
299
|
+
else if (schemaProperty.type === "integer") fieldType = "integer";
|
|
300
|
+
else if (schemaProperty.type === "boolean") fieldType = "boolean";
|
|
301
|
+
else if (schemaProperty.type === "array") fieldType = "array";
|
|
302
|
+
else if (schemaProperty.type === "object") fieldType = "object";
|
|
303
|
+
else if ("enum" in schemaProperty && schemaProperty.enum) fieldType = "select";
|
|
304
|
+
else if ("format" in schemaProperty) {
|
|
305
|
+
if (schemaProperty.format === "date") fieldType = "date";
|
|
306
|
+
else if (schemaProperty.format === "date-time") fieldType = "datetime";
|
|
307
|
+
else if (schemaProperty.format === "email") fieldType = "email";
|
|
308
|
+
else if (schemaProperty.format === "uri") fieldType = "url";
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const fieldErrors = validation.errors.filter((e) => e.field === path);
|
|
313
|
+
const isTouched = state.touched[path] ?? false;
|
|
314
|
+
const showErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
|
|
315
|
+
const displayedErrors = showErrors ? fieldErrors : [];
|
|
316
|
+
const hasErrors = displayedErrors.length > 0;
|
|
317
|
+
const isRequired = required[path] ?? false;
|
|
318
|
+
return {
|
|
319
|
+
name: path,
|
|
320
|
+
value: getValueAtPath(path),
|
|
321
|
+
type: fieldType,
|
|
322
|
+
label: (fieldDef == null ? void 0 : fieldDef.label) || path.charAt(0).toUpperCase() + path.slice(1),
|
|
323
|
+
description: fieldDef == null ? void 0 : fieldDef.description,
|
|
324
|
+
placeholder: fieldDef == null ? void 0 : fieldDef.placeholder,
|
|
325
|
+
visible: visibility[path] !== false,
|
|
326
|
+
enabled: enabled[path] !== false,
|
|
327
|
+
required: isRequired,
|
|
328
|
+
touched: isTouched,
|
|
329
|
+
errors: displayedErrors,
|
|
330
|
+
onChange: handlers.onChange,
|
|
331
|
+
onBlur: handlers.onBlur,
|
|
332
|
+
// ARIA accessibility attributes
|
|
333
|
+
"aria-invalid": hasErrors || void 0,
|
|
334
|
+
"aria-describedby": hasErrors ? `${path}-error` : void 0,
|
|
335
|
+
"aria-required": isRequired || void 0
|
|
336
|
+
};
|
|
337
|
+
}, [spec, state.touched, state.isSubmitted, visibility, enabled, required, validation.errors, validateOn, getValueAtPath, getFieldHandlers]);
|
|
338
|
+
const getSelectFieldProps = useCallback((path) => {
|
|
339
|
+
const baseProps = getFieldProps(path);
|
|
340
|
+
const fieldDef = spec.fields[path];
|
|
341
|
+
return {
|
|
342
|
+
...baseProps,
|
|
343
|
+
options: (fieldDef == null ? void 0 : fieldDef.options) ?? []
|
|
344
|
+
};
|
|
345
|
+
}, [getFieldProps, spec.fields]);
|
|
346
|
+
const getArrayHelpers = useCallback((path) => {
|
|
347
|
+
const fieldDef = spec.fields[path];
|
|
348
|
+
const currentValue = getValueAtPath(path) ?? [];
|
|
349
|
+
const minItems = (fieldDef == null ? void 0 : fieldDef.minItems) ?? 0;
|
|
350
|
+
const maxItems = (fieldDef == null ? void 0 : fieldDef.maxItems) ?? Infinity;
|
|
351
|
+
const canAdd = currentValue.length < maxItems;
|
|
352
|
+
const canRemove = currentValue.length > minItems;
|
|
353
|
+
const getItemFieldProps = (index, fieldName) => {
|
|
354
|
+
var _a;
|
|
355
|
+
const itemPath = `${path}[${index}].${fieldName}`;
|
|
356
|
+
const itemFieldDef = (_a = fieldDef == null ? void 0 : fieldDef.itemFields) == null ? void 0 : _a[fieldName];
|
|
357
|
+
const handlers = getFieldHandlers(itemPath);
|
|
358
|
+
const item = currentValue[index];
|
|
359
|
+
const itemValue = item == null ? void 0 : item[fieldName];
|
|
360
|
+
const fieldErrors = validation.errors.filter((e) => e.field === itemPath);
|
|
361
|
+
const isTouched = state.touched[itemPath] ?? false;
|
|
362
|
+
const showErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
|
|
363
|
+
return {
|
|
364
|
+
name: itemPath,
|
|
365
|
+
value: itemValue,
|
|
366
|
+
type: (itemFieldDef == null ? void 0 : itemFieldDef.type) || "text",
|
|
367
|
+
label: (itemFieldDef == null ? void 0 : itemFieldDef.label) || fieldName.charAt(0).toUpperCase() + fieldName.slice(1),
|
|
368
|
+
description: itemFieldDef == null ? void 0 : itemFieldDef.description,
|
|
369
|
+
placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
|
|
370
|
+
visible: true,
|
|
371
|
+
enabled: enabled[path] !== false,
|
|
372
|
+
required: false,
|
|
373
|
+
// TODO: Evaluate item field required
|
|
374
|
+
touched: isTouched,
|
|
375
|
+
errors: showErrors ? fieldErrors : [],
|
|
376
|
+
onChange: handlers.onChange,
|
|
377
|
+
onBlur: handlers.onBlur
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
return {
|
|
381
|
+
items: currentValue,
|
|
382
|
+
push: (item) => {
|
|
383
|
+
if (canAdd) {
|
|
384
|
+
setValueAtPath(path, [...currentValue, item]);
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
remove: (index) => {
|
|
388
|
+
if (canRemove) {
|
|
389
|
+
const newArray = [...currentValue];
|
|
390
|
+
newArray.splice(index, 1);
|
|
391
|
+
setValueAtPath(path, newArray);
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
move: (from, to) => {
|
|
395
|
+
const newArray = [...currentValue];
|
|
396
|
+
const [item] = newArray.splice(from, 1);
|
|
397
|
+
newArray.splice(to, 0, item);
|
|
398
|
+
setValueAtPath(path, newArray);
|
|
399
|
+
},
|
|
400
|
+
swap: (indexA, indexB) => {
|
|
401
|
+
const newArray = [...currentValue];
|
|
402
|
+
[newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];
|
|
403
|
+
setValueAtPath(path, newArray);
|
|
404
|
+
},
|
|
405
|
+
insert: (index, item) => {
|
|
406
|
+
if (canAdd) {
|
|
407
|
+
const newArray = [...currentValue];
|
|
408
|
+
newArray.splice(index, 0, item);
|
|
409
|
+
setValueAtPath(path, newArray);
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
getItemFieldProps,
|
|
413
|
+
minItems,
|
|
414
|
+
maxItems,
|
|
415
|
+
canAdd,
|
|
416
|
+
canRemove
|
|
417
|
+
};
|
|
418
|
+
}, [spec.fields, getValueAtPath, setValueAtPath, getFieldHandlers, enabled, state.touched, state.isSubmitted, validation.errors, validateOn]);
|
|
419
|
+
return {
|
|
420
|
+
data: state.data,
|
|
421
|
+
computed,
|
|
422
|
+
visibility,
|
|
423
|
+
required,
|
|
424
|
+
enabled,
|
|
425
|
+
touched: state.touched,
|
|
426
|
+
errors: validation.errors,
|
|
427
|
+
isValid: validation.valid,
|
|
428
|
+
isSubmitting: state.isSubmitting,
|
|
429
|
+
isSubmitted: state.isSubmitted,
|
|
430
|
+
isDirty: state.isDirty,
|
|
431
|
+
spec,
|
|
432
|
+
wizard,
|
|
433
|
+
setFieldValue,
|
|
434
|
+
setFieldTouched,
|
|
435
|
+
setValues,
|
|
436
|
+
validateField,
|
|
437
|
+
validateForm,
|
|
438
|
+
submitForm,
|
|
439
|
+
resetForm,
|
|
440
|
+
getFieldProps,
|
|
441
|
+
getSelectFieldProps,
|
|
442
|
+
getArrayHelpers
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/FormRenderer.tsx
|
|
447
|
+
import React, { forwardRef, useImperativeHandle, useRef as useRef2, useMemo as useMemo2, useCallback as useCallback2 } from "react";
|
|
448
|
+
|
|
449
|
+
// src/context.ts
|
|
450
|
+
import { createContext, useContext } from "react";
|
|
451
|
+
var FormaContext = createContext(null);
|
|
452
|
+
function useFormaContext() {
|
|
453
|
+
const context = useContext(FormaContext);
|
|
454
|
+
if (!context) {
|
|
455
|
+
throw new Error("useFormaContext must be used within a FormaContext.Provider");
|
|
456
|
+
}
|
|
457
|
+
return context;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/FormRenderer.tsx
|
|
461
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
462
|
+
function DefaultLayout({ children, onSubmit, isSubmitting }) {
|
|
463
|
+
return /* @__PURE__ */ jsxs(
|
|
464
|
+
"form",
|
|
465
|
+
{
|
|
466
|
+
onSubmit: (e) => {
|
|
467
|
+
e.preventDefault();
|
|
468
|
+
onSubmit();
|
|
469
|
+
},
|
|
470
|
+
children: [
|
|
471
|
+
children,
|
|
472
|
+
/* @__PURE__ */ jsx("button", { type: "submit", disabled: isSubmitting, children: isSubmitting ? "Submitting..." : "Submit" })
|
|
473
|
+
]
|
|
474
|
+
}
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
function DefaultFieldWrapper({ fieldPath, field, children, errors, required, visible }) {
|
|
478
|
+
if (!visible) return null;
|
|
479
|
+
const errorId = `${fieldPath}-error`;
|
|
480
|
+
const descriptionId = field.description ? `${fieldPath}-description` : void 0;
|
|
481
|
+
const hasErrors = errors.length > 0;
|
|
482
|
+
return /* @__PURE__ */ jsxs("div", { className: "field-wrapper", "data-field-path": fieldPath, children: [
|
|
483
|
+
field.label && /* @__PURE__ */ jsxs("label", { htmlFor: fieldPath, children: [
|
|
484
|
+
field.label,
|
|
485
|
+
required && /* @__PURE__ */ jsx("span", { className: "required", "aria-hidden": "true", children: "*" }),
|
|
486
|
+
required && /* @__PURE__ */ jsx("span", { className: "sr-only", children: " (required)" })
|
|
487
|
+
] }),
|
|
488
|
+
children,
|
|
489
|
+
hasErrors && /* @__PURE__ */ jsx(
|
|
490
|
+
"div",
|
|
491
|
+
{
|
|
492
|
+
id: errorId,
|
|
493
|
+
className: "field-errors",
|
|
494
|
+
role: "alert",
|
|
495
|
+
"aria-live": "polite",
|
|
496
|
+
children: errors.map((error, i) => /* @__PURE__ */ jsx("span", { className: "error", children: error.message }, i))
|
|
497
|
+
}
|
|
498
|
+
),
|
|
499
|
+
field.description && /* @__PURE__ */ jsx("p", { id: descriptionId, className: "field-description", children: field.description })
|
|
500
|
+
] });
|
|
501
|
+
}
|
|
502
|
+
function DefaultPageWrapper({ title, description, children }) {
|
|
503
|
+
return /* @__PURE__ */ jsxs("div", { className: "page-wrapper", children: [
|
|
504
|
+
/* @__PURE__ */ jsx("h2", { children: title }),
|
|
505
|
+
description && /* @__PURE__ */ jsx("p", { children: description }),
|
|
506
|
+
children
|
|
507
|
+
] });
|
|
508
|
+
}
|
|
509
|
+
function getNumberConstraints(schema) {
|
|
510
|
+
if (!schema) return {};
|
|
511
|
+
if (schema.type !== "number" && schema.type !== "integer") return {};
|
|
512
|
+
return {
|
|
513
|
+
min: "minimum" in schema ? schema.minimum : void 0,
|
|
514
|
+
max: "maximum" in schema ? schema.maximum : void 0,
|
|
515
|
+
step: schema.type === "integer" ? 1 : void 0
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function createDefaultItem(itemFields) {
|
|
519
|
+
const item = {};
|
|
520
|
+
for (const [fieldName, fieldDef] of Object.entries(itemFields)) {
|
|
521
|
+
if (fieldDef.type === "boolean") {
|
|
522
|
+
item[fieldName] = false;
|
|
523
|
+
} else if (fieldDef.type === "number" || fieldDef.type === "integer") {
|
|
524
|
+
item[fieldName] = null;
|
|
525
|
+
} else {
|
|
526
|
+
item[fieldName] = "";
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return item;
|
|
530
|
+
}
|
|
531
|
+
var FormRenderer = forwardRef(
|
|
532
|
+
function FormRenderer2(props, ref) {
|
|
533
|
+
const {
|
|
534
|
+
spec,
|
|
535
|
+
initialData,
|
|
536
|
+
onSubmit,
|
|
537
|
+
onChange,
|
|
538
|
+
components,
|
|
539
|
+
layout: Layout = DefaultLayout,
|
|
540
|
+
fieldWrapper: FieldWrapper = DefaultFieldWrapper,
|
|
541
|
+
pageWrapper: PageWrapper = DefaultPageWrapper,
|
|
542
|
+
validateOn
|
|
543
|
+
} = props;
|
|
544
|
+
const forma = useForma({
|
|
545
|
+
spec,
|
|
546
|
+
initialData,
|
|
547
|
+
onSubmit,
|
|
548
|
+
onChange,
|
|
549
|
+
validateOn
|
|
550
|
+
});
|
|
551
|
+
const fieldRefs = useRef2(/* @__PURE__ */ new Map());
|
|
552
|
+
const arrayHelpersCache = useRef2(/* @__PURE__ */ new Map());
|
|
553
|
+
const focusField = useCallback2((path) => {
|
|
554
|
+
const element = fieldRefs.current.get(path);
|
|
555
|
+
element == null ? void 0 : element.focus();
|
|
556
|
+
}, []);
|
|
557
|
+
const focusFirstError = useCallback2(() => {
|
|
558
|
+
const firstError = forma.errors[0];
|
|
559
|
+
if (firstError) {
|
|
560
|
+
focusField(firstError.field);
|
|
561
|
+
}
|
|
562
|
+
}, [forma.errors, focusField]);
|
|
563
|
+
useImperativeHandle(
|
|
564
|
+
ref,
|
|
565
|
+
() => ({
|
|
566
|
+
submitForm: forma.submitForm,
|
|
567
|
+
resetForm: forma.resetForm,
|
|
568
|
+
validateForm: forma.validateForm,
|
|
569
|
+
focusField,
|
|
570
|
+
focusFirstError,
|
|
571
|
+
getValues: () => forma.data,
|
|
572
|
+
setValues: forma.setValues,
|
|
573
|
+
isValid: forma.isValid,
|
|
574
|
+
isDirty: forma.isDirty
|
|
575
|
+
}),
|
|
576
|
+
[forma, focusField, focusFirstError]
|
|
577
|
+
);
|
|
578
|
+
const fieldsToRender = useMemo2(() => {
|
|
579
|
+
var _a;
|
|
580
|
+
if (spec.pages && spec.pages.length > 0 && forma.wizard) {
|
|
581
|
+
const currentPage = forma.wizard.currentPage;
|
|
582
|
+
if (currentPage) {
|
|
583
|
+
return currentPage.fields;
|
|
584
|
+
}
|
|
585
|
+
return ((_a = spec.pages[0]) == null ? void 0 : _a.fields) ?? [];
|
|
586
|
+
}
|
|
587
|
+
return spec.fieldOrder;
|
|
588
|
+
}, [spec.pages, spec.fieldOrder, forma.wizard]);
|
|
589
|
+
const renderField = useCallback2((fieldPath) => {
|
|
590
|
+
const fieldDef = spec.fields[fieldPath];
|
|
591
|
+
if (!fieldDef) return null;
|
|
592
|
+
const isVisible = forma.visibility[fieldPath] !== false;
|
|
593
|
+
if (!isVisible) return null;
|
|
594
|
+
const fieldType = fieldDef.type || (fieldDef.itemFields ? "array" : "text");
|
|
595
|
+
const componentKey = fieldType;
|
|
596
|
+
const Component = components[componentKey] || components.fallback;
|
|
597
|
+
if (!Component) {
|
|
598
|
+
console.warn(`No component found for field type: ${fieldType}`);
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
const errors = forma.errors.filter((e) => e.field === fieldPath);
|
|
602
|
+
const touched = forma.touched[fieldPath] ?? false;
|
|
603
|
+
const required = forma.required[fieldPath] ?? false;
|
|
604
|
+
const disabled = forma.enabled[fieldPath] === false;
|
|
605
|
+
const schemaProperty = spec.schema.properties[fieldPath];
|
|
606
|
+
const baseProps = {
|
|
607
|
+
name: fieldPath,
|
|
608
|
+
field: fieldDef,
|
|
609
|
+
value: forma.data[fieldPath],
|
|
610
|
+
touched,
|
|
611
|
+
required,
|
|
612
|
+
disabled,
|
|
613
|
+
errors,
|
|
614
|
+
onChange: (value) => forma.setFieldValue(fieldPath, value),
|
|
615
|
+
onBlur: () => forma.setFieldTouched(fieldPath),
|
|
616
|
+
// Convenience properties
|
|
617
|
+
visible: true,
|
|
618
|
+
// Always true since we already filtered for visibility
|
|
619
|
+
enabled: !disabled,
|
|
620
|
+
label: fieldDef.label ?? fieldPath,
|
|
621
|
+
description: fieldDef.description,
|
|
622
|
+
placeholder: fieldDef.placeholder
|
|
623
|
+
};
|
|
624
|
+
let fieldProps = baseProps;
|
|
625
|
+
if (fieldType === "number" || fieldType === "integer") {
|
|
626
|
+
const constraints = getNumberConstraints(schemaProperty);
|
|
627
|
+
fieldProps = {
|
|
628
|
+
...baseProps,
|
|
629
|
+
fieldType,
|
|
630
|
+
value: baseProps.value,
|
|
631
|
+
onChange: baseProps.onChange,
|
|
632
|
+
...constraints
|
|
633
|
+
};
|
|
634
|
+
} else if (fieldType === "select" || fieldType === "multiselect") {
|
|
635
|
+
fieldProps = {
|
|
636
|
+
...baseProps,
|
|
637
|
+
fieldType,
|
|
638
|
+
value: baseProps.value,
|
|
639
|
+
onChange: baseProps.onChange,
|
|
640
|
+
options: fieldDef.options ?? []
|
|
641
|
+
};
|
|
642
|
+
} else if (fieldType === "array" && fieldDef.itemFields) {
|
|
643
|
+
const arrayValue = baseProps.value ?? [];
|
|
644
|
+
const minItems = fieldDef.minItems ?? 0;
|
|
645
|
+
const maxItems = fieldDef.maxItems ?? Infinity;
|
|
646
|
+
const itemFieldDefs = fieldDef.itemFields;
|
|
647
|
+
if (!arrayHelpersCache.current.has(fieldPath)) {
|
|
648
|
+
arrayHelpersCache.current.set(fieldPath, {
|
|
649
|
+
push: (item) => {
|
|
650
|
+
const currentArray = forma.data[fieldPath] ?? [];
|
|
651
|
+
const newItem = item ?? createDefaultItem(itemFieldDefs);
|
|
652
|
+
forma.setFieldValue(fieldPath, [...currentArray, newItem]);
|
|
653
|
+
},
|
|
654
|
+
insert: (index, item) => {
|
|
655
|
+
const currentArray = forma.data[fieldPath] ?? [];
|
|
656
|
+
const newArray = [...currentArray];
|
|
657
|
+
newArray.splice(index, 0, item);
|
|
658
|
+
forma.setFieldValue(fieldPath, newArray);
|
|
659
|
+
},
|
|
660
|
+
remove: (index) => {
|
|
661
|
+
const currentArray = forma.data[fieldPath] ?? [];
|
|
662
|
+
const newArray = [...currentArray];
|
|
663
|
+
newArray.splice(index, 1);
|
|
664
|
+
forma.setFieldValue(fieldPath, newArray);
|
|
665
|
+
},
|
|
666
|
+
move: (from, to) => {
|
|
667
|
+
const currentArray = forma.data[fieldPath] ?? [];
|
|
668
|
+
const newArray = [...currentArray];
|
|
669
|
+
const [item] = newArray.splice(from, 1);
|
|
670
|
+
newArray.splice(to, 0, item);
|
|
671
|
+
forma.setFieldValue(fieldPath, newArray);
|
|
672
|
+
},
|
|
673
|
+
swap: (indexA, indexB) => {
|
|
674
|
+
const currentArray = forma.data[fieldPath] ?? [];
|
|
675
|
+
const newArray = [...currentArray];
|
|
676
|
+
[newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];
|
|
677
|
+
forma.setFieldValue(fieldPath, newArray);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
const cachedHelpers = arrayHelpersCache.current.get(fieldPath);
|
|
682
|
+
const helpers = {
|
|
683
|
+
items: arrayValue,
|
|
684
|
+
push: cachedHelpers.push,
|
|
685
|
+
insert: cachedHelpers.insert,
|
|
686
|
+
remove: cachedHelpers.remove,
|
|
687
|
+
move: cachedHelpers.move,
|
|
688
|
+
swap: cachedHelpers.swap,
|
|
689
|
+
getItemFieldProps: (index, fieldName) => {
|
|
690
|
+
var _a;
|
|
691
|
+
const itemFieldDef = itemFieldDefs[fieldName];
|
|
692
|
+
const itemPath = `${fieldPath}[${index}].${fieldName}`;
|
|
693
|
+
const itemValue = (_a = arrayValue[index]) == null ? void 0 : _a[fieldName];
|
|
694
|
+
return {
|
|
695
|
+
name: itemPath,
|
|
696
|
+
value: itemValue,
|
|
697
|
+
type: (itemFieldDef == null ? void 0 : itemFieldDef.type) ?? "text",
|
|
698
|
+
label: (itemFieldDef == null ? void 0 : itemFieldDef.label) ?? fieldName,
|
|
699
|
+
description: itemFieldDef == null ? void 0 : itemFieldDef.description,
|
|
700
|
+
placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
|
|
701
|
+
visible: true,
|
|
702
|
+
enabled: !disabled,
|
|
703
|
+
required: (itemFieldDef == null ? void 0 : itemFieldDef.requiredWhen) === "true",
|
|
704
|
+
touched: forma.touched[itemPath] ?? false,
|
|
705
|
+
errors: forma.errors.filter((e) => e.field === itemPath),
|
|
706
|
+
onChange: (value) => {
|
|
707
|
+
const currentArray = forma.data[fieldPath] ?? [];
|
|
708
|
+
const newArray = [...currentArray];
|
|
709
|
+
const item = newArray[index] ?? {};
|
|
710
|
+
newArray[index] = { ...item, [fieldName]: value };
|
|
711
|
+
forma.setFieldValue(fieldPath, newArray);
|
|
712
|
+
},
|
|
713
|
+
onBlur: () => forma.setFieldTouched(itemPath),
|
|
714
|
+
itemIndex: index,
|
|
715
|
+
fieldName,
|
|
716
|
+
options: itemFieldDef == null ? void 0 : itemFieldDef.options
|
|
717
|
+
};
|
|
718
|
+
},
|
|
719
|
+
minItems,
|
|
720
|
+
maxItems,
|
|
721
|
+
canAdd: arrayValue.length < maxItems,
|
|
722
|
+
canRemove: arrayValue.length > minItems
|
|
723
|
+
};
|
|
724
|
+
fieldProps = {
|
|
725
|
+
...baseProps,
|
|
726
|
+
fieldType: "array",
|
|
727
|
+
value: arrayValue,
|
|
728
|
+
onChange: baseProps.onChange,
|
|
729
|
+
helpers,
|
|
730
|
+
itemFields: itemFieldDefs,
|
|
731
|
+
minItems,
|
|
732
|
+
maxItems
|
|
733
|
+
};
|
|
734
|
+
} else {
|
|
735
|
+
fieldProps = {
|
|
736
|
+
...baseProps,
|
|
737
|
+
fieldType,
|
|
738
|
+
value: baseProps.value ?? "",
|
|
739
|
+
onChange: baseProps.onChange
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
const componentProps = { field: fieldProps, spec };
|
|
743
|
+
return /* @__PURE__ */ jsx(
|
|
744
|
+
FieldWrapper,
|
|
745
|
+
{
|
|
746
|
+
fieldPath,
|
|
747
|
+
field: fieldDef,
|
|
748
|
+
errors,
|
|
749
|
+
touched,
|
|
750
|
+
required,
|
|
751
|
+
visible: isVisible,
|
|
752
|
+
children: React.createElement(Component, componentProps)
|
|
753
|
+
},
|
|
754
|
+
fieldPath
|
|
755
|
+
);
|
|
756
|
+
}, [spec, forma, components, FieldWrapper]);
|
|
757
|
+
const renderedFields = useMemo2(
|
|
758
|
+
() => fieldsToRender.map(renderField),
|
|
759
|
+
[fieldsToRender, renderField]
|
|
760
|
+
);
|
|
761
|
+
const content = useMemo2(() => {
|
|
762
|
+
if (spec.pages && spec.pages.length > 0 && forma.wizard) {
|
|
763
|
+
const currentPage = forma.wizard.currentPage;
|
|
764
|
+
if (!currentPage) return null;
|
|
765
|
+
return /* @__PURE__ */ jsx(
|
|
766
|
+
PageWrapper,
|
|
767
|
+
{
|
|
768
|
+
title: currentPage.title,
|
|
769
|
+
description: currentPage.description,
|
|
770
|
+
pageIndex: forma.wizard.currentPageIndex,
|
|
771
|
+
totalPages: forma.wizard.pages.length,
|
|
772
|
+
children: renderedFields
|
|
773
|
+
}
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
return /* @__PURE__ */ jsx(Fragment, { children: renderedFields });
|
|
777
|
+
}, [spec.pages, forma.wizard, PageWrapper, renderedFields]);
|
|
778
|
+
return /* @__PURE__ */ jsx(FormaContext.Provider, { value: forma, children: /* @__PURE__ */ jsx(
|
|
779
|
+
Layout,
|
|
780
|
+
{
|
|
781
|
+
onSubmit: forma.submitForm,
|
|
782
|
+
isSubmitting: forma.isSubmitting,
|
|
783
|
+
isValid: forma.isValid,
|
|
784
|
+
children: content
|
|
785
|
+
}
|
|
786
|
+
) });
|
|
787
|
+
}
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
// src/FieldRenderer.tsx
|
|
791
|
+
import React2 from "react";
|
|
792
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
793
|
+
function getNumberConstraints2(schema) {
|
|
794
|
+
if (!schema) return {};
|
|
795
|
+
if (schema.type !== "number" && schema.type !== "integer") return {};
|
|
796
|
+
return {
|
|
797
|
+
min: "minimum" in schema ? schema.minimum : void 0,
|
|
798
|
+
max: "maximum" in schema ? schema.maximum : void 0,
|
|
799
|
+
step: schema.type === "integer" ? 1 : void 0
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function createDefaultItem2(itemFields) {
|
|
803
|
+
const item = {};
|
|
804
|
+
for (const [fieldName, fieldDef] of Object.entries(itemFields)) {
|
|
805
|
+
if (fieldDef.type === "boolean") {
|
|
806
|
+
item[fieldName] = false;
|
|
807
|
+
} else if (fieldDef.type === "number" || fieldDef.type === "integer") {
|
|
808
|
+
item[fieldName] = null;
|
|
809
|
+
} else {
|
|
810
|
+
item[fieldName] = "";
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return item;
|
|
814
|
+
}
|
|
815
|
+
function FieldRenderer({ fieldPath, components, className }) {
|
|
816
|
+
const forma = useFormaContext();
|
|
817
|
+
const { spec } = forma;
|
|
818
|
+
const fieldDef = spec.fields[fieldPath];
|
|
819
|
+
if (!fieldDef) {
|
|
820
|
+
console.warn(`Field not found: ${fieldPath}`);
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
const isVisible = forma.visibility[fieldPath] !== false;
|
|
824
|
+
if (!isVisible) return null;
|
|
825
|
+
const fieldType = fieldDef.type || (fieldDef.itemFields ? "array" : "text");
|
|
826
|
+
const componentKey = fieldType;
|
|
827
|
+
const Component = components[componentKey] || components.fallback;
|
|
828
|
+
if (!Component) {
|
|
829
|
+
console.warn(`No component found for field type: ${fieldType}`);
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
const errors = forma.errors.filter((e) => e.field === fieldPath);
|
|
833
|
+
const touched = forma.touched[fieldPath] ?? false;
|
|
834
|
+
const required = forma.required[fieldPath] ?? false;
|
|
835
|
+
const disabled = forma.enabled[fieldPath] === false;
|
|
836
|
+
const schemaProperty = spec.schema.properties[fieldPath];
|
|
837
|
+
const baseProps = {
|
|
838
|
+
name: fieldPath,
|
|
839
|
+
field: fieldDef,
|
|
840
|
+
value: forma.data[fieldPath],
|
|
841
|
+
touched,
|
|
842
|
+
required,
|
|
843
|
+
disabled,
|
|
844
|
+
errors,
|
|
845
|
+
onChange: (value) => forma.setFieldValue(fieldPath, value),
|
|
846
|
+
onBlur: () => forma.setFieldTouched(fieldPath),
|
|
847
|
+
// Convenience properties
|
|
848
|
+
visible: true,
|
|
849
|
+
// Always true since we already filtered for visibility
|
|
850
|
+
enabled: !disabled,
|
|
851
|
+
label: fieldDef.label ?? fieldPath,
|
|
852
|
+
description: fieldDef.description,
|
|
853
|
+
placeholder: fieldDef.placeholder
|
|
854
|
+
};
|
|
855
|
+
let fieldProps = baseProps;
|
|
856
|
+
if (fieldType === "number") {
|
|
857
|
+
const constraints = getNumberConstraints2(schemaProperty);
|
|
858
|
+
fieldProps = {
|
|
859
|
+
...baseProps,
|
|
860
|
+
fieldType: "number",
|
|
861
|
+
value: baseProps.value,
|
|
862
|
+
onChange: baseProps.onChange,
|
|
863
|
+
...constraints
|
|
864
|
+
};
|
|
865
|
+
} else if (fieldType === "integer") {
|
|
866
|
+
const constraints = getNumberConstraints2(schemaProperty);
|
|
867
|
+
fieldProps = {
|
|
868
|
+
...baseProps,
|
|
869
|
+
fieldType: "integer",
|
|
870
|
+
value: baseProps.value,
|
|
871
|
+
onChange: baseProps.onChange,
|
|
872
|
+
min: constraints.min,
|
|
873
|
+
max: constraints.max
|
|
874
|
+
};
|
|
875
|
+
} else if (fieldType === "select") {
|
|
876
|
+
fieldProps = {
|
|
877
|
+
...baseProps,
|
|
878
|
+
fieldType: "select",
|
|
879
|
+
value: baseProps.value,
|
|
880
|
+
onChange: baseProps.onChange,
|
|
881
|
+
options: fieldDef.options ?? []
|
|
882
|
+
};
|
|
883
|
+
} else if (fieldType === "multiselect") {
|
|
884
|
+
fieldProps = {
|
|
885
|
+
...baseProps,
|
|
886
|
+
fieldType: "multiselect",
|
|
887
|
+
value: baseProps.value ?? [],
|
|
888
|
+
onChange: baseProps.onChange,
|
|
889
|
+
options: fieldDef.options ?? []
|
|
890
|
+
};
|
|
891
|
+
} else if (fieldType === "array" && fieldDef.itemFields) {
|
|
892
|
+
const arrayValue = baseProps.value ?? [];
|
|
893
|
+
const minItems = fieldDef.minItems ?? 0;
|
|
894
|
+
const maxItems = fieldDef.maxItems ?? Infinity;
|
|
895
|
+
const itemFieldDefs = fieldDef.itemFields;
|
|
896
|
+
const helpers = {
|
|
897
|
+
items: arrayValue,
|
|
898
|
+
push: (item) => {
|
|
899
|
+
const newItem = item ?? createDefaultItem2(itemFieldDefs);
|
|
900
|
+
forma.setFieldValue(fieldPath, [...arrayValue, newItem]);
|
|
901
|
+
},
|
|
902
|
+
insert: (index, item) => {
|
|
903
|
+
const newArray = [...arrayValue];
|
|
904
|
+
newArray.splice(index, 0, item);
|
|
905
|
+
forma.setFieldValue(fieldPath, newArray);
|
|
906
|
+
},
|
|
907
|
+
remove: (index) => {
|
|
908
|
+
const newArray = [...arrayValue];
|
|
909
|
+
newArray.splice(index, 1);
|
|
910
|
+
forma.setFieldValue(fieldPath, newArray);
|
|
911
|
+
},
|
|
912
|
+
move: (from, to) => {
|
|
913
|
+
const newArray = [...arrayValue];
|
|
914
|
+
const [item] = newArray.splice(from, 1);
|
|
915
|
+
newArray.splice(to, 0, item);
|
|
916
|
+
forma.setFieldValue(fieldPath, newArray);
|
|
917
|
+
},
|
|
918
|
+
swap: (indexA, indexB) => {
|
|
919
|
+
const newArray = [...arrayValue];
|
|
920
|
+
[newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];
|
|
921
|
+
forma.setFieldValue(fieldPath, newArray);
|
|
922
|
+
},
|
|
923
|
+
getItemFieldProps: (index, fieldName) => {
|
|
924
|
+
var _a;
|
|
925
|
+
const itemFieldDef = itemFieldDefs[fieldName];
|
|
926
|
+
const itemPath = `${fieldPath}[${index}].${fieldName}`;
|
|
927
|
+
const itemValue = (_a = arrayValue[index]) == null ? void 0 : _a[fieldName];
|
|
928
|
+
return {
|
|
929
|
+
name: itemPath,
|
|
930
|
+
value: itemValue,
|
|
931
|
+
type: (itemFieldDef == null ? void 0 : itemFieldDef.type) ?? "text",
|
|
932
|
+
label: (itemFieldDef == null ? void 0 : itemFieldDef.label) ?? fieldName,
|
|
933
|
+
description: itemFieldDef == null ? void 0 : itemFieldDef.description,
|
|
934
|
+
placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
|
|
935
|
+
visible: true,
|
|
936
|
+
enabled: !disabled,
|
|
937
|
+
required: (itemFieldDef == null ? void 0 : itemFieldDef.requiredWhen) === "true",
|
|
938
|
+
touched: forma.touched[itemPath] ?? false,
|
|
939
|
+
errors: forma.errors.filter((e) => e.field === itemPath),
|
|
940
|
+
onChange: (value) => {
|
|
941
|
+
const newArray = [...arrayValue];
|
|
942
|
+
const item = newArray[index] ?? {};
|
|
943
|
+
newArray[index] = { ...item, [fieldName]: value };
|
|
944
|
+
forma.setFieldValue(fieldPath, newArray);
|
|
945
|
+
},
|
|
946
|
+
onBlur: () => forma.setFieldTouched(itemPath),
|
|
947
|
+
itemIndex: index,
|
|
948
|
+
fieldName,
|
|
949
|
+
options: itemFieldDef == null ? void 0 : itemFieldDef.options
|
|
950
|
+
};
|
|
951
|
+
},
|
|
952
|
+
minItems,
|
|
953
|
+
maxItems,
|
|
954
|
+
canAdd: arrayValue.length < maxItems,
|
|
955
|
+
canRemove: arrayValue.length > minItems
|
|
956
|
+
};
|
|
957
|
+
fieldProps = {
|
|
958
|
+
...baseProps,
|
|
959
|
+
fieldType: "array",
|
|
960
|
+
value: arrayValue,
|
|
961
|
+
onChange: baseProps.onChange,
|
|
962
|
+
helpers,
|
|
963
|
+
itemFields: itemFieldDefs,
|
|
964
|
+
minItems,
|
|
965
|
+
maxItems
|
|
966
|
+
};
|
|
967
|
+
} else {
|
|
968
|
+
fieldProps = {
|
|
969
|
+
...baseProps,
|
|
970
|
+
fieldType,
|
|
971
|
+
value: baseProps.value ?? "",
|
|
972
|
+
onChange: baseProps.onChange
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
const componentProps = { field: fieldProps, spec };
|
|
976
|
+
const element = React2.createElement(Component, componentProps);
|
|
977
|
+
if (className) {
|
|
978
|
+
return /* @__PURE__ */ jsx2("div", { className, children: element });
|
|
979
|
+
}
|
|
980
|
+
return element;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/ErrorBoundary.tsx
|
|
984
|
+
import React3 from "react";
|
|
985
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
986
|
+
function DefaultErrorFallback({ error, onReset }) {
|
|
987
|
+
return /* @__PURE__ */ jsxs2("div", { className: "forma-error-boundary", role: "alert", children: [
|
|
988
|
+
/* @__PURE__ */ jsx3("h3", { children: "Something went wrong" }),
|
|
989
|
+
/* @__PURE__ */ jsx3("p", { children: "An error occurred while rendering the form." }),
|
|
990
|
+
/* @__PURE__ */ jsxs2("details", { children: [
|
|
991
|
+
/* @__PURE__ */ jsx3("summary", { children: "Error details" }),
|
|
992
|
+
/* @__PURE__ */ jsx3("pre", { children: error.message })
|
|
993
|
+
] }),
|
|
994
|
+
/* @__PURE__ */ jsx3("button", { type: "button", onClick: onReset, children: "Try again" })
|
|
995
|
+
] });
|
|
996
|
+
}
|
|
997
|
+
var FormaErrorBoundary = class extends React3.Component {
|
|
998
|
+
constructor(props) {
|
|
999
|
+
super(props);
|
|
1000
|
+
this.state = { hasError: false, error: null };
|
|
1001
|
+
}
|
|
1002
|
+
static getDerivedStateFromError(error) {
|
|
1003
|
+
return { hasError: true, error };
|
|
1004
|
+
}
|
|
1005
|
+
componentDidCatch(error, errorInfo) {
|
|
1006
|
+
var _a, _b;
|
|
1007
|
+
(_b = (_a = this.props).onError) == null ? void 0 : _b.call(_a, error, errorInfo);
|
|
1008
|
+
}
|
|
1009
|
+
componentDidUpdate(prevProps) {
|
|
1010
|
+
if (this.state.hasError && prevProps.resetKey !== this.props.resetKey) {
|
|
1011
|
+
this.setState({ hasError: false, error: null });
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
reset = () => {
|
|
1015
|
+
this.setState({ hasError: false, error: null });
|
|
1016
|
+
};
|
|
1017
|
+
render() {
|
|
1018
|
+
if (this.state.hasError && this.state.error) {
|
|
1019
|
+
const { fallback } = this.props;
|
|
1020
|
+
if (typeof fallback === "function") {
|
|
1021
|
+
return fallback(this.state.error, this.reset);
|
|
1022
|
+
}
|
|
1023
|
+
if (fallback) {
|
|
1024
|
+
return fallback;
|
|
1025
|
+
}
|
|
1026
|
+
return /* @__PURE__ */ jsx3(DefaultErrorFallback, { error: this.state.error, onReset: this.reset });
|
|
1027
|
+
}
|
|
1028
|
+
return this.props.children;
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
export {
|
|
1032
|
+
FieldRenderer,
|
|
1033
|
+
FormRenderer,
|
|
1034
|
+
FormaContext,
|
|
1035
|
+
FormaErrorBoundary,
|
|
1036
|
+
useForma,
|
|
1037
|
+
useFormaContext
|
|
1038
|
+
};
|
|
1039
|
+
//# sourceMappingURL=index.js.map
|