@form-engine-ts/react 1.1.0 → 2.0.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 +30 -0
- package/dist/index.cjs +612 -141
- package/dist/index.d.cts +109 -6
- package/dist/index.d.ts +109 -6
- package/dist/index.js +589 -110
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -24,13 +24,390 @@ __export(index_exports, {
|
|
|
24
24
|
FormProvider: () => FormProvider,
|
|
25
25
|
FormRenderer: () => FormRenderer,
|
|
26
26
|
useField: () => useField,
|
|
27
|
-
useForm: () => useForm
|
|
27
|
+
useForm: () => useForm,
|
|
28
|
+
useFormBuilder: () => useFormBuilder
|
|
28
29
|
});
|
|
29
30
|
module.exports = __toCommonJS(index_exports);
|
|
30
31
|
|
|
31
32
|
// src/builder.tsx
|
|
33
|
+
var import_core2 = require("@form-engine-ts/core");
|
|
34
|
+
var import_react2 = require("react");
|
|
35
|
+
|
|
36
|
+
// src/hooks/useFormBuilder.ts
|
|
32
37
|
var import_core = require("@form-engine-ts/core");
|
|
33
38
|
var import_react = require("react");
|
|
39
|
+
var DEFAULT_PREFIXES = { field: "q", option: "opt", page: "page" };
|
|
40
|
+
function defaultIdFactory(kind, existingIds) {
|
|
41
|
+
const prefix = DEFAULT_PREFIXES[kind];
|
|
42
|
+
let id;
|
|
43
|
+
do {
|
|
44
|
+
id = `${prefix}_${globalThis.crypto.randomUUID().slice(0, 8)}`;
|
|
45
|
+
} while (existingIds.has(id));
|
|
46
|
+
return id;
|
|
47
|
+
}
|
|
48
|
+
function newField(type, id, optionId) {
|
|
49
|
+
const base = { id, title: "New question", required: false };
|
|
50
|
+
if (type === "text" || type === "textarea") return { ...base, type };
|
|
51
|
+
if (type === "number") return { ...base, type };
|
|
52
|
+
if (type === "rating") return { ...base, type, min: 1, max: 5 };
|
|
53
|
+
if (type === "checkbox") return { ...base, type };
|
|
54
|
+
return optionId === void 0 ? void 0 : { ...base, type, options: [{ id: optionId, label: "Option 1" }] };
|
|
55
|
+
}
|
|
56
|
+
function move(items, sourceIndex, targetIndex) {
|
|
57
|
+
if (sourceIndex < 0 || targetIndex < 0 || targetIndex >= items.length || sourceIndex === targetIndex)
|
|
58
|
+
return void 0;
|
|
59
|
+
const result = [...items];
|
|
60
|
+
const [item] = result.splice(sourceIndex, 1);
|
|
61
|
+
if (item === void 0) return void 0;
|
|
62
|
+
result.splice(targetIndex, 0, item);
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
function addPolicyIssues(schema, policy, issues) {
|
|
66
|
+
if (policy?.maxFields !== void 0 && schema.fields.length > policy.maxFields) {
|
|
67
|
+
issues.push({
|
|
68
|
+
path: "fields",
|
|
69
|
+
code: "max_fields_exceeded",
|
|
70
|
+
message: `At most ${policy.maxFields} fields are allowed.`
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
schema.fields.forEach((field, index) => {
|
|
74
|
+
if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(field.type)) {
|
|
75
|
+
issues.push({
|
|
76
|
+
path: `fields[${index}].type`,
|
|
77
|
+
code: "disallowed_field_type",
|
|
78
|
+
message: `Field type ${field.type} is not allowed.`
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (policy?.maxTextLength !== void 0) {
|
|
82
|
+
for (const [property, text] of [
|
|
83
|
+
["title", field.title],
|
|
84
|
+
["description", field.description]
|
|
85
|
+
]) {
|
|
86
|
+
if (text !== void 0 && text.length > policy.maxTextLength) {
|
|
87
|
+
issues.push({
|
|
88
|
+
path: `fields[${index}].${property}`,
|
|
89
|
+
code: "max_text_length_exceeded",
|
|
90
|
+
message: `Text must be at most ${policy.maxTextLength} characters.`
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (policy?.maxOptionsPerField !== void 0 && "options" in field && field.options.length > policy.maxOptionsPerField) {
|
|
96
|
+
issues.push({
|
|
97
|
+
path: `fields[${index}].options`,
|
|
98
|
+
code: "max_options_exceeded",
|
|
99
|
+
message: `At most ${policy.maxOptionsPerField} options are allowed.`
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
for (const locale of policy?.requiredLocales ?? []) {
|
|
104
|
+
if (!(schema.supportedLocales ?? []).includes(locale)) {
|
|
105
|
+
issues.push({
|
|
106
|
+
path: "supportedLocales",
|
|
107
|
+
code: "required_locale_missing",
|
|
108
|
+
message: `Required locale ${locale} is missing.`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (locale === schema.defaultLocale) continue;
|
|
112
|
+
const requiredTranslations = [
|
|
113
|
+
{ path: `translations.${locale}.title`, value: schema.translations?.[locale]?.title },
|
|
114
|
+
...schema.fields.map((field, index) => ({
|
|
115
|
+
path: `fields[${index}].translations.${locale}.title`,
|
|
116
|
+
value: field.translations?.[locale]?.title
|
|
117
|
+
})),
|
|
118
|
+
...schema.fields.flatMap(
|
|
119
|
+
(field, fieldIndex) => "options" in field ? field.options.map((option, optionIndex) => ({
|
|
120
|
+
path: `fields[${fieldIndex}].options[${optionIndex}].translations.${locale}`,
|
|
121
|
+
value: option.translations?.[locale]
|
|
122
|
+
})) : []
|
|
123
|
+
),
|
|
124
|
+
...schema.pages?.flatMap(
|
|
125
|
+
(page, pageIndex) => page.title === void 0 ? [] : [{ path: `pages[${pageIndex}].translations.${locale}.title`, value: page.translations?.[locale]?.title }]
|
|
126
|
+
) ?? []
|
|
127
|
+
];
|
|
128
|
+
for (const translation of requiredTranslations) {
|
|
129
|
+
if (translation.value === void 0 || translation.value.trim().length === 0) {
|
|
130
|
+
issues.push({
|
|
131
|
+
path: translation.path,
|
|
132
|
+
code: "required_translation_missing",
|
|
133
|
+
message: `A translation for required locale ${locale} is missing.`
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function useFormBuilder({
|
|
140
|
+
schema,
|
|
141
|
+
onChange,
|
|
142
|
+
policy,
|
|
143
|
+
idFactory = defaultIdFactory
|
|
144
|
+
}) {
|
|
145
|
+
const createId = (0, import_react.useCallback)(
|
|
146
|
+
(kind, existingIds) => {
|
|
147
|
+
const id = idFactory(kind, existingIds).trim();
|
|
148
|
+
return id.length > 0 && !existingIds.has(id) ? id : void 0;
|
|
149
|
+
},
|
|
150
|
+
[idFactory]
|
|
151
|
+
);
|
|
152
|
+
const addField = (0, import_react.useCallback)(
|
|
153
|
+
(type, pageId) => {
|
|
154
|
+
if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(type)) {
|
|
155
|
+
return { success: false, error: { type: "disallowed_field_type", fieldType: type } };
|
|
156
|
+
}
|
|
157
|
+
if (policy?.maxFields !== void 0 && schema.fields.length >= policy.maxFields) {
|
|
158
|
+
return { success: false, error: { type: "max_fields_exceeded", max: policy.maxFields } };
|
|
159
|
+
}
|
|
160
|
+
const fieldId = createId("field", new Set(schema.fields.map((field2) => field2.id)));
|
|
161
|
+
const needsOption = ["select", "radio", "multi-select"].includes(type);
|
|
162
|
+
const optionId = needsOption ? createId(
|
|
163
|
+
"option",
|
|
164
|
+
new Set(
|
|
165
|
+
schema.fields.flatMap((field2) => "options" in field2 ? field2.options.map((option) => option.id) : [])
|
|
166
|
+
)
|
|
167
|
+
) : void 0;
|
|
168
|
+
if (fieldId === void 0 || needsOption && optionId === void 0) {
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
error: { type: "invalid_operation", message: "idFactory returned a duplicate or empty ID." }
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const field = newField(type, fieldId, optionId);
|
|
175
|
+
if (field === void 0) {
|
|
176
|
+
return { success: false, error: { type: "invalid_operation", message: "Could not create the field." } };
|
|
177
|
+
}
|
|
178
|
+
const pages = schema.pages?.map((page, index) => ({
|
|
179
|
+
...page,
|
|
180
|
+
questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, fieldId] : page.questionIds
|
|
181
|
+
}));
|
|
182
|
+
if (schema.pages !== void 0 && !schema.pages.some((page) => page.id === pageId) && pageId !== void 0) {
|
|
183
|
+
return { success: false, error: { type: "invalid_operation", message: `Unknown page: ${pageId}` } };
|
|
184
|
+
}
|
|
185
|
+
onChange({ ...schema, fields: [...schema.fields, field], ...pages === void 0 ? {} : { pages } });
|
|
186
|
+
return { success: true };
|
|
187
|
+
},
|
|
188
|
+
[createId, onChange, policy, schema]
|
|
189
|
+
);
|
|
190
|
+
const removeField = (0, import_react.useCallback)(
|
|
191
|
+
(fieldId) => {
|
|
192
|
+
if (schema.fields.length <= 1 || !schema.fields.some((field) => field.id === fieldId)) return;
|
|
193
|
+
const fields = schema.fields.filter((field) => field.id !== fieldId).map(
|
|
194
|
+
(field) => field.displayCondition?.questionId === fieldId ? (({ displayCondition: _condition, ...candidate }) => candidate)(field) : field
|
|
195
|
+
);
|
|
196
|
+
const remainingPages = schema.pages?.map((page) => ({ ...page, questionIds: page.questionIds.filter((id) => id !== fieldId) })).filter((page) => page.questionIds.length > 0);
|
|
197
|
+
if (schema.pages !== void 0 && remainingPages?.length === 0) {
|
|
198
|
+
const { pages: _pages, ...singlePageSchema } = schema;
|
|
199
|
+
onChange({ ...singlePageSchema, fields });
|
|
200
|
+
} else {
|
|
201
|
+
onChange({ ...schema, fields, ...remainingPages === void 0 ? {} : { pages: remainingPages } });
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
[onChange, schema]
|
|
205
|
+
);
|
|
206
|
+
const moveField = (0, import_react.useCallback)(
|
|
207
|
+
(fieldId, targetIndex) => {
|
|
208
|
+
const fields = move(
|
|
209
|
+
schema.fields,
|
|
210
|
+
schema.fields.findIndex((field) => field.id === fieldId),
|
|
211
|
+
targetIndex
|
|
212
|
+
);
|
|
213
|
+
if (fields !== void 0) {
|
|
214
|
+
const indexById = new Map(fields.map((field, index) => [field.id, index]));
|
|
215
|
+
const safeFields = fields.map((field, index) => {
|
|
216
|
+
const sourceIndex = field.displayCondition === void 0 ? void 0 : indexById.get(field.displayCondition.questionId);
|
|
217
|
+
if (field.displayCondition === void 0 || sourceIndex !== void 0 && sourceIndex < index) return field;
|
|
218
|
+
const { displayCondition: _condition, ...withoutCondition } = field;
|
|
219
|
+
return withoutCondition;
|
|
220
|
+
});
|
|
221
|
+
onChange({ ...schema, fields: safeFields });
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
[onChange, schema]
|
|
225
|
+
);
|
|
226
|
+
const updateField = (0, import_react.useCallback)(
|
|
227
|
+
(fieldId, updater) => {
|
|
228
|
+
const current = schema.fields.find((field) => field.id === fieldId);
|
|
229
|
+
if (current === void 0) return;
|
|
230
|
+
const updated = updater(current);
|
|
231
|
+
if (updated.id !== fieldId) return;
|
|
232
|
+
if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(updated.type)) return;
|
|
233
|
+
const maxTextLength = policy?.maxTextLength;
|
|
234
|
+
if (maxTextLength !== void 0 && [updated.title, updated.description].some((text) => text !== void 0 && text.length > maxTextLength)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
onChange({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? updated : field) });
|
|
238
|
+
},
|
|
239
|
+
[onChange, policy, schema]
|
|
240
|
+
);
|
|
241
|
+
const addOption = (0, import_react.useCallback)(
|
|
242
|
+
(fieldId) => {
|
|
243
|
+
const field = schema.fields.find((candidate) => candidate.id === fieldId);
|
|
244
|
+
if (field === void 0 || !("options" in field)) {
|
|
245
|
+
return { success: false, error: { type: "invalid_operation", message: `Field ${fieldId} has no options.` } };
|
|
246
|
+
}
|
|
247
|
+
if (policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField) {
|
|
248
|
+
return { success: false, error: { type: "max_options_exceeded", max: policy.maxOptionsPerField } };
|
|
249
|
+
}
|
|
250
|
+
const existingIds = new Set(
|
|
251
|
+
schema.fields.flatMap(
|
|
252
|
+
(candidate) => "options" in candidate ? candidate.options.map((option2) => option2.id) : []
|
|
253
|
+
)
|
|
254
|
+
);
|
|
255
|
+
const optionId = createId("option", existingIds);
|
|
256
|
+
if (optionId === void 0) {
|
|
257
|
+
return {
|
|
258
|
+
success: false,
|
|
259
|
+
error: { type: "invalid_operation", message: "idFactory returned a duplicate or empty ID." }
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const option = { id: optionId, label: `Option ${field.options.length + 1}` };
|
|
263
|
+
onChange({
|
|
264
|
+
...schema,
|
|
265
|
+
fields: schema.fields.map(
|
|
266
|
+
(candidate) => candidate.id === fieldId && "options" in candidate ? { ...candidate, options: [...candidate.options, option] } : candidate
|
|
267
|
+
)
|
|
268
|
+
});
|
|
269
|
+
return { success: true };
|
|
270
|
+
},
|
|
271
|
+
[createId, onChange, policy, schema]
|
|
272
|
+
);
|
|
273
|
+
const removeOption = (0, import_react.useCallback)(
|
|
274
|
+
(fieldId, optionId) => {
|
|
275
|
+
const field = schema.fields.find((candidate) => candidate.id === fieldId);
|
|
276
|
+
if (field === void 0 || !("options" in field) || field.options.length <= 1) return;
|
|
277
|
+
onChange({
|
|
278
|
+
...schema,
|
|
279
|
+
fields: schema.fields.map(
|
|
280
|
+
(candidate) => candidate.id === fieldId && "options" in candidate ? { ...candidate, options: candidate.options.filter((option) => option.id !== optionId) } : candidate
|
|
281
|
+
)
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
[onChange, schema]
|
|
285
|
+
);
|
|
286
|
+
const moveOption = (0, import_react.useCallback)(
|
|
287
|
+
(fieldId, optionId, targetIndex) => {
|
|
288
|
+
const field = schema.fields.find((candidate) => candidate.id === fieldId);
|
|
289
|
+
if (field === void 0 || !("options" in field)) return;
|
|
290
|
+
const options = move(
|
|
291
|
+
field.options,
|
|
292
|
+
field.options.findIndex((option) => option.id === optionId),
|
|
293
|
+
targetIndex
|
|
294
|
+
);
|
|
295
|
+
if (options === void 0) return;
|
|
296
|
+
onChange({
|
|
297
|
+
...schema,
|
|
298
|
+
fields: schema.fields.map(
|
|
299
|
+
(candidate) => candidate.id === fieldId && "options" in candidate ? { ...candidate, options } : candidate
|
|
300
|
+
)
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
[onChange, schema]
|
|
304
|
+
);
|
|
305
|
+
const addPage = (0, import_react.useCallback)(
|
|
306
|
+
(questionId) => {
|
|
307
|
+
const existingIds = new Set(schema.pages?.map((page) => page.id) ?? []);
|
|
308
|
+
const pageId = createId("page", existingIds);
|
|
309
|
+
if (pageId === void 0) return;
|
|
310
|
+
if (schema.pages === void 0) {
|
|
311
|
+
onChange({ ...schema, pages: [{ id: pageId, questionIds: schema.fields.map((field) => field.id) }] });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const movableQuestionId = questionId ?? schema.pages.find((page) => page.questionIds.length > 1)?.questionIds.at(-1);
|
|
315
|
+
if (movableQuestionId === void 0) return;
|
|
316
|
+
const sourcePage = schema.pages.find((page) => page.questionIds.includes(movableQuestionId));
|
|
317
|
+
if (sourcePage === void 0 || sourcePage.questionIds.length <= 1) return;
|
|
318
|
+
const pages = [
|
|
319
|
+
...schema.pages.map((page) => ({
|
|
320
|
+
...page,
|
|
321
|
+
questionIds: page.questionIds.filter((id) => id !== movableQuestionId)
|
|
322
|
+
})),
|
|
323
|
+
{ id: pageId, questionIds: [movableQuestionId] }
|
|
324
|
+
];
|
|
325
|
+
onChange({ ...schema, pages });
|
|
326
|
+
},
|
|
327
|
+
[createId, onChange, schema]
|
|
328
|
+
);
|
|
329
|
+
const removePage = (0, import_react.useCallback)(
|
|
330
|
+
(pageId) => {
|
|
331
|
+
if (schema.pages === void 0) return;
|
|
332
|
+
const index = schema.pages.findIndex((page) => page.id === pageId);
|
|
333
|
+
const removed = schema.pages[index];
|
|
334
|
+
if (removed === void 0) return;
|
|
335
|
+
if (schema.pages.length === 1) {
|
|
336
|
+
const { pages: _pages, ...singlePage } = schema;
|
|
337
|
+
onChange(singlePage);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const targetIndex = index === 0 ? 1 : index - 1;
|
|
341
|
+
onChange({
|
|
342
|
+
...schema,
|
|
343
|
+
pages: schema.pages.map(
|
|
344
|
+
(page, pageIndex) => pageIndex === targetIndex ? { ...page, questionIds: [...page.questionIds, ...removed.questionIds] } : page
|
|
345
|
+
).filter((page) => page.id !== pageId)
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
[onChange, schema]
|
|
349
|
+
);
|
|
350
|
+
const setLocaleTranslation = (0, import_react.useCallback)(
|
|
351
|
+
(locale, target, property, text) => {
|
|
352
|
+
if (locale.trim().length === 0 || policy?.maxTextLength !== void 0 && text.length > policy.maxTextLength)
|
|
353
|
+
return;
|
|
354
|
+
const supportedLocales = [.../* @__PURE__ */ new Set([...schema.supportedLocales ?? [], locale])];
|
|
355
|
+
if (target === "form" && ["title", "description", "completionMessage"].includes(property)) {
|
|
356
|
+
onChange({
|
|
357
|
+
...schema,
|
|
358
|
+
supportedLocales,
|
|
359
|
+
translations: { ...schema.translations, [locale]: { ...schema.translations?.[locale], [property]: text } }
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const fields = schema.fields.map((field) => {
|
|
364
|
+
if (field.id === target && ["title", "description"].includes(property)) {
|
|
365
|
+
return {
|
|
366
|
+
...field,
|
|
367
|
+
translations: { ...field.translations, [locale]: { ...field.translations?.[locale], [property]: text } }
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (!("options" in field) || property !== "label") return field;
|
|
371
|
+
return {
|
|
372
|
+
...field,
|
|
373
|
+
options: field.options.map(
|
|
374
|
+
(option) => option.id === target ? { ...option, translations: { ...option.translations, [locale]: text } } : option
|
|
375
|
+
)
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
const pages = schema.pages?.map(
|
|
379
|
+
(page) => page.id === target && ["title", "description"].includes(property) ? {
|
|
380
|
+
...page,
|
|
381
|
+
translations: { ...page.translations, [locale]: { ...page.translations?.[locale], [property]: text } }
|
|
382
|
+
} : page
|
|
383
|
+
);
|
|
384
|
+
onChange({ ...schema, supportedLocales, fields, ...pages === void 0 ? {} : { pages } });
|
|
385
|
+
},
|
|
386
|
+
[onChange, policy, schema]
|
|
387
|
+
);
|
|
388
|
+
const validationIssues = (0, import_react.useMemo)(() => {
|
|
389
|
+
const result = (0, import_core.validateFormSchema)(schema);
|
|
390
|
+
const issues = result.valid ? [] : [...result.issues];
|
|
391
|
+
addPolicyIssues(schema, policy, issues);
|
|
392
|
+
return issues;
|
|
393
|
+
}, [policy, schema]);
|
|
394
|
+
return {
|
|
395
|
+
schema,
|
|
396
|
+
addField,
|
|
397
|
+
removeField,
|
|
398
|
+
moveField,
|
|
399
|
+
updateField,
|
|
400
|
+
addOption,
|
|
401
|
+
removeOption,
|
|
402
|
+
moveOption,
|
|
403
|
+
addPage,
|
|
404
|
+
removePage,
|
|
405
|
+
setLocaleTranslation,
|
|
406
|
+
validationIssues
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/builder.tsx
|
|
34
411
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
35
412
|
var FIELD_TYPES = [
|
|
36
413
|
"text",
|
|
@@ -154,7 +531,7 @@ function withoutDisplayCondition(field) {
|
|
|
154
531
|
return rest;
|
|
155
532
|
}
|
|
156
533
|
function sanitizeBuilderSchema(schema) {
|
|
157
|
-
const sanitized = (0,
|
|
534
|
+
const sanitized = (0, import_core2.sanitizeSchema)(schema);
|
|
158
535
|
const indexById = new Map(sanitized.fields.map((field, index) => [field.id, index]));
|
|
159
536
|
const fields = sanitized.fields.map((field, index) => {
|
|
160
537
|
const sourceId = field.displayCondition?.questionId;
|
|
@@ -214,20 +591,32 @@ function ConditionValueEditor({
|
|
|
214
591
|
}
|
|
215
592
|
);
|
|
216
593
|
}
|
|
217
|
-
function FormBuilder({
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
594
|
+
function FormBuilder({
|
|
595
|
+
schema,
|
|
596
|
+
onChange,
|
|
597
|
+
locale = "en",
|
|
598
|
+
translator,
|
|
599
|
+
translationAdapter,
|
|
600
|
+
policy,
|
|
601
|
+
idFactory
|
|
602
|
+
}) {
|
|
603
|
+
const headless = useFormBuilder({
|
|
604
|
+
schema,
|
|
605
|
+
onChange,
|
|
606
|
+
...policy === void 0 ? {} : { policy },
|
|
607
|
+
...idFactory === void 0 ? {} : { idFactory }
|
|
608
|
+
});
|
|
609
|
+
const [newPageQuestionId, setNewPageQuestionId] = (0, import_react2.useState)("");
|
|
610
|
+
const [newLocale, setNewLocale] = (0, import_react2.useState)("");
|
|
611
|
+
const [editingLocale, setEditingLocale] = (0, import_react2.useState)("");
|
|
612
|
+
const [isTranslating, setIsTranslating] = (0, import_react2.useState)(false);
|
|
613
|
+
const [translationError, setTranslationError] = (0, import_react2.useState)(null);
|
|
223
614
|
const translate = (key, params = {}) => {
|
|
224
615
|
const translated = translator?.translate(key, locale, params);
|
|
225
616
|
return translated === void 0 ? interpolate(BUILDER_DEFAULTS[key] ?? key, params) : translated;
|
|
226
617
|
};
|
|
227
618
|
const emitSchema = (candidate) => onChange(sanitizeBuilderSchema(candidate));
|
|
228
|
-
const updateField =
|
|
229
|
-
emitSchema({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? update(field) : field) });
|
|
230
|
-
};
|
|
619
|
+
const updateField = headless.updateField;
|
|
231
620
|
const changeType = (fieldId, type) => {
|
|
232
621
|
emitSchema({
|
|
233
622
|
...schema,
|
|
@@ -243,10 +632,7 @@ function FormBuilder({ schema, onChange, locale = "en", translator, translationA
|
|
|
243
632
|
})
|
|
244
633
|
});
|
|
245
634
|
};
|
|
246
|
-
const removeField =
|
|
247
|
-
if (schema.fields.length === 1) return;
|
|
248
|
-
emitSchema({ ...schema, fields: schema.fields.filter((field) => field.id !== fieldId) });
|
|
249
|
-
};
|
|
635
|
+
const removeField = headless.removeField;
|
|
250
636
|
const moveField = (index, offset) => {
|
|
251
637
|
const target = index + offset;
|
|
252
638
|
if (target < 0 || target >= schema.fields.length) return;
|
|
@@ -258,21 +644,7 @@ function FormBuilder({ schema, onChange, locale = "en", translator, translationA
|
|
|
258
644
|
fields[target] = current;
|
|
259
645
|
emitSchema({ ...schema, fields });
|
|
260
646
|
};
|
|
261
|
-
const addField = () =>
|
|
262
|
-
const id = createUniqueId("q", new Set(schema.fields.map((field) => field.id)));
|
|
263
|
-
const nextSchema = {
|
|
264
|
-
...schema,
|
|
265
|
-
fields: [...schema.fields, { id, type: "text", title: translate("builder.newQuestionTitle"), required: false }]
|
|
266
|
-
};
|
|
267
|
-
emitSchema(
|
|
268
|
-
schema.pages === void 0 ? nextSchema : {
|
|
269
|
-
...nextSchema,
|
|
270
|
-
pages: schema.pages.map(
|
|
271
|
-
(page, index) => index === (schema.pages?.length ?? 0) - 1 ? { ...page, questionIds: [...page.questionIds, id] } : page
|
|
272
|
-
)
|
|
273
|
-
}
|
|
274
|
-
);
|
|
275
|
-
};
|
|
647
|
+
const addField = () => headless.addField("text");
|
|
276
648
|
const enablePages = () => {
|
|
277
649
|
if (schema.pages !== void 0) return;
|
|
278
650
|
emitSchema({
|
|
@@ -370,7 +742,10 @@ function FormBuilder({ schema, onChange, locale = "en", translator, translationA
|
|
|
370
742
|
setIsTranslating(true);
|
|
371
743
|
setTranslationError(null);
|
|
372
744
|
try {
|
|
373
|
-
|
|
745
|
+
const populated = await (0, import_core2.populateSchemaTranslations)(schema, [editingLocale], translationAdapter, {
|
|
746
|
+
overwrite: "all"
|
|
747
|
+
});
|
|
748
|
+
onChange(populated.schema);
|
|
374
749
|
} catch (cause) {
|
|
375
750
|
setTranslationError(cause instanceof Error ? cause.message : String(cause));
|
|
376
751
|
} finally {
|
|
@@ -719,7 +1094,9 @@ function FormBuilder({ schema, onChange, locale = "en", translator, translationA
|
|
|
719
1094
|
{
|
|
720
1095
|
value: field.type,
|
|
721
1096
|
onChange: (event) => changeType(field.id, event.currentTarget.value),
|
|
722
|
-
children: FIELD_TYPES.
|
|
1097
|
+
children: FIELD_TYPES.filter(
|
|
1098
|
+
(type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
|
|
1099
|
+
).map((type) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: type, children: translate(fieldTypeKey(type)) }, type))
|
|
723
1100
|
}
|
|
724
1101
|
)
|
|
725
1102
|
] }),
|
|
@@ -892,13 +1269,7 @@ function FormBuilder({ schema, onChange, locale = "en", translator, translationA
|
|
|
892
1269
|
{
|
|
893
1270
|
type: "button",
|
|
894
1271
|
disabled: field.options.length === 1,
|
|
895
|
-
onClick: () =>
|
|
896
|
-
field.id,
|
|
897
|
-
(current) => "options" in current ? {
|
|
898
|
-
...current,
|
|
899
|
-
options: current.options.filter((_item, itemIndex) => itemIndex !== optionIndex)
|
|
900
|
-
} : current
|
|
901
|
-
),
|
|
1272
|
+
onClick: () => headless.removeOption(field.id, option.id),
|
|
902
1273
|
children: translate("builder.remove")
|
|
903
1274
|
}
|
|
904
1275
|
)
|
|
@@ -907,22 +1278,8 @@ function FormBuilder({ schema, onChange, locale = "en", translator, translationA
|
|
|
907
1278
|
"button",
|
|
908
1279
|
{
|
|
909
1280
|
type: "button",
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
(current) => "options" in current ? (() => {
|
|
913
|
-
const id = createUniqueId("opt", new Set(current.options.map((option) => option.id)));
|
|
914
|
-
return {
|
|
915
|
-
...current,
|
|
916
|
-
options: [
|
|
917
|
-
...current.options,
|
|
918
|
-
{
|
|
919
|
-
id,
|
|
920
|
-
label: translate("builder.newOptionLabel", { index: current.options.length + 1 })
|
|
921
|
-
}
|
|
922
|
-
]
|
|
923
|
-
};
|
|
924
|
-
})() : current
|
|
925
|
-
),
|
|
1281
|
+
disabled: policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
|
|
1282
|
+
onClick: () => headless.addOption(field.id),
|
|
926
1283
|
children: translate("builder.addOption")
|
|
927
1284
|
}
|
|
928
1285
|
)
|
|
@@ -984,15 +1341,24 @@ function FormBuilder({ schema, onChange, locale = "en", translator, translationA
|
|
|
984
1341
|
] })
|
|
985
1342
|
] }, field.id);
|
|
986
1343
|
}) }),
|
|
987
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1344
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1345
|
+
"button",
|
|
1346
|
+
{
|
|
1347
|
+
className: "form-engine-builder__add",
|
|
1348
|
+
type: "button",
|
|
1349
|
+
disabled: policy?.maxFields !== void 0 && schema.fields.length >= policy.maxFields,
|
|
1350
|
+
onClick: addField,
|
|
1351
|
+
children: translate("builder.addQuestion")
|
|
1352
|
+
}
|
|
1353
|
+
)
|
|
988
1354
|
] });
|
|
989
1355
|
}
|
|
990
1356
|
|
|
991
1357
|
// src/context.tsx
|
|
992
|
-
var
|
|
993
|
-
var
|
|
1358
|
+
var import_core3 = require("@form-engine-ts/core");
|
|
1359
|
+
var import_react3 = require("react");
|
|
994
1360
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
995
|
-
var FormContext = (0,
|
|
1361
|
+
var FormContext = (0, import_react3.createContext)(null);
|
|
996
1362
|
function issuesByField(issues) {
|
|
997
1363
|
const result = {};
|
|
998
1364
|
for (const issue of issues) result[issue.fieldId] ??= issue;
|
|
@@ -1007,30 +1373,30 @@ function FormProvider({
|
|
|
1007
1373
|
onSubmit,
|
|
1008
1374
|
children
|
|
1009
1375
|
}) {
|
|
1010
|
-
const validSchema = (0,
|
|
1011
|
-
(0,
|
|
1012
|
-
const localized = (0,
|
|
1013
|
-
(0,
|
|
1376
|
+
const validSchema = (0, import_react3.useMemo)(() => {
|
|
1377
|
+
(0, import_core3.assertValidFormSchema)(schema);
|
|
1378
|
+
const localized = (0, import_core3.resolveLocalizedSchema)(schema, locale);
|
|
1379
|
+
(0, import_core3.assertValidFormSchema)(localized);
|
|
1014
1380
|
return localized;
|
|
1015
1381
|
}, [locale, schema]);
|
|
1016
|
-
const [values, setValues] = (0,
|
|
1017
|
-
const [errors, setErrors] = (0,
|
|
1018
|
-
const [submitStatus, setSubmitStatus] = (0,
|
|
1019
|
-
const [submitError, setSubmitError] = (0,
|
|
1020
|
-
const [validationPageIndex, setValidationPageIndex] = (0,
|
|
1021
|
-
const visibility = (0,
|
|
1022
|
-
const pageVisibility = (0,
|
|
1023
|
-
(0,
|
|
1382
|
+
const [values, setValues] = (0, import_react3.useState)(() => ({ ...initialValues }));
|
|
1383
|
+
const [errors, setErrors] = (0, import_react3.useState)({});
|
|
1384
|
+
const [submitStatus, setSubmitStatus] = (0, import_react3.useState)("idle");
|
|
1385
|
+
const [submitError, setSubmitError] = (0, import_react3.useState)(null);
|
|
1386
|
+
const [validationPageIndex, setValidationPageIndex] = (0, import_react3.useState)(null);
|
|
1387
|
+
const visibility = (0, import_react3.useMemo)(() => (0, import_core3.calculateFieldVisibility)(validSchema, values), [validSchema, values]);
|
|
1388
|
+
const pageVisibility = (0, import_react3.useMemo)(() => (0, import_core3.calculatePageVisibility)(validSchema, values), [validSchema, values]);
|
|
1389
|
+
(0, import_react3.useEffect)(() => {
|
|
1024
1390
|
const fieldIds = new Set(validSchema.fields.map((field) => field.id));
|
|
1025
1391
|
setValues((current) => Object.fromEntries(Object.entries(current).filter(([fieldId]) => fieldIds.has(fieldId))));
|
|
1026
1392
|
}, [validSchema]);
|
|
1027
|
-
const setValue = (0,
|
|
1393
|
+
const setValue = (0, import_react3.useCallback)(
|
|
1028
1394
|
(fieldId, value) => {
|
|
1029
1395
|
setValues((current) => {
|
|
1030
1396
|
const next = { ...current, [fieldId]: value };
|
|
1031
1397
|
setErrors((currentErrors) => {
|
|
1032
1398
|
if (Object.keys(currentErrors).length === 0) return currentErrors;
|
|
1033
|
-
const result = validationPageIndex === null ? (0,
|
|
1399
|
+
const result = validationPageIndex === null ? (0, import_core3.validateAnswers)(validSchema, next) : (0, import_core3.validatePageAnswers)(validSchema, validationPageIndex, next);
|
|
1034
1400
|
return issuesByField(result.issues);
|
|
1035
1401
|
});
|
|
1036
1402
|
return next;
|
|
@@ -1040,7 +1406,7 @@ function FormProvider({
|
|
|
1040
1406
|
},
|
|
1041
1407
|
[validSchema, validationPageIndex]
|
|
1042
1408
|
);
|
|
1043
|
-
const restoreValues = (0,
|
|
1409
|
+
const restoreValues = (0, import_react3.useCallback)(
|
|
1044
1410
|
(restoredValues) => {
|
|
1045
1411
|
const fieldIds = new Set(validSchema.fields.map((field) => field.id));
|
|
1046
1412
|
setValues(Object.fromEntries(Object.entries(restoredValues).filter(([fieldId]) => fieldIds.has(fieldId))));
|
|
@@ -1051,9 +1417,9 @@ function FormProvider({
|
|
|
1051
1417
|
},
|
|
1052
1418
|
[validSchema]
|
|
1053
1419
|
);
|
|
1054
|
-
const validatePage = (0,
|
|
1420
|
+
const validatePage = (0, import_react3.useCallback)(
|
|
1055
1421
|
(pageIndex) => {
|
|
1056
|
-
const result = (0,
|
|
1422
|
+
const result = (0, import_core3.validatePageAnswers)(validSchema, pageIndex, values);
|
|
1057
1423
|
setErrors(issuesByField(result.issues));
|
|
1058
1424
|
setValidationPageIndex(result.valid ? null : pageIndex);
|
|
1059
1425
|
setSubmitStatus("idle");
|
|
@@ -1062,42 +1428,51 @@ function FormProvider({
|
|
|
1062
1428
|
},
|
|
1063
1429
|
[validSchema, values]
|
|
1064
1430
|
);
|
|
1065
|
-
const reset = (0,
|
|
1431
|
+
const reset = (0, import_react3.useCallback)(() => {
|
|
1066
1432
|
setValues({ ...initialValues });
|
|
1067
1433
|
setErrors({});
|
|
1068
1434
|
setValidationPageIndex(null);
|
|
1069
1435
|
setSubmitStatus("idle");
|
|
1070
1436
|
setSubmitError(null);
|
|
1071
1437
|
}, [initialValues]);
|
|
1072
|
-
const submit = (0,
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1438
|
+
const submit = (0, import_react3.useCallback)(
|
|
1439
|
+
async (beforeSubmit) => {
|
|
1440
|
+
const validation = (0, import_core3.validateAnswers)(validSchema, values);
|
|
1441
|
+
if (!validation.valid) {
|
|
1442
|
+
setErrors(issuesByField(validation.issues));
|
|
1443
|
+
setValidationPageIndex(null);
|
|
1444
|
+
setSubmitStatus("error");
|
|
1445
|
+
setSubmitError(null);
|
|
1446
|
+
return { status: "invalid", issues: validation.issues };
|
|
1447
|
+
}
|
|
1448
|
+
setErrors({});
|
|
1076
1449
|
setValidationPageIndex(null);
|
|
1077
|
-
setSubmitStatus("error");
|
|
1078
1450
|
setSubmitError(null);
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1451
|
+
const visibleValues = (0, import_core3.selectVisibleAnswers)(validSchema, values);
|
|
1452
|
+
try {
|
|
1453
|
+
if (beforeSubmit !== void 0 && await beforeSubmit(visibleValues) === "cancel") {
|
|
1454
|
+
setSubmitStatus("idle");
|
|
1455
|
+
return { status: "cancelled" };
|
|
1456
|
+
}
|
|
1457
|
+
setSubmitStatus("submitting");
|
|
1458
|
+
await onSubmit(visibleValues);
|
|
1459
|
+
if (resetOnSuccess) setValues({ ...initialValues });
|
|
1460
|
+
setSubmitStatus("success");
|
|
1461
|
+
return { status: "success" };
|
|
1462
|
+
} catch (cause) {
|
|
1463
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1464
|
+
setSubmitError(error);
|
|
1465
|
+
setSubmitStatus("error");
|
|
1466
|
+
return { status: "error", error };
|
|
1467
|
+
}
|
|
1468
|
+
},
|
|
1469
|
+
[initialValues, onSubmit, resetOnSuccess, validSchema, values]
|
|
1470
|
+
);
|
|
1471
|
+
const translate = (0, import_react3.useCallback)(
|
|
1097
1472
|
(key, params) => translator.translate(key, locale, params),
|
|
1098
1473
|
[locale, translator]
|
|
1099
1474
|
);
|
|
1100
|
-
const contextValue = (0,
|
|
1475
|
+
const contextValue = (0, import_react3.useMemo)(
|
|
1101
1476
|
() => ({
|
|
1102
1477
|
schema: validSchema,
|
|
1103
1478
|
locale,
|
|
@@ -1137,7 +1512,7 @@ function FormProvider({
|
|
|
1137
1512
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FormContext.Provider, { value: contextValue, children });
|
|
1138
1513
|
}
|
|
1139
1514
|
function useForm() {
|
|
1140
|
-
const context = (0,
|
|
1515
|
+
const context = (0, import_react3.useContext)(FormContext);
|
|
1141
1516
|
if (context === null) throw new Error("useForm must be called inside a FormProvider.");
|
|
1142
1517
|
return context;
|
|
1143
1518
|
}
|
|
@@ -1145,13 +1520,13 @@ function useField(fieldId) {
|
|
|
1145
1520
|
const form = useForm();
|
|
1146
1521
|
const field = form.schema.fields.find((item) => item.id === fieldId);
|
|
1147
1522
|
if (field === void 0) throw new Error(`Unknown form field: ${fieldId}`);
|
|
1148
|
-
const setValue = (0,
|
|
1523
|
+
const setValue = (0, import_react3.useCallback)((value) => form.setValue(fieldId, value), [fieldId, form]);
|
|
1149
1524
|
return { field, value: form.values[fieldId], error: form.errors[fieldId], setValue };
|
|
1150
1525
|
}
|
|
1151
1526
|
|
|
1152
1527
|
// src/renderer.tsx
|
|
1153
|
-
var
|
|
1154
|
-
var
|
|
1528
|
+
var import_core4 = require("@form-engine-ts/core");
|
|
1529
|
+
var import_react4 = require("react");
|
|
1155
1530
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1156
1531
|
function describedBy(field, error, helpId, errorId) {
|
|
1157
1532
|
const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
|
|
@@ -1354,36 +1729,39 @@ function parseDraft(serialized) {
|
|
|
1354
1729
|
return null;
|
|
1355
1730
|
}
|
|
1356
1731
|
}
|
|
1357
|
-
function
|
|
1732
|
+
function ContextFormRenderer({
|
|
1358
1733
|
components = {},
|
|
1359
1734
|
className = "",
|
|
1360
1735
|
successMessageKey,
|
|
1361
1736
|
errorMessageKey,
|
|
1362
|
-
autoSaveKey
|
|
1737
|
+
autoSaveKey,
|
|
1738
|
+
beforeSubmit,
|
|
1739
|
+
onDraftSave,
|
|
1740
|
+
slots = {}
|
|
1363
1741
|
}) {
|
|
1364
1742
|
const form = useForm();
|
|
1365
|
-
const prefix = (0,
|
|
1366
|
-
const formRef = (0,
|
|
1367
|
-
const loadedDraftKey = (0,
|
|
1368
|
-
const [draftRestored, setDraftRestored] = (0,
|
|
1369
|
-
const [currentPageIndex, setCurrentPageIndex] = (0,
|
|
1370
|
-
const [focusFieldId, setFocusFieldId] = (0,
|
|
1743
|
+
const prefix = (0, import_react4.useId)().replace(/:/g, "");
|
|
1744
|
+
const formRef = (0, import_react4.useRef)(null);
|
|
1745
|
+
const loadedDraftKey = (0, import_react4.useRef)(null);
|
|
1746
|
+
const [draftRestored, setDraftRestored] = (0, import_react4.useState)(false);
|
|
1747
|
+
const [currentPageIndex, setCurrentPageIndex] = (0, import_react4.useState)(0);
|
|
1748
|
+
const [focusFieldId, setFocusFieldId] = (0, import_react4.useState)(null);
|
|
1371
1749
|
const pages = form.schema.pages;
|
|
1372
|
-
const visiblePageIndexes = (0,
|
|
1750
|
+
const visiblePageIndexes = (0, import_react4.useMemo)(
|
|
1373
1751
|
() => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
|
|
1374
1752
|
[form.pageVisibility, pages]
|
|
1375
1753
|
);
|
|
1376
1754
|
const activePage = pages?.[currentPageIndex];
|
|
1377
1755
|
const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
|
|
1378
1756
|
const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
|
|
1379
|
-
(0,
|
|
1757
|
+
(0, import_react4.useEffect)(() => {
|
|
1380
1758
|
if (pages === void 0 || visiblePageIndexes.length === 0) {
|
|
1381
1759
|
setCurrentPageIndex(0);
|
|
1382
1760
|
return;
|
|
1383
1761
|
}
|
|
1384
1762
|
if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
|
|
1385
1763
|
}, [currentPageIndex, pages, visiblePageIndexes]);
|
|
1386
|
-
(0,
|
|
1764
|
+
(0, import_react4.useEffect)(() => {
|
|
1387
1765
|
if (focusFieldId === null) return;
|
|
1388
1766
|
const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
|
|
1389
1767
|
(element) => element.dataset.fieldId === focusFieldId
|
|
@@ -1394,7 +1772,7 @@ function FormRenderer({
|
|
|
1394
1772
|
setFocusFieldId(null);
|
|
1395
1773
|
}
|
|
1396
1774
|
}, [focusFieldId]);
|
|
1397
|
-
(0,
|
|
1775
|
+
(0, import_react4.useEffect)(() => {
|
|
1398
1776
|
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
1399
1777
|
const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
|
|
1400
1778
|
if (loadedDraftKey.current === loadIdentity) return;
|
|
@@ -1406,10 +1784,11 @@ function FormRenderer({
|
|
|
1406
1784
|
form.restoreValues(draft.values);
|
|
1407
1785
|
setDraftRestored(true);
|
|
1408
1786
|
}, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
|
|
1409
|
-
(0,
|
|
1410
|
-
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
1787
|
+
(0, import_react4.useEffect)(() => {
|
|
1411
1788
|
if (form.submitStatus === "success") return;
|
|
1412
1789
|
const timeout = globalThis.setTimeout(() => {
|
|
1790
|
+
onDraftSave?.(form.values);
|
|
1791
|
+
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
1413
1792
|
const draft = {
|
|
1414
1793
|
formId: form.schema.id,
|
|
1415
1794
|
formVersion: form.schema.version,
|
|
@@ -1419,7 +1798,7 @@ function FormRenderer({
|
|
|
1419
1798
|
globalThis.localStorage.setItem(autoSaveKey, JSON.stringify(draft));
|
|
1420
1799
|
}, 500);
|
|
1421
1800
|
return () => globalThis.clearTimeout(timeout);
|
|
1422
|
-
}, [autoSaveKey, form.schema.id, form.schema.version, form.submitStatus, form.values]);
|
|
1801
|
+
}, [autoSaveKey, form.schema.id, form.schema.version, form.submitStatus, form.values, onDraftSave]);
|
|
1423
1802
|
const focusFirstIssue = (fieldId) => {
|
|
1424
1803
|
if (fieldId !== void 0) setFocusFieldId(fieldId);
|
|
1425
1804
|
};
|
|
@@ -1432,27 +1811,39 @@ function FormRenderer({
|
|
|
1432
1811
|
const nextPageIndex = visiblePageIndexes[activeVisibleIndex + 1];
|
|
1433
1812
|
if (nextPageIndex !== void 0) setCurrentPageIndex(nextPageIndex);
|
|
1434
1813
|
};
|
|
1435
|
-
const
|
|
1436
|
-
|
|
1437
|
-
const validation = (0, import_core3.validateAnswers)(form.schema, form.values);
|
|
1814
|
+
const submitValues = async () => {
|
|
1815
|
+
const validation = (0, import_core4.validateAnswers)(form.schema, form.values);
|
|
1438
1816
|
const firstInvalidFieldId = validation.issues[0]?.fieldId;
|
|
1439
|
-
const
|
|
1440
|
-
if (
|
|
1817
|
+
const result = await form.submit(beforeSubmit);
|
|
1818
|
+
if (result.status === "invalid") {
|
|
1441
1819
|
const invalidPageIndex = pages?.findIndex(
|
|
1442
1820
|
(page) => firstInvalidFieldId === void 0 ? false : page.questionIds.includes(firstInvalidFieldId)
|
|
1443
1821
|
);
|
|
1444
1822
|
if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
|
|
1445
1823
|
focusFirstIssue(firstInvalidFieldId);
|
|
1446
|
-
return;
|
|
1824
|
+
return result;
|
|
1447
1825
|
}
|
|
1826
|
+
if (result.status !== "success") return result;
|
|
1448
1827
|
if (autoSaveKey !== void 0 && typeof globalThis.localStorage !== "undefined") {
|
|
1449
1828
|
globalThis.localStorage.removeItem(autoSaveKey);
|
|
1450
1829
|
setDraftRestored(false);
|
|
1451
1830
|
}
|
|
1452
1831
|
setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
|
|
1832
|
+
return result;
|
|
1453
1833
|
};
|
|
1834
|
+
const handleSubmit = (event) => {
|
|
1835
|
+
event.preventDefault();
|
|
1836
|
+
void submitValues();
|
|
1837
|
+
};
|
|
1838
|
+
const validationIssues = Object.values(form.errors).filter((issue) => issue !== void 0);
|
|
1839
|
+
const canPrev = pages !== void 0 && activeVisibleIndex > 0;
|
|
1840
|
+
const canNext = pages !== void 0 && activeVisibleIndex < visiblePageIndexes.length - 1;
|
|
1841
|
+
const renderSubmitButton = () => slots.renderSubmitButton?.({ isSubmitting: form.isSubmitting, onSubmit: () => void submitValues() }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "fe-submit", type: "submit", disabled: form.isSubmitting, children: form.translate(form.schema.submitLabelKey ?? "form.submit") });
|
|
1454
1842
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { ref: formRef, className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
|
|
1455
|
-
|
|
1843
|
+
slots.renderHeader?.({
|
|
1844
|
+
title: form.schema.title,
|
|
1845
|
+
...form.schema.description === void 0 ? {} : { description: form.schema.description }
|
|
1846
|
+
}) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { className: "fe-header", children: [
|
|
1456
1847
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { children: form.schema.title }),
|
|
1457
1848
|
form.schema.description === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: form.schema.description }),
|
|
1458
1849
|
pages === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-progress", children: [
|
|
@@ -1480,42 +1871,122 @@ function FormRenderer({
|
|
|
1480
1871
|
activePage?.title === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "fe-page-title", children: activePage.title }),
|
|
1481
1872
|
activePage?.description === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "fe-page-description", children: activePage.description }),
|
|
1482
1873
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-fields", children: form.schema.fields.filter((field) => form.visibility[field.id] === true && (fieldIds === void 0 || fieldIds.has(field.id))).map((field) => {
|
|
1874
|
+
const error = form.errors[field.id];
|
|
1483
1875
|
const props = {
|
|
1484
1876
|
field,
|
|
1485
1877
|
value: form.values[field.id],
|
|
1486
|
-
error
|
|
1878
|
+
error,
|
|
1487
1879
|
setValue: (value) => form.setValue(field.id, value),
|
|
1488
1880
|
translate: form.translate,
|
|
1489
1881
|
inputId: `${prefix}-${field.id}`,
|
|
1490
1882
|
errorId: `${prefix}-${field.id}-error`,
|
|
1491
1883
|
helpId: `${prefix}-${field.id}-help`
|
|
1492
1884
|
};
|
|
1885
|
+
if (slots.renderField !== void 0) {
|
|
1886
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react4.Fragment, { children: slots.renderField({
|
|
1887
|
+
question: field,
|
|
1888
|
+
value: form.values[field.id],
|
|
1889
|
+
onChange: (value) => {
|
|
1890
|
+
if (isFormValue(value)) form.setValue(field.id, value);
|
|
1891
|
+
},
|
|
1892
|
+
...error === void 0 ? {} : { error }
|
|
1893
|
+
}) }, field.id);
|
|
1894
|
+
}
|
|
1493
1895
|
const Component = components[field.type];
|
|
1494
1896
|
return Component === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DefaultField, { ...props }, field.id) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { ...props }, field.id);
|
|
1495
1897
|
}) }),
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1898
|
+
validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-validation-summary", role: "alert", children: [
|
|
1899
|
+
validationIssues.length,
|
|
1900
|
+
" validation error",
|
|
1901
|
+
validationIssues.length === 1 ? "" : "s",
|
|
1902
|
+
"."
|
|
1903
|
+
] }),
|
|
1904
|
+
pages === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
1905
|
+
slots.renderNavigation?.({
|
|
1906
|
+
currentPage: 0,
|
|
1907
|
+
totalPages: 1,
|
|
1908
|
+
canPrev: false,
|
|
1909
|
+
canNext: false,
|
|
1910
|
+
onPrev: () => void 0,
|
|
1911
|
+
onNext: () => void 0
|
|
1912
|
+
}),
|
|
1913
|
+
renderSubmitButton()
|
|
1914
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "form-step-navigation", children: [
|
|
1915
|
+
slots.renderNavigation?.({
|
|
1916
|
+
currentPage: activeVisibleIndex,
|
|
1917
|
+
totalPages: visiblePageIndexes.length,
|
|
1918
|
+
canPrev,
|
|
1919
|
+
canNext,
|
|
1920
|
+
onPrev: () => setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0),
|
|
1921
|
+
onNext: handleNext
|
|
1922
|
+
}) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
1923
|
+
canPrev ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1924
|
+
"button",
|
|
1925
|
+
{
|
|
1926
|
+
className: "btn-prev",
|
|
1927
|
+
type: "button",
|
|
1928
|
+
onClick: () => setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0),
|
|
1929
|
+
children: form.translate("form.back")
|
|
1930
|
+
}
|
|
1931
|
+
) : null,
|
|
1932
|
+
canNext ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "btn-next", type: "button", onClick: handleNext, children: form.translate("form.next") }) : null
|
|
1933
|
+
] }),
|
|
1934
|
+
canNext ? null : renderSubmitButton()
|
|
1507
1935
|
] }),
|
|
1508
1936
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-status", "aria-live": "polite", children: [
|
|
1509
|
-
form.submitStatus === "success"
|
|
1937
|
+
form.submitStatus === "success" ? slots.renderCompletion?.({
|
|
1938
|
+
message: form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey))
|
|
1939
|
+
}) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "status", children: form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey)) }) : null,
|
|
1510
1940
|
form.submitStatus === "error" && form.submitError !== null && errorMessageKey !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", children: form.translate(errorMessageKey) }) : null
|
|
1511
1941
|
] })
|
|
1512
1942
|
] });
|
|
1513
1943
|
}
|
|
1944
|
+
var RENDERER_MESSAGES = {
|
|
1945
|
+
"form.submit": "Submit",
|
|
1946
|
+
"form.back": "Back",
|
|
1947
|
+
"form.next": "Next",
|
|
1948
|
+
"form.step": "Step {{current}} / {{total}}",
|
|
1949
|
+
"form.draftRestored": "Draft restored",
|
|
1950
|
+
"validation.required": "This field is required."
|
|
1951
|
+
};
|
|
1952
|
+
var defaultRendererTranslator = {
|
|
1953
|
+
translate(key, _locale, params = {}) {
|
|
1954
|
+
return (RENDERER_MESSAGES[key] ?? key).replace(
|
|
1955
|
+
/\{\{(\w+)\}\}/g,
|
|
1956
|
+
(token, name) => Object.hasOwn(params, name) ? String(params[name]) : token
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
function FormRenderer(props) {
|
|
1961
|
+
if (!("schema" in props)) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ContextFormRenderer, { ...props });
|
|
1962
|
+
const {
|
|
1963
|
+
schema,
|
|
1964
|
+
locale = schema.defaultLocale ?? "en",
|
|
1965
|
+
translator = defaultRendererTranslator,
|
|
1966
|
+
initialValues,
|
|
1967
|
+
resetOnSuccess,
|
|
1968
|
+
onSubmit,
|
|
1969
|
+
...rendererProps
|
|
1970
|
+
} = props;
|
|
1971
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1972
|
+
FormProvider,
|
|
1973
|
+
{
|
|
1974
|
+
schema,
|
|
1975
|
+
locale,
|
|
1976
|
+
translator,
|
|
1977
|
+
onSubmit,
|
|
1978
|
+
...initialValues === void 0 ? {} : { initialValues },
|
|
1979
|
+
...resetOnSuccess === void 0 ? {} : { resetOnSuccess },
|
|
1980
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ContextFormRenderer, { ...rendererProps })
|
|
1981
|
+
}
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1514
1984
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1515
1985
|
0 && (module.exports = {
|
|
1516
1986
|
FormBuilder,
|
|
1517
1987
|
FormProvider,
|
|
1518
1988
|
FormRenderer,
|
|
1519
1989
|
useField,
|
|
1520
|
-
useForm
|
|
1990
|
+
useForm,
|
|
1991
|
+
useFormBuilder
|
|
1521
1992
|
});
|