@form-engine-ts/react 1.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/dist/index.cjs ADDED
@@ -0,0 +1,840 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ FormBuilder: () => FormBuilder,
24
+ FormProvider: () => FormProvider,
25
+ FormRenderer: () => FormRenderer,
26
+ useField: () => useField,
27
+ useForm: () => useForm
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/builder.tsx
32
+ var import_core = require("@form-engine-ts/core");
33
+ var import_jsx_runtime = require("react/jsx-runtime");
34
+ var FIELD_TYPES = [
35
+ "text",
36
+ "textarea",
37
+ "number",
38
+ "rating",
39
+ "select",
40
+ "multi-select",
41
+ "checkbox",
42
+ "radio"
43
+ ];
44
+ var BUILDER_DEFAULTS = {
45
+ "builder.formBuilder": "Form builder",
46
+ "builder.moveUp": "Move {{title}} up",
47
+ "builder.moveDown": "Move {{title}} down",
48
+ "builder.delete": "Delete {{title}}",
49
+ "builder.deleteAction": "Delete",
50
+ "builder.questionTitle": "\u8CEA\u554F\u6587 / Question Title",
51
+ "builder.questionTitlePlaceholder": "Example: Tell us what we could improve",
52
+ "builder.newQuestionTitle": "New question",
53
+ "builder.type": "Type",
54
+ "builder.required": "Required",
55
+ "builder.minimum": "Minimum",
56
+ "builder.maximum": "Maximum",
57
+ "builder.options": "Options",
58
+ "builder.optionLabel": "\u9078\u629E\u80A2 / Option Label {{index}}",
59
+ "builder.optionLabelPlaceholder": "Example: Very satisfied",
60
+ "builder.newOptionLabel": "Option {{index}}",
61
+ "builder.remove": "Remove",
62
+ "builder.addOption": "Add option",
63
+ "builder.displayCondition": "Display condition",
64
+ "builder.alwaysVisible": "Always visible",
65
+ "builder.conditionOperator": "Condition operator",
66
+ "builder.conditionValue": "Condition value",
67
+ "builder.conditionTrue": "true",
68
+ "builder.conditionFalse": "false",
69
+ "builder.addQuestion": "Add question",
70
+ "builder.fieldType.text": "Text",
71
+ "builder.fieldType.textarea": "Textarea",
72
+ "builder.fieldType.number": "Number",
73
+ "builder.fieldType.rating": "Rating",
74
+ "builder.fieldType.select": "Select",
75
+ "builder.fieldType.multi-select": "Multi-select",
76
+ "builder.fieldType.checkbox": "Checkbox",
77
+ "builder.fieldType.radio": "Radio",
78
+ "builder.operator.equals": "equals",
79
+ "builder.operator.not_equals": "does not equal",
80
+ "builder.operator.contains": "contains",
81
+ "builder.operator.not_empty": "is not empty"
82
+ };
83
+ function interpolate(template, params) {
84
+ return template.replace(
85
+ /\{\{(\w+)\}\}/g,
86
+ (token, name) => Object.hasOwn(params, name) ? String(params[name]) : token
87
+ );
88
+ }
89
+ function fieldTypeKey(type) {
90
+ return `builder.fieldType.${type}`;
91
+ }
92
+ function operatorKey(operator) {
93
+ return `builder.operator.${operator}`;
94
+ }
95
+ function createUniqueId(prefix, existingIds) {
96
+ let id;
97
+ do {
98
+ id = `${prefix}_${globalThis.crypto.randomUUID().slice(0, 8)}`;
99
+ } while (existingIds.has(id));
100
+ return id;
101
+ }
102
+ function baseField(field, type) {
103
+ return {
104
+ id: field.id,
105
+ type,
106
+ title: field.title,
107
+ ...field.description === void 0 ? {} : { description: field.description },
108
+ ...field.translationKey === void 0 ? {} : { translationKey: field.translationKey },
109
+ required: field.required,
110
+ ...field.displayCondition === void 0 ? {} : { displayCondition: field.displayCondition }
111
+ };
112
+ }
113
+ function normalizeField(field, type, newOptionLabel) {
114
+ const base = baseField(field, type);
115
+ if (type === "text" || type === "textarea") return { ...base, type };
116
+ if (type === "number") return { ...base, type };
117
+ if (type === "rating") return { ...base, type, min: 1, max: 5 };
118
+ if (type === "checkbox") return { ...base, type };
119
+ const options = "options" in field && field.options.length > 0 ? field.options : [{ id: createUniqueId("opt", /* @__PURE__ */ new Set()), label: newOptionLabel }];
120
+ return { ...base, type, options };
121
+ }
122
+ function defaultConditionValue(field) {
123
+ if (field.type === "checkbox") return true;
124
+ if (field.type === "number" || field.type === "rating") return field.min ?? 1;
125
+ if ("options" in field) return field.options[0]?.id ?? "";
126
+ return "";
127
+ }
128
+ function conditionOperators(field) {
129
+ if (field.type === "multi-select") return ["contains", "not_empty"];
130
+ if (field.type === "text" || field.type === "textarea") {
131
+ return ["equals", "not_equals", "contains", "not_empty"];
132
+ }
133
+ return ["equals", "not_equals", "not_empty"];
134
+ }
135
+ function withoutDisplayCondition(field) {
136
+ const { displayCondition: _displayCondition, ...rest } = field;
137
+ return rest;
138
+ }
139
+ function sanitizeBuilderSchema(schema) {
140
+ const sanitized = (0, import_core.sanitizeSchema)(schema);
141
+ const indexById = new Map(sanitized.fields.map((field, index) => [field.id, index]));
142
+ return {
143
+ ...sanitized,
144
+ fields: sanitized.fields.map((field, index) => {
145
+ const sourceId = field.displayCondition?.questionId;
146
+ if (sourceId === void 0) return field;
147
+ const sourceIndex = indexById.get(sourceId);
148
+ return sourceIndex !== void 0 && sourceIndex < index ? field : withoutDisplayCondition(field);
149
+ })
150
+ };
151
+ }
152
+ function conditionWithValue(questionId, operator, value) {
153
+ return operator === "not_empty" ? { questionId, operator } : { questionId, operator, value };
154
+ }
155
+ function ConditionValueEditor({
156
+ source,
157
+ condition,
158
+ onChange,
159
+ translate
160
+ }) {
161
+ if (condition.operator === "not_empty") return null;
162
+ const update = (value) => onChange({ ...condition, value });
163
+ if (source.type === "checkbox") {
164
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { value: String(condition.value), onChange: (event) => update(event.currentTarget.value === "true"), children: [
165
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "true", children: translate("builder.conditionTrue") }),
166
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "false", children: translate("builder.conditionFalse") })
167
+ ] });
168
+ }
169
+ if (source.type === "number" || source.type === "rating") {
170
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
171
+ "input",
172
+ {
173
+ "aria-label": translate("builder.conditionValue"),
174
+ type: "number",
175
+ value: typeof condition.value === "number" ? condition.value : "",
176
+ onChange: (event) => update(event.currentTarget.value === "" ? 0 : event.currentTarget.valueAsNumber)
177
+ }
178
+ );
179
+ }
180
+ if ("options" in source) {
181
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", { value: String(condition.value ?? ""), onChange: (event) => update(event.currentTarget.value), children: source.options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: option.id, children: option.label }, option.id)) });
182
+ }
183
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
184
+ "input",
185
+ {
186
+ "aria-label": translate("builder.conditionValue"),
187
+ type: "text",
188
+ value: typeof condition.value === "string" ? condition.value : "",
189
+ onChange: (event) => update(event.currentTarget.value)
190
+ }
191
+ );
192
+ }
193
+ function FormBuilder({ schema, onChange, locale = "en", translator }) {
194
+ const translate = (key, params = {}) => {
195
+ const translated = translator?.translate(key, locale, params);
196
+ return translated === void 0 ? interpolate(BUILDER_DEFAULTS[key] ?? key, params) : translated;
197
+ };
198
+ const emitSchema = (candidate) => onChange(sanitizeBuilderSchema(candidate));
199
+ const updateField = (fieldId, update) => {
200
+ emitSchema({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? update(field) : field) });
201
+ };
202
+ const changeType = (fieldId, type) => {
203
+ emitSchema({
204
+ ...schema,
205
+ fields: schema.fields.map((field) => {
206
+ if (field.id === fieldId) {
207
+ return normalizeField(field, type, translate("builder.newOptionLabel", { index: 1 }));
208
+ }
209
+ if (field.displayCondition?.questionId === fieldId) {
210
+ const { displayCondition: _condition, ...withoutCondition } = field;
211
+ return withoutCondition;
212
+ }
213
+ return field;
214
+ })
215
+ });
216
+ };
217
+ const removeField = (fieldId) => {
218
+ if (schema.fields.length === 1) return;
219
+ emitSchema({ ...schema, fields: schema.fields.filter((field) => field.id !== fieldId) });
220
+ };
221
+ const moveField = (index, offset) => {
222
+ const target = index + offset;
223
+ if (target < 0 || target >= schema.fields.length) return;
224
+ const fields = [...schema.fields];
225
+ const current = fields[index];
226
+ const other = fields[target];
227
+ if (current === void 0 || other === void 0) return;
228
+ fields[index] = other;
229
+ fields[target] = current;
230
+ emitSchema({ ...schema, fields });
231
+ };
232
+ const addField = () => {
233
+ const id = createUniqueId("q", new Set(schema.fields.map((field) => field.id)));
234
+ emitSchema({
235
+ ...schema,
236
+ fields: [...schema.fields, { id, type: "text", title: translate("builder.newQuestionTitle"), required: false }]
237
+ });
238
+ };
239
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "form-engine-builder", "aria-label": translate("builder.formBuilder"), children: [
240
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "form-engine-builder__list", children: schema.fields.map((field, index) => {
241
+ const condition = field.displayCondition;
242
+ const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
243
+ const availableSources = schema.fields.slice(0, index);
244
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { className: "form-engine-builder__question", children: [
245
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("legend", { children: field.title }),
246
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__toolbar", children: [
247
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
248
+ "button",
249
+ {
250
+ type: "button",
251
+ disabled: index === 0,
252
+ onClick: () => moveField(index, -1),
253
+ "aria-label": translate("builder.moveUp", { title: field.title }),
254
+ children: "\u2191"
255
+ }
256
+ ),
257
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
258
+ "button",
259
+ {
260
+ type: "button",
261
+ disabled: index === schema.fields.length - 1,
262
+ onClick: () => moveField(index, 1),
263
+ "aria-label": translate("builder.moveDown", { title: field.title }),
264
+ children: "\u2193"
265
+ }
266
+ ),
267
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
268
+ "button",
269
+ {
270
+ type: "button",
271
+ disabled: schema.fields.length === 1,
272
+ onClick: () => removeField(field.id),
273
+ "aria-label": translate("builder.delete", { title: field.title }),
274
+ children: translate("builder.deleteAction")
275
+ }
276
+ )
277
+ ] }),
278
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__grid", children: [
279
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
280
+ translate("builder.questionTitle"),
281
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
282
+ "input",
283
+ {
284
+ value: field.title,
285
+ placeholder: translate("builder.questionTitlePlaceholder"),
286
+ onChange: (event) => updateField(field.id, (current) => ({
287
+ ...current,
288
+ title: event.currentTarget.value.trim().length === 0 ? current.title : event.currentTarget.value
289
+ }))
290
+ }
291
+ )
292
+ ] }),
293
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
294
+ translate("builder.type"),
295
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
296
+ "select",
297
+ {
298
+ value: field.type,
299
+ onChange: (event) => changeType(field.id, event.currentTarget.value),
300
+ children: FIELD_TYPES.map((type) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: type, children: translate(fieldTypeKey(type)) }, type))
301
+ }
302
+ )
303
+ ] }),
304
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "form-engine-builder__check", children: [
305
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
306
+ "input",
307
+ {
308
+ type: "checkbox",
309
+ checked: field.required === true,
310
+ onChange: (event) => updateField(field.id, (current) => ({ ...current, required: event.currentTarget.checked }))
311
+ }
312
+ ),
313
+ translate("builder.required")
314
+ ] })
315
+ ] }),
316
+ field.type === "rating" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__grid", children: [
317
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
318
+ translate("builder.minimum"),
319
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
320
+ "input",
321
+ {
322
+ type: "number",
323
+ value: field.min ?? 1,
324
+ onChange: (event) => {
325
+ const min = event.currentTarget.valueAsNumber;
326
+ if (!Number.isInteger(min)) return;
327
+ updateField(
328
+ field.id,
329
+ (current) => current.type === "rating" ? { ...current, min, max: Math.max(min, current.max ?? 5) } : current
330
+ );
331
+ }
332
+ }
333
+ )
334
+ ] }),
335
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
336
+ translate("builder.maximum"),
337
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
338
+ "input",
339
+ {
340
+ type: "number",
341
+ value: field.max ?? 5,
342
+ onChange: (event) => {
343
+ const max = event.currentTarget.valueAsNumber;
344
+ if (!Number.isInteger(max)) return;
345
+ updateField(
346
+ field.id,
347
+ (current) => current.type === "rating" ? { ...current, min: Math.min(current.min ?? 1, max), max } : current
348
+ );
349
+ }
350
+ }
351
+ )
352
+ ] })
353
+ ] }) : null,
354
+ "options" in field ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__options", children: [
355
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: translate("builder.options") }),
356
+ field.options.map((option, optionIndex) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__option", children: [
357
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
358
+ "input",
359
+ {
360
+ "aria-label": translate("builder.optionLabel", { index: optionIndex + 1 }),
361
+ value: option.label,
362
+ placeholder: translate("builder.optionLabelPlaceholder"),
363
+ onChange: (event) => updateField(field.id, (current) => {
364
+ if (!("options" in current)) return current;
365
+ const label = event.currentTarget.value;
366
+ if (label.trim().length === 0) return current;
367
+ return {
368
+ ...current,
369
+ options: current.options.map(
370
+ (item, itemIndex) => itemIndex === optionIndex ? { ...item, label } : item
371
+ )
372
+ };
373
+ })
374
+ }
375
+ ),
376
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
377
+ "button",
378
+ {
379
+ type: "button",
380
+ disabled: field.options.length === 1,
381
+ onClick: () => updateField(
382
+ field.id,
383
+ (current) => "options" in current ? {
384
+ ...current,
385
+ options: current.options.filter((_item, itemIndex) => itemIndex !== optionIndex)
386
+ } : current
387
+ ),
388
+ children: translate("builder.remove")
389
+ }
390
+ )
391
+ ] }, option.id)),
392
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
393
+ "button",
394
+ {
395
+ type: "button",
396
+ onClick: () => updateField(
397
+ field.id,
398
+ (current) => "options" in current ? (() => {
399
+ const id = createUniqueId("opt", new Set(current.options.map((option) => option.id)));
400
+ return {
401
+ ...current,
402
+ options: [
403
+ ...current.options,
404
+ {
405
+ id,
406
+ label: translate("builder.newOptionLabel", { index: current.options.length + 1 })
407
+ }
408
+ ]
409
+ };
410
+ })() : current
411
+ ),
412
+ children: translate("builder.addOption")
413
+ }
414
+ )
415
+ ] }) : null,
416
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__condition", children: [
417
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
418
+ translate("builder.displayCondition"),
419
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
420
+ "select",
421
+ {
422
+ value: condition?.questionId ?? "",
423
+ onChange: (event) => {
424
+ const selected = schema.fields.find((item) => item.id === event.currentTarget.value);
425
+ updateField(field.id, (current) => {
426
+ if (selected === void 0) {
427
+ const { displayCondition: _condition, ...withoutCondition } = current;
428
+ return withoutCondition;
429
+ }
430
+ const operator = conditionOperators(selected)[0] ?? "not_empty";
431
+ return {
432
+ ...current,
433
+ displayCondition: conditionWithValue(selected.id, operator, defaultConditionValue(selected))
434
+ };
435
+ });
436
+ },
437
+ children: [
438
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: translate("builder.alwaysVisible") }),
439
+ availableSources.map((candidate) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: candidate.id, children: candidate.title }, candidate.id))
440
+ ]
441
+ }
442
+ )
443
+ ] }),
444
+ condition !== void 0 && source !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
445
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
446
+ "select",
447
+ {
448
+ "aria-label": translate("builder.conditionOperator"),
449
+ value: condition.operator,
450
+ onChange: (event) => {
451
+ const operator = event.currentTarget.value;
452
+ updateField(field.id, (current) => ({
453
+ ...current,
454
+ displayCondition: conditionWithValue(source.id, operator, defaultConditionValue(source))
455
+ }));
456
+ },
457
+ children: conditionOperators(source).map((operator) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: operator, children: translate(operatorKey(operator)) }, operator))
458
+ }
459
+ ),
460
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
461
+ ConditionValueEditor,
462
+ {
463
+ source,
464
+ condition,
465
+ onChange: (next) => updateField(field.id, (current) => ({ ...current, displayCondition: next })),
466
+ translate
467
+ }
468
+ )
469
+ ] }) : null
470
+ ] })
471
+ ] }, field.id);
472
+ }) }),
473
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "form-engine-builder__add", type: "button", onClick: addField, children: translate("builder.addQuestion") })
474
+ ] });
475
+ }
476
+
477
+ // src/context.tsx
478
+ var import_core2 = require("@form-engine-ts/core");
479
+ var import_react = require("react");
480
+ var import_jsx_runtime2 = require("react/jsx-runtime");
481
+ var FormContext = (0, import_react.createContext)(null);
482
+ function issuesByField(issues) {
483
+ const result = {};
484
+ for (const issue of issues) result[issue.fieldId] ??= issue;
485
+ return result;
486
+ }
487
+ function FormProvider({
488
+ schema,
489
+ locale,
490
+ translator,
491
+ initialValues = {},
492
+ resetOnSuccess = false,
493
+ onSubmit,
494
+ children
495
+ }) {
496
+ const validSchema = (0, import_react.useMemo)(() => {
497
+ (0, import_core2.assertValidFormSchema)(schema);
498
+ return schema;
499
+ }, [schema]);
500
+ const [values, setValues] = (0, import_react.useState)(() => ({ ...initialValues }));
501
+ const [errors, setErrors] = (0, import_react.useState)({});
502
+ const [submitStatus, setSubmitStatus] = (0, import_react.useState)("idle");
503
+ const [submitError, setSubmitError] = (0, import_react.useState)(null);
504
+ const visibility = (0, import_react.useMemo)(() => (0, import_core2.calculateFieldVisibility)(validSchema, values), [validSchema, values]);
505
+ (0, import_react.useEffect)(() => {
506
+ const fieldIds = new Set(validSchema.fields.map((field) => field.id));
507
+ setValues((current) => Object.fromEntries(Object.entries(current).filter(([fieldId]) => fieldIds.has(fieldId))));
508
+ }, [validSchema]);
509
+ const setValue = (0, import_react.useCallback)(
510
+ (fieldId, value) => {
511
+ setValues((current) => {
512
+ const next = { ...current, [fieldId]: value };
513
+ setErrors((currentErrors) => {
514
+ if (Object.keys(currentErrors).length === 0) return currentErrors;
515
+ const result = (0, import_core2.validateAnswers)(validSchema, next);
516
+ return issuesByField(result.issues);
517
+ });
518
+ return next;
519
+ });
520
+ setSubmitStatus((current) => current === "success" || current === "error" ? "idle" : current);
521
+ setSubmitError(null);
522
+ },
523
+ [validSchema]
524
+ );
525
+ const reset = (0, import_react.useCallback)(() => {
526
+ setValues({ ...initialValues });
527
+ setErrors({});
528
+ setSubmitStatus("idle");
529
+ setSubmitError(null);
530
+ }, [initialValues]);
531
+ const submit = (0, import_react.useCallback)(async () => {
532
+ const validation = (0, import_core2.validateAnswers)(validSchema, values);
533
+ if (!validation.valid) {
534
+ setErrors(issuesByField(validation.issues));
535
+ setSubmitStatus("error");
536
+ setSubmitError(null);
537
+ return false;
538
+ }
539
+ setErrors({});
540
+ setSubmitStatus("submitting");
541
+ setSubmitError(null);
542
+ try {
543
+ await onSubmit((0, import_core2.selectVisibleAnswers)(validSchema, values));
544
+ if (resetOnSuccess) setValues({ ...initialValues });
545
+ setSubmitStatus("success");
546
+ return true;
547
+ } catch (cause) {
548
+ setSubmitError(cause instanceof Error ? cause : new Error(String(cause)));
549
+ setSubmitStatus("error");
550
+ return false;
551
+ }
552
+ }, [initialValues, onSubmit, resetOnSuccess, validSchema, values]);
553
+ const translate = (0, import_react.useCallback)(
554
+ (key, params) => translator.translate(key, locale, params),
555
+ [locale, translator]
556
+ );
557
+ const contextValue = (0, import_react.useMemo)(
558
+ () => ({
559
+ schema: validSchema,
560
+ locale,
561
+ translator,
562
+ values,
563
+ visibility,
564
+ errors,
565
+ submitStatus,
566
+ submitError,
567
+ isSubmitting: submitStatus === "submitting",
568
+ setValue,
569
+ reset,
570
+ submit,
571
+ translate
572
+ }),
573
+ [
574
+ errors,
575
+ locale,
576
+ reset,
577
+ setValue,
578
+ submit,
579
+ submitError,
580
+ submitStatus,
581
+ translate,
582
+ translator,
583
+ validSchema,
584
+ values,
585
+ visibility
586
+ ]
587
+ );
588
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FormContext.Provider, { value: contextValue, children });
589
+ }
590
+ function useForm() {
591
+ const context = (0, import_react.useContext)(FormContext);
592
+ if (context === null) throw new Error("useForm must be called inside a FormProvider.");
593
+ return context;
594
+ }
595
+ function useField(fieldId) {
596
+ const form = useForm();
597
+ const field = form.schema.fields.find((item) => item.id === fieldId);
598
+ if (field === void 0) throw new Error(`Unknown form field: ${fieldId}`);
599
+ const setValue = (0, import_react.useCallback)((value) => form.setValue(fieldId, value), [fieldId, form]);
600
+ return { field, value: form.values[fieldId], error: form.errors[fieldId], setValue };
601
+ }
602
+
603
+ // src/renderer.tsx
604
+ var import_core3 = require("@form-engine-ts/core");
605
+ var import_react2 = require("react");
606
+ var import_jsx_runtime3 = require("react/jsx-runtime");
607
+ function describedBy(field, error, helpId, errorId) {
608
+ const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
609
+ Boolean
610
+ );
611
+ return ids.length === 0 ? void 0 : ids.join(" ");
612
+ }
613
+ function RequiredMark({ required }) {
614
+ return required ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "fe-required", "aria-hidden": "true", children: [
615
+ " ",
616
+ "*"
617
+ ] }) : null;
618
+ }
619
+ function FieldMessage({ props }) {
620
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
621
+ props.field.description === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { id: props.helpId, className: "fe-help", children: props.field.description }),
622
+ props.error === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { id: props.errorId, className: "fe-error", children: props.translate(props.error.messageKey, props.error.params) })
623
+ ] });
624
+ }
625
+ function DefaultField(props) {
626
+ const { field, value, setValue, inputId, error, translate } = props;
627
+ const ariaProps = {
628
+ "aria-describedby": describedBy(field, error, props.helpId, props.errorId),
629
+ "aria-invalid": error === void 0 ? void 0 : true
630
+ };
631
+ if (field.type === "checkbox") {
632
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-field fe-field--checkbox", "data-field-id": field.id, children: [
633
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-check-label", htmlFor: inputId, children: [
634
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
635
+ "input",
636
+ {
637
+ ...ariaProps,
638
+ id: inputId,
639
+ name: field.id,
640
+ type: "checkbox",
641
+ checked: value === true,
642
+ onChange: (event) => setValue(event.currentTarget.checked)
643
+ }
644
+ ),
645
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
646
+ field.title,
647
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RequiredMark, { required: field.required })
648
+ ] })
649
+ ] }),
650
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FieldMessage, { props })
651
+ ] });
652
+ }
653
+ if (field.type === "radio" || field.type === "multi-select") {
654
+ const selected = Array.isArray(value) ? value : [];
655
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("fieldset", { className: `fe-field fe-field--${field.type}`, "data-field-id": field.id, ...ariaProps, children: [
656
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("legend", { className: "fe-label", children: [
657
+ field.title,
658
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RequiredMark, { required: field.required })
659
+ ] }),
660
+ field.options.map((option, index) => {
661
+ const optionId = `${inputId}-${index}`;
662
+ const checked = field.type === "radio" ? value === option.id : selected.includes(option.id);
663
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-check-label", htmlFor: optionId, children: [
664
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
665
+ "input",
666
+ {
667
+ id: optionId,
668
+ name: field.id,
669
+ type: field.type === "radio" ? "radio" : "checkbox",
670
+ value: option.id,
671
+ checked,
672
+ onChange: (event) => {
673
+ if (field.type === "radio") setValue(option.id);
674
+ else
675
+ setValue(
676
+ event.currentTarget.checked ? [...selected, option.id] : selected.filter((item) => item !== option.id)
677
+ );
678
+ }
679
+ }
680
+ ),
681
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: option.label })
682
+ ] }, option.id);
683
+ }),
684
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FieldMessage, { props })
685
+ ] });
686
+ }
687
+ if (field.type === "rating") {
688
+ const min = field.min ?? 1;
689
+ const max = field.max ?? 5;
690
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("fieldset", { className: "fe-field fe-field--rating", "data-field-id": field.id, ...ariaProps, children: [
691
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("legend", { className: "fe-label", children: [
692
+ field.title,
693
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RequiredMark, { required: field.required })
694
+ ] }),
695
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-rating-options", children: Array.from({ length: max - min + 1 }, (_, index) => min + index).map((rating) => {
696
+ const optionId = `${inputId}-${rating}`;
697
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-rating-label", htmlFor: optionId, children: [
698
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
699
+ "input",
700
+ {
701
+ id: optionId,
702
+ name: field.id,
703
+ type: "radio",
704
+ value: rating,
705
+ checked: value === rating,
706
+ onChange: () => setValue(rating)
707
+ }
708
+ ),
709
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: rating })
710
+ ] }, rating);
711
+ }) }),
712
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FieldMessage, { props })
713
+ ] });
714
+ }
715
+ const label = /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-label", htmlFor: inputId, children: [
716
+ field.title,
717
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RequiredMark, { required: field.required })
718
+ ] });
719
+ let control;
720
+ if (field.type === "textarea") {
721
+ control = /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
722
+ "textarea",
723
+ {
724
+ ...ariaProps,
725
+ id: inputId,
726
+ name: field.id,
727
+ placeholder: field.placeholderKey === void 0 ? void 0 : translate(field.placeholderKey),
728
+ value: typeof value === "string" ? value : "",
729
+ onChange: (event) => setValue(event.currentTarget.value)
730
+ }
731
+ );
732
+ } else if (field.type === "number") {
733
+ control = /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
734
+ "input",
735
+ {
736
+ ...ariaProps,
737
+ id: inputId,
738
+ name: field.id,
739
+ type: "number",
740
+ min: field.min,
741
+ max: field.max,
742
+ step: field.step,
743
+ placeholder: field.placeholderKey === void 0 ? void 0 : translate(field.placeholderKey),
744
+ value: typeof value === "number" ? value : "",
745
+ onChange: (event) => setValue(event.currentTarget.value === "" ? void 0 : event.currentTarget.valueAsNumber)
746
+ }
747
+ );
748
+ } else if (field.type === "select") {
749
+ control = /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
750
+ "select",
751
+ {
752
+ ...ariaProps,
753
+ id: inputId,
754
+ name: field.id,
755
+ value: typeof value === "string" ? value : "",
756
+ onChange: (event) => setValue(event.currentTarget.value || void 0),
757
+ children: [
758
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "", children: "\u2014" }),
759
+ field.options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: option.id, children: option.label }, option.id))
760
+ ]
761
+ }
762
+ );
763
+ } else {
764
+ const placeholderKey = "placeholderKey" in field ? field.placeholderKey : void 0;
765
+ control = /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
766
+ "input",
767
+ {
768
+ ...ariaProps,
769
+ id: inputId,
770
+ name: field.id,
771
+ type: "text",
772
+ placeholder: placeholderKey === void 0 ? void 0 : translate(placeholderKey),
773
+ value: typeof value === "string" ? value : "",
774
+ onChange: (event) => setValue(event.currentTarget.value)
775
+ }
776
+ );
777
+ }
778
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `fe-field fe-field--${field.type}`, "data-field-id": field.id, children: [
779
+ label,
780
+ control,
781
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FieldMessage, { props })
782
+ ] });
783
+ }
784
+ function FormRenderer({
785
+ components = {},
786
+ className = "",
787
+ successMessageKey,
788
+ errorMessageKey
789
+ }) {
790
+ const form = useForm();
791
+ const prefix = (0, import_react2.useId)().replace(/:/g, "");
792
+ const handleSubmit = async (event) => {
793
+ event.preventDefault();
794
+ const formElement = event.currentTarget;
795
+ const validation = (0, import_core3.validateAnswers)(form.schema, form.values);
796
+ const firstInvalidFieldId = validation.issues[0]?.fieldId;
797
+ const valid = await form.submit();
798
+ if (!valid && firstInvalidFieldId !== void 0) {
799
+ queueMicrotask(() => {
800
+ const fieldContainer = [...formElement.querySelectorAll("[data-field-id]")].find(
801
+ (element) => element.dataset.fieldId === firstInvalidFieldId
802
+ );
803
+ fieldContainer?.querySelector("input, select, textarea")?.focus();
804
+ });
805
+ }
806
+ };
807
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
808
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { className: "fe-header", children: [
809
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { children: form.schema.title }),
810
+ form.schema.description === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: form.schema.description })
811
+ ] }),
812
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-fields", children: form.schema.fields.filter((field) => form.visibility[field.id] === true).map((field) => {
813
+ const props = {
814
+ field,
815
+ value: form.values[field.id],
816
+ error: form.errors[field.id],
817
+ setValue: (value) => form.setValue(field.id, value),
818
+ translate: form.translate,
819
+ inputId: `${prefix}-${field.id}`,
820
+ errorId: `${prefix}-${field.id}-error`,
821
+ helpId: `${prefix}-${field.id}-help`
822
+ };
823
+ const Component = components[field.type];
824
+ return Component === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DefaultField, { ...props }, field.id) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { ...props }, field.id);
825
+ }) }),
826
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "fe-submit", type: "submit", disabled: form.isSubmitting, children: form.translate(form.schema.submitLabelKey ?? "form.submit") }),
827
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-status", "aria-live": "polite", children: [
828
+ form.submitStatus === "success" && successMessageKey !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "status", children: form.translate(successMessageKey) }) : null,
829
+ form.submitStatus === "error" && form.submitError !== null && errorMessageKey !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", children: form.translate(errorMessageKey) }) : null
830
+ ] })
831
+ ] });
832
+ }
833
+ // Annotate the CommonJS export names for ESM import in node:
834
+ 0 && (module.exports = {
835
+ FormBuilder,
836
+ FormProvider,
837
+ FormRenderer,
838
+ useField,
839
+ useForm
840
+ });