@evanschleret/formforgeclient 1.2.4 → 2.0.1

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.
Files changed (83) hide show
  1. package/README.md +10 -0
  2. package/dist/module.cjs +1 -0
  3. package/dist/module.d.cts +1 -0
  4. package/dist/module.d.mts +1 -0
  5. package/dist/module.d.ts +1 -0
  6. package/dist/module.json +1 -1
  7. package/dist/module.mjs +1 -0
  8. package/dist/runtime/api/client.js +4 -2
  9. package/dist/runtime/api/request.d.ts +1 -0
  10. package/dist/runtime/api/schema.js +4 -4
  11. package/dist/runtime/assets/formforge.css +1 -0
  12. package/dist/runtime/composables/index.d.ts +1 -1
  13. package/dist/runtime/composables/useFormForgeBuilder.d.ts +24 -2
  14. package/dist/runtime/composables/useFormForgeBuilder.js +299 -43
  15. package/dist/runtime/composables/useFormForgeForm.js +15 -5
  16. package/dist/runtime/composables/useFormForgeI18n.d.ts +299 -19
  17. package/dist/runtime/composables/useFormForgeI18n.js +299 -19
  18. package/dist/runtime/composables/useFormForgeSubmit.js +31 -9
  19. package/dist/runtime/index.d.ts +1 -0
  20. package/dist/runtime/renderers/default/FormForgeBuilder.d.vue.ts +21 -2
  21. package/dist/runtime/renderers/default/FormForgeBuilder.vue +689 -738
  22. package/dist/runtime/renderers/default/FormForgeBuilder.vue.d.ts +21 -2
  23. package/dist/runtime/renderers/default/FormForgeBuilderBlockSettingsModal.d.vue.ts +17 -0
  24. package/dist/runtime/renderers/default/FormForgeBuilderBlockSettingsModal.vue +32 -0
  25. package/dist/runtime/renderers/default/FormForgeBuilderBlockSettingsModal.vue.d.ts +17 -0
  26. package/dist/runtime/renderers/default/FormForgeRenderer.d.vue.ts +3 -4
  27. package/dist/runtime/renderers/default/FormForgeRenderer.vue +344 -294
  28. package/dist/runtime/renderers/default/FormForgeRenderer.vue.d.ts +3 -4
  29. package/dist/runtime/renderers/default/FormForgeRendererField.d.vue.ts +22 -0
  30. package/dist/runtime/renderers/default/FormForgeRendererField.vue +237 -0
  31. package/dist/runtime/renderers/default/FormForgeRendererField.vue.d.ts +22 -0
  32. package/dist/runtime/renderers/default/FormForgeRendererPage.d.vue.ts +18 -0
  33. package/dist/runtime/renderers/default/FormForgeRendererPage.vue +31 -0
  34. package/dist/runtime/renderers/default/FormForgeRendererPage.vue.d.ts +18 -0
  35. package/dist/runtime/renderers/default/FormForgeResponse.vue +4 -3
  36. package/dist/runtime/renderers/default/builder/FormForgeBuilderAddressFieldsCard.d.vue.ts +11 -0
  37. package/dist/runtime/renderers/default/builder/FormForgeBuilderAddressFieldsCard.vue +118 -0
  38. package/dist/runtime/renderers/default/builder/FormForgeBuilderAddressFieldsCard.vue.d.ts +11 -0
  39. package/dist/runtime/renderers/default/builder/FormForgeBuilderBlockCard.d.vue.ts +46 -0
  40. package/dist/runtime/renderers/default/builder/FormForgeBuilderBlockCard.vue +205 -0
  41. package/dist/runtime/renderers/default/builder/FormForgeBuilderBlockCard.vue.d.ts +46 -0
  42. package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceDisplayField.d.vue.ts +11 -0
  43. package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceDisplayField.vue +37 -0
  44. package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceDisplayField.vue.d.ts +11 -0
  45. package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceOptionsField.d.vue.ts +11 -0
  46. package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceOptionsField.vue +195 -0
  47. package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceOptionsField.vue.d.ts +11 -0
  48. package/dist/runtime/renderers/default/builder/FormForgeBuilderDescriptionField.d.vue.ts +14 -0
  49. package/dist/runtime/renderers/default/builder/FormForgeBuilderDescriptionField.vue +91 -0
  50. package/dist/runtime/renderers/default/builder/FormForgeBuilderDescriptionField.vue.d.ts +14 -0
  51. package/dist/runtime/renderers/default/builder/FormForgeBuilderLogicPanel.d.vue.ts +13 -0
  52. package/dist/runtime/renderers/default/builder/FormForgeBuilderLogicPanel.vue +387 -0
  53. package/dist/runtime/renderers/default/builder/FormForgeBuilderLogicPanel.vue.d.ts +13 -0
  54. package/dist/runtime/renderers/default/builder/FormForgeBuilderQuestionRow.d.vue.ts +44 -0
  55. package/dist/runtime/renderers/default/builder/FormForgeBuilderQuestionRow.vue +328 -0
  56. package/dist/runtime/renderers/default/builder/FormForgeBuilderQuestionRow.vue.d.ts +44 -0
  57. package/dist/runtime/renderers/default/builder/FormForgeBuilderTemporalModeField.d.vue.ts +11 -0
  58. package/dist/runtime/renderers/default/builder/FormForgeBuilderTemporalModeField.vue +47 -0
  59. package/dist/runtime/renderers/default/builder/FormForgeBuilderTemporalModeField.vue.d.ts +11 -0
  60. package/dist/runtime/renderers/default/builder/FormForgeBuilderValidationRulesSection.d.vue.ts +14 -0
  61. package/dist/runtime/renderers/default/builder/FormForgeBuilderValidationRulesSection.vue +595 -0
  62. package/dist/runtime/renderers/default/builder/FormForgeBuilderValidationRulesSection.vue.d.ts +14 -0
  63. package/dist/runtime/renderers/default/builder/builderFieldHelpers.d.ts +3 -0
  64. package/dist/runtime/renderers/default/builder/builderFieldHelpers.js +4 -0
  65. package/dist/runtime/types/index.d.ts +1 -1
  66. package/dist/runtime/types/management.d.ts +12 -0
  67. package/dist/runtime/types/schema.d.ts +72 -4
  68. package/dist/runtime/utils/defaults.d.ts +7 -0
  69. package/dist/runtime/utils/defaults.js +86 -0
  70. package/dist/runtime/utils/page-logic.d.ts +24 -0
  71. package/dist/runtime/utils/page-logic.js +351 -0
  72. package/dist/runtime/utils/rich-text.d.ts +3 -0
  73. package/dist/runtime/utils/rich-text.js +72 -0
  74. package/dist/runtime/utils/schema.d.ts +1 -1
  75. package/dist/runtime/utils/schema.js +70 -16
  76. package/dist/runtime/utils/temporal.d.ts +10 -0
  77. package/dist/runtime/utils/temporal.js +28 -0
  78. package/dist/runtime/utils/validation.d.ts +5 -0
  79. package/dist/runtime/utils/validation.js +36 -0
  80. package/dist/runtime/validation/zod.d.ts +5 -2
  81. package/dist/runtime/validation/zod.js +563 -54
  82. package/dist/types.d.mts +2 -0
  83. package/package.json +18 -14
@@ -1,25 +1,23 @@
1
1
  <script setup>
2
2
  import { computed, ref, useTemplateRef, watch } from "#imports";
3
- import { getLocalTimeZone, parseAbsoluteToLocal, parseDate, parseDateTime, parseTime } from "@internationalized/date";
4
- import UCheckbox from "@nuxt/ui/components/Checkbox.vue";
5
- import UCheckboxGroup from "@nuxt/ui/components/CheckboxGroup.vue";
6
- import UFileUpload from "@nuxt/ui/components/FileUpload.vue";
3
+ import { parseDate, parseTime } from "@internationalized/date";
7
4
  import UInput from "@nuxt/ui/components/Input.vue";
8
- import UInputDate from "@nuxt/ui/components/InputDate.vue";
9
- import UInputNumber from "@nuxt/ui/components/InputNumber.vue";
10
- import UInputTime from "@nuxt/ui/components/InputTime.vue";
11
5
  import UProgress from "@nuxt/ui/components/Progress.vue";
12
- import URadioGroup from "@nuxt/ui/components/RadioGroup.vue";
13
- import USelect from "@nuxt/ui/components/Select.vue";
14
- import USelectMenu from "@nuxt/ui/components/SelectMenu.vue";
15
- import UStepper from "@nuxt/ui/components/Stepper.vue";
16
- import USwitch from "@nuxt/ui/components/Switch.vue";
17
- import UTextarea from "@nuxt/ui/components/Textarea.vue";
18
6
  import { isFormForgeJsonObject } from "../../utils/object";
7
+ import { createDefaultAddressFields } from "../../utils/defaults";
8
+ import { resolveTemporalMode } from "../../utils/temporal";
19
9
  import { sanitizePayloadWithSchema } from "../../utils/renderer-payload";
20
10
  import { useFormForgeForm } from "../../composables/useFormForgeForm";
21
11
  import { useFormForgeI18n } from "../../composables/useFormForgeI18n";
22
12
  import { useFormForgeSubmit } from "../../composables/useFormForgeSubmit";
13
+ import { createFormForgeZodSchema } from "../../validation/zod";
14
+ import {
15
+ evaluatePageLogicRule,
16
+ getPageLogic,
17
+ resolvePageLogicJumpTarget
18
+ } from "../../utils/page-logic";
19
+ import FormForgeRendererField from "./FormForgeRendererField.vue";
20
+ import FormForgeRendererPage from "./FormForgeRendererPage.vue";
23
21
  const props = defineProps({
24
22
  schema: { type: Object, required: false, default: void 0 },
25
23
  modelValue: { type: Object, required: false, default: void 0 },
@@ -37,12 +35,12 @@ const props = defineProps({
37
35
  uploadMode: { type: String, required: false, default: void 0 },
38
36
  clearAfterSubmit: { type: Boolean, required: false, default: false },
39
37
  showProgress: { type: Boolean, required: false, default: false },
40
- progressVariant: { type: String, required: false, default: "stepper" },
41
38
  showAlertOnError: { type: Boolean, required: false, default: false },
42
39
  validateOn: { type: Array, required: false, default: void 0 },
43
- validateOnBlur: { type: Boolean, required: false, default: void 0 }
40
+ validateOnBlur: { type: Boolean, required: false, default: void 0 },
41
+ previewPageKey: { type: [String, null], required: false, default: null }
44
42
  });
45
- const { t } = useFormForgeI18n({
43
+ const { t, locale } = useFormForgeI18n({
46
44
  locale: () => props.clientConfig?.locale
47
45
  });
48
46
  const emit = defineEmits(["update:modelValue", "submit", "submitted", "error"]);
@@ -106,7 +104,7 @@ const internalSubmit = useFormForgeSubmit({
106
104
  key: internalFormKey.value === "" ? "__missing_form_key__" : internalFormKey.value,
107
105
  version: props.formVersion,
108
106
  endpoint: props.endpoint,
109
- schema: () => internalForm.schema.value,
107
+ schema: () => getResolvedSchema(),
110
108
  state: () => internalForm.state.value,
111
109
  clientConfig: props.clientConfig
112
110
  });
@@ -134,7 +132,13 @@ function getResolvedSchema() {
134
132
  }
135
133
  function getResolvedZodSchema() {
136
134
  if (usesExternalSchema.value) {
137
- return unwrapZodSchemaProp(props.zodSchema);
135
+ const schema = unwrapSchemaProp(props.schema);
136
+ if (schema === null) {
137
+ return void 0;
138
+ }
139
+ return unwrapZodSchemaProp(props.zodSchema) ?? createFormForgeZodSchema(schema, {
140
+ locale: locale.value
141
+ });
138
142
  }
139
143
  return internalForm.zodSchema.value;
140
144
  }
@@ -160,6 +164,32 @@ const formState = computed({
160
164
  internalForm.replaceState(value);
161
165
  }
162
166
  });
167
+ const submissionCode = ref("");
168
+ const isPreviewMode = computed(() => {
169
+ return typeof props.previewPageKey === "string" && props.previewPageKey.trim() !== "";
170
+ });
171
+ const requiresSubmissionCode = computed(() => {
172
+ const schema = getResolvedSchema();
173
+ if (schema === null) {
174
+ return false;
175
+ }
176
+ return schema.submission_code_required === true;
177
+ });
178
+ const availabilityAlert = computed(() => {
179
+ const schema = getResolvedSchema();
180
+ if (schema === null) {
181
+ return null;
182
+ }
183
+ const publishAt = typeof schema.publish_at === "string" ? Date.parse(schema.publish_at) : Number.NaN;
184
+ if (!Number.isNaN(publishAt) && publishAt > Date.now()) {
185
+ return t("renderer.formNotAvailableYet");
186
+ }
187
+ const pauseAt = typeof schema.pause_at === "string" ? Date.parse(schema.pause_at) : Number.NaN;
188
+ if (!Number.isNaN(pauseAt) && pauseAt <= Date.now()) {
189
+ return t("renderer.formPaused");
190
+ }
191
+ return null;
192
+ });
163
193
  watch(
164
194
  () => [usesExternalModel.value, getResolvedSchema(), unwrapModelValueProp(props.modelValue)],
165
195
  ([externalModel, schema, modelValue]) => {
@@ -200,6 +230,26 @@ const displayPages = computed(() => {
200
230
  }
201
231
  ];
202
232
  });
233
+ const previewPage = computed(() => {
234
+ if (!isPreviewMode.value) {
235
+ return null;
236
+ }
237
+ const pageKey = props.previewPageKey?.trim();
238
+ if (typeof pageKey !== "string" || pageKey === "") {
239
+ return displayPages.value[0] ?? null;
240
+ }
241
+ return displayPages.value.find((page) => page.page_key === pageKey) ?? displayPages.value[0] ?? null;
242
+ });
243
+ const pageLogicByKey = computed(() => {
244
+ const map = {};
245
+ for (const page of displayPages.value) {
246
+ map[page.page_key] = getPageLogic(page);
247
+ }
248
+ return map;
249
+ });
250
+ const hasPageLogicRules = computed(() => {
251
+ return Object.values(pageLogicByKey.value).some((logic) => logic.rules.length > 0);
252
+ });
203
253
  const fieldsByKey = computed(() => {
204
254
  const map = {};
205
255
  for (const page of displayPages.value) {
@@ -233,7 +283,7 @@ function isEmptyValue(value) {
233
283
  const rangeValue = value;
234
284
  return isEmptyValue(rangeValue.start) && isEmptyValue(rangeValue.end);
235
285
  }
236
- return Object.keys(value).length === 0;
286
+ return Object.values(value).every((entry) => isEmptyValue(entry));
237
287
  }
238
288
  return false;
239
289
  }
@@ -407,7 +457,30 @@ const runtimeConditions = computed(() => {
407
457
  };
408
458
  }
409
459
  }
410
- if (schema === null || !Array.isArray(schema.conditions)) {
460
+ const requiredByLogic = /* @__PURE__ */ new Set();
461
+ if (schema === null || hasPageLogicRules.value || !Array.isArray(schema.conditions)) {
462
+ for (const [pageKey, logic] of Object.entries(pageLogicByKey.value)) {
463
+ const page = displayPages.value.find((item) => item.page_key === pageKey);
464
+ if (page === void 0) {
465
+ continue;
466
+ }
467
+ for (const rule of logic.rules) {
468
+ if (!evaluatePageLogicRule(rule, page, (field) => formState.value[field.name])) {
469
+ continue;
470
+ }
471
+ for (const thenAction of rule.then) {
472
+ if (thenAction.action === "require" && typeof thenAction.field_key === "string" && thenAction.field_key !== "") {
473
+ requiredByLogic.add(thenAction.field_key);
474
+ }
475
+ }
476
+ }
477
+ }
478
+ for (const fieldKey of requiredByLogic) {
479
+ const fieldState = fieldsState[fieldKey];
480
+ if (fieldState !== void 0) {
481
+ fieldState.required = true;
482
+ }
483
+ }
411
484
  return {
412
485
  pages: pagesState,
413
486
  fields: fieldsState
@@ -479,6 +552,28 @@ const runtimeConditions = computed(() => {
479
552
  fieldState.required = true;
480
553
  }
481
554
  }
555
+ for (const [pageKey, logic] of Object.entries(pageLogicByKey.value)) {
556
+ const page = displayPages.value.find((item) => item.page_key === pageKey);
557
+ if (page === void 0) {
558
+ continue;
559
+ }
560
+ for (const rule of logic.rules) {
561
+ if (!evaluatePageLogicRule(rule, page, (field) => formState.value[field.name])) {
562
+ continue;
563
+ }
564
+ for (const thenAction of rule.then) {
565
+ if (thenAction.action === "require" && typeof thenAction.field_key === "string" && thenAction.field_key !== "") {
566
+ requiredByLogic.add(thenAction.field_key);
567
+ }
568
+ }
569
+ }
570
+ }
571
+ for (const fieldKey of requiredByLogic) {
572
+ const fieldState = fieldsState[fieldKey];
573
+ if (fieldState !== void 0) {
574
+ fieldState.required = true;
575
+ }
576
+ }
482
577
  return {
483
578
  pages: pagesState,
484
579
  fields: fieldsState
@@ -497,6 +592,26 @@ const pageKeyByFieldName = computed(() => {
497
592
  return map;
498
593
  });
499
594
  const activePageKey = ref(null);
595
+ const isPageTransitioning = ref(false);
596
+ watch(
597
+ () => [isPreviewMode.value, props.previewPageKey, displayPages.value.map((page) => page.page_key)],
598
+ ([previewMode, previewPageKey, pageKeys]) => {
599
+ if (!previewMode) {
600
+ return;
601
+ }
602
+ const pageKey = typeof previewPageKey === "string" ? previewPageKey.trim() : "";
603
+ if (pageKey !== "" && pageKeys.includes(pageKey)) {
604
+ activePageKey.value = pageKey;
605
+ return;
606
+ }
607
+ if (pageKeys.length > 0 && (activePageKey.value === null || !pageKeys.includes(activePageKey.value))) {
608
+ activePageKey.value = pageKeys[0];
609
+ }
610
+ },
611
+ {
612
+ immediate: true
613
+ }
614
+ );
500
615
  watch(
501
616
  () => visiblePages.value.map((page) => page.page_key),
502
617
  (pageKeys) => {
@@ -527,17 +642,7 @@ const activePageIndex = computed(() => {
527
642
  return index;
528
643
  });
529
644
  const shouldShowProgress = computed(() => {
530
- return props.showProgress && visiblePages.value.length > 1;
531
- });
532
- const stepperItems = computed(() => {
533
- return visiblePages.value.map((page, index) => ({
534
- title: page.title !== "" ? page.title : t("renderer.pageTitle", { index: index + 1 }),
535
- description: typeof page.description === "string" && page.description.trim() !== "" ? page.description : void 0,
536
- value: index + 1
537
- }));
538
- });
539
- const pagedMode = computed(() => {
540
- return shouldShowProgress.value;
645
+ return props.showProgress && visiblePages.value.length > 1 && (!isPreviewMode.value || props.simulation);
541
646
  });
542
647
  const currentVisiblePage = computed(() => {
543
648
  if (visiblePages.value.length === 0) {
@@ -546,9 +651,57 @@ const currentVisiblePage = computed(() => {
546
651
  const page = visiblePages.value[activePageIndex.value];
547
652
  return page ?? null;
548
653
  });
654
+ const progressValue = computed(() => {
655
+ if (visiblePages.value.length === 0) {
656
+ return 0;
657
+ }
658
+ return (activePageIndex.value + 1) / visiblePages.value.length * 100;
659
+ });
660
+ const shouldShowNavigation = computed(() => {
661
+ return visiblePages.value.length > 1 && (!isPreviewMode.value || props.simulation);
662
+ });
663
+ function resolveNextPageKey() {
664
+ const currentKey = activePageKey.value;
665
+ if (currentKey === null) {
666
+ return null;
667
+ }
668
+ const currentDisplayIndex = displayPages.value.findIndex((page) => page.page_key === currentKey);
669
+ if (currentDisplayIndex < 0) {
670
+ return null;
671
+ }
672
+ const currentPage = displayPages.value[currentDisplayIndex];
673
+ const logic = pageLogicByKey.value[currentPage.page_key];
674
+ if (logic !== void 0) {
675
+ for (const rule of logic.rules) {
676
+ if (!evaluatePageLogicRule(rule, currentPage, (field) => formState.value[field.name])) {
677
+ continue;
678
+ }
679
+ const gotoTarget = resolvePageLogicJumpTarget(rule, currentDisplayIndex, displayPages.value.length);
680
+ if (gotoTarget !== null) {
681
+ const targetPage = displayPages.value[gotoTarget];
682
+ if (targetPage !== void 0 && isPageVisible(targetPage)) {
683
+ return targetPage.page_key;
684
+ }
685
+ }
686
+ }
687
+ }
688
+ for (let index = currentDisplayIndex + 1; index < displayPages.value.length; index += 1) {
689
+ const page = displayPages.value[index];
690
+ if (page !== void 0 && isPageVisible(page)) {
691
+ return page.page_key;
692
+ }
693
+ }
694
+ return null;
695
+ }
549
696
  const renderedPages = computed(() => {
550
- if (!pagedMode.value) {
551
- return visiblePages.value;
697
+ if (isPreviewMode.value && props.simulation) {
698
+ if (currentVisiblePage.value !== null) {
699
+ return [currentVisiblePage.value];
700
+ }
701
+ return [];
702
+ }
703
+ if (previewPage.value !== null) {
704
+ return [previewPage.value];
552
705
  }
553
706
  if (currentVisiblePage.value === null) {
554
707
  return [];
@@ -559,14 +712,21 @@ const canGoPrev = computed(() => {
559
712
  return activePageIndex.value > 0;
560
713
  });
561
714
  const canGoNext = computed(() => {
562
- return activePageIndex.value < visiblePages.value.length - 1;
715
+ return resolveNextPageKey() !== null;
563
716
  });
564
717
  const isLastVisiblePage = computed(() => {
565
- return visiblePages.value.length > 0 && activePageIndex.value === visiblePages.value.length - 1;
718
+ return resolveNextPageKey() === null;
566
719
  });
567
720
  const shouldShowErrorAlert = computed(() => {
568
721
  return props.showAlertOnError && rendererErrors.value.length > 0;
569
722
  });
723
+ function filterErrorsByFieldNames(errors, fieldNames) {
724
+ if (fieldNames.length === 0) {
725
+ return errors;
726
+ }
727
+ const fieldNameSet = new Set(fieldNames);
728
+ return errors.filter((error) => typeof error.name === "string" && fieldNameSet.has(error.name));
729
+ }
570
730
  const resolvedSubmitLabel = computed(() => {
571
731
  if (typeof props.submitLabel === "string" && props.submitLabel.trim() !== "") {
572
732
  return props.submitLabel;
@@ -595,6 +755,9 @@ function isFieldVisible(field) {
595
755
  return runtimeField.visible;
596
756
  }
597
757
  function isFieldRequired(field) {
758
+ if (field.type === "address") {
759
+ return addressFieldDefinitions(field).some((addressField) => addressField.visible && addressField.required);
760
+ }
598
761
  const runtimeField = runtimeConditions.value.fields[field.field_key];
599
762
  if (runtimeField === void 0) {
600
763
  return field.required === true;
@@ -618,23 +781,43 @@ function setActivePageIndex(index) {
618
781
  const page = visiblePages.value[safeIndex];
619
782
  activePageKey.value = page.page_key;
620
783
  }
621
- function onStepperModelUpdate(value) {
622
- if (typeof value !== "number") {
623
- return;
624
- }
625
- setActivePageIndex(value - 1);
626
- }
627
- function goToPrevPage() {
784
+ async function goToPrevPage() {
628
785
  if (!canGoPrev.value) {
629
786
  return;
630
787
  }
631
- setActivePageIndex(activePageIndex.value - 1);
788
+ isPageTransitioning.value = true;
789
+ try {
790
+ setActivePageIndex(activePageIndex.value - 1);
791
+ } finally {
792
+ await Promise.resolve();
793
+ isPageTransitioning.value = false;
794
+ }
632
795
  }
633
- function goToNextPage() {
634
- if (!canGoNext.value) {
635
- return;
796
+ async function goToNextPage() {
797
+ isPageTransitioning.value = true;
798
+ try {
799
+ const currentPage = currentVisiblePage.value;
800
+ if (currentPage !== null) {
801
+ const fieldNames = currentPage.fields.map((field) => field.name).filter((fieldName) => fieldName.trim() !== "");
802
+ if (fieldNames.length > 0) {
803
+ const isValid = await validateForm({
804
+ name: fieldNames,
805
+ nested: true
806
+ });
807
+ if (!isValid) {
808
+ return;
809
+ }
810
+ }
811
+ }
812
+ const nextPageKey = resolveNextPageKey();
813
+ if (nextPageKey === null) {
814
+ return;
815
+ }
816
+ setActivePage(nextPageKey);
817
+ } finally {
818
+ await Promise.resolve();
819
+ isPageTransitioning.value = false;
636
820
  }
637
- setActivePageIndex(activePageIndex.value + 1);
638
821
  }
639
822
  function resolvePageIndexByFieldName(fieldName) {
640
823
  const pageKey = pageKeyByFieldName.value[fieldName];
@@ -671,7 +854,18 @@ async function validateForm(options = {}) {
671
854
  return true;
672
855
  }
673
856
  try {
674
- await formInstance.validate(options);
857
+ const result = await formInstance.validate(options);
858
+ if (result === false) {
859
+ const requestedFieldNames = Array.isArray(options.name) ? options.name : typeof options.name === "string" ? [options.name] : [];
860
+ const errors = filterErrorsByFieldNames(formInstance.getErrors(), requestedFieldNames);
861
+ rendererErrors.value = errors.map((error) => ({
862
+ id: error.id,
863
+ name: error.name,
864
+ message: error.message
865
+ }));
866
+ navigateToFirstErrorPage(rendererErrors.value);
867
+ return false;
868
+ }
675
869
  rendererErrors.value = [];
676
870
  return true;
677
871
  } catch {
@@ -715,7 +909,7 @@ const shouldValidateFieldOnBlur = computed(() => {
715
909
  return usesExternalModel.value;
716
910
  });
717
911
  function onFieldBlur(fieldName) {
718
- if (!shouldValidateFieldOnBlur.value) {
912
+ if (!shouldValidateFieldOnBlur.value || isPageTransitioning.value) {
719
913
  return;
720
914
  }
721
915
  validateField(fieldName).catch(() => {
@@ -727,81 +921,33 @@ defineExpose({
727
921
  clearErrors,
728
922
  getErrors
729
923
  });
730
- function hasDateMethod(value) {
731
- if (value === null || Array.isArray(value) || typeof value !== "object") {
732
- return false;
733
- }
734
- return typeof value.toDate === "function";
735
- }
736
924
  function hasToStringMethod(value) {
737
925
  if (value === null || Array.isArray(value) || typeof value !== "object") {
738
926
  return false;
739
927
  }
740
928
  return typeof value.toString === "function";
741
929
  }
742
- function isRangeInput(value) {
743
- if (value === null || Array.isArray(value) || typeof value !== "object") {
744
- return false;
745
- }
746
- return "start" in value && "end" in value;
747
- }
748
930
  function isFileValue(value) {
749
931
  if (typeof File === "undefined") {
750
932
  return false;
751
933
  }
752
934
  return value instanceof File;
753
935
  }
754
- function stripDatetimeOffset(value) {
755
- if (value.endsWith("Z")) {
756
- return value.slice(0, -1);
757
- }
758
- const match = value.match(/^(.*)([+-]\d{2}:\d{2})$/);
759
- if (match === null) {
760
- return value;
761
- }
762
- return match[1];
763
- }
764
- function formatTwoDigits(value) {
765
- return String(value).padStart(2, "0");
766
- }
767
- function serializeDateWithOffset(date) {
768
- const year = date.getFullYear();
769
- const month = formatTwoDigits(date.getMonth() + 1);
770
- const day = formatTwoDigits(date.getDate());
771
- const hours = formatTwoDigits(date.getHours());
772
- const minutes = formatTwoDigits(date.getMinutes());
773
- const seconds = formatTwoDigits(date.getSeconds());
774
- const offsetMinutes = -date.getTimezoneOffset();
775
- const sign = offsetMinutes >= 0 ? "+" : "-";
776
- const absoluteOffset = Math.abs(offsetMinutes);
777
- const offsetHours = formatTwoDigits(Math.floor(absoluteOffset / 60));
778
- const offsetRemainder = formatTwoDigits(absoluteOffset % 60);
779
- return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${sign}${offsetHours}:${offsetRemainder}`;
780
- }
781
- function serializeDateAsUtc(date) {
782
- return date.toISOString().replace(".000Z", "Z");
783
- }
784
- function parseSingleDateValue(type, value) {
936
+ function parseSingleDateValue(mode, value) {
785
937
  try {
786
- if (type === "date") {
938
+ if (mode === "date") {
787
939
  return parseDate(value);
788
940
  }
789
- if (type === "time") {
941
+ if (mode === "time") {
790
942
  return parseTime(value);
791
943
  }
792
- if (type === "datetime") {
793
- if (value.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(value)) {
794
- return parseAbsoluteToLocal(value);
795
- }
796
- return parseDateTime(stripDatetimeOffset(value));
797
- }
798
944
  } catch {
799
945
  return null;
800
946
  }
801
947
  return null;
802
948
  }
803
- function serializeSingleDateValue(type, value, mode) {
804
- if (type === "date" || type === "time") {
949
+ function serializeSingleDateValue(mode, value) {
950
+ if (mode === "date" || mode === "time") {
805
951
  if (hasToStringMethod(value)) {
806
952
  return value.toString();
807
953
  }
@@ -810,60 +956,32 @@ function serializeSingleDateValue(type, value, mode) {
810
956
  }
811
957
  return null;
812
958
  }
813
- if (type === "datetime") {
814
- if (hasDateMethod(value)) {
815
- const date = value.toDate(getLocalTimeZone());
816
- return mode === "utc" ? serializeDateAsUtc(date) : serializeDateWithOffset(date);
817
- }
818
- if (typeof value === "string") {
819
- return value;
820
- }
821
- return null;
822
- }
823
959
  return null;
824
960
  }
825
- function parseRangeValue(type, value) {
826
- if (value === null || Array.isArray(value) || typeof value !== "object") {
827
- return null;
961
+ function choiceDisplayValue(field) {
962
+ if (field.display === "list" || field.display === "menu") {
963
+ return field.display;
828
964
  }
829
- const rangeValue = value;
830
- const startValue = rangeValue.start;
831
- const endValue = rangeValue.end;
832
- const parsedStart = typeof startValue === "string" ? parseSingleDateValue(type === "date_range" ? "date" : "datetime", startValue) : null;
833
- const parsedEnd = typeof endValue === "string" ? parseSingleDateValue(type === "date_range" ? "date" : "datetime", endValue) : null;
834
- return {
835
- start: parsedStart,
836
- end: parsedEnd
837
- };
838
- }
839
- function serializeRangeValue(type, value) {
840
- if (!isRangeInput(value)) {
841
- return {
842
- start: null,
843
- end: null
844
- };
965
+ if (field.type === "radio" || field.type === "checkbox_group") {
966
+ return "list";
845
967
  }
846
- const startValue = serializeSingleDateValue(type === "date_range" ? "date" : "datetime", value.start, props.datetimeMode);
847
- const endValue = serializeSingleDateValue(type === "date_range" ? "date" : "datetime", value.end, props.datetimeMode);
848
- return {
849
- start: startValue,
850
- end: endValue
851
- };
968
+ return "menu";
852
969
  }
853
970
  function normalizeSelectOptions(options) {
854
971
  if (options === void 0) {
855
972
  return [];
856
973
  }
857
974
  const items = [];
858
- for (const option of options) {
975
+ for (const [index, option] of options.entries()) {
859
976
  if (typeof option === "string" || typeof option === "number" || typeof option === "boolean" || option === null) {
977
+ const primitiveLabel = option === null ? "" : String(option);
860
978
  items.push({
861
- label: String(option ?? ""),
979
+ label: primitiveLabel.trim() === "" ? `Option ${index + 1}` : primitiveLabel,
862
980
  value: option
863
981
  });
864
982
  continue;
865
983
  }
866
- const label = typeof option.label === "string" ? option.label : String(option.value ?? "");
984
+ const label = typeof option.label === "string" && option.label.trim() !== "" ? option.label : `Option ${index + 1}`;
867
985
  items.push({
868
986
  label,
869
987
  value: option.value,
@@ -902,6 +1020,15 @@ function getResolvedFormFieldUi(field) {
902
1020
  function getFieldValue(field) {
903
1021
  return formState.value[field.name];
904
1022
  }
1023
+ function addressFieldDefinitions(field) {
1024
+ if (field.type !== "address") {
1025
+ return [];
1026
+ }
1027
+ if (!Array.isArray(field.address_fields) || field.address_fields.length === 0) {
1028
+ return createDefaultAddressFields(locale.value);
1029
+ }
1030
+ return field.address_fields;
1031
+ }
905
1032
  function setFieldValue(fieldName, value) {
906
1033
  formState.value = {
907
1034
  ...formState.value,
@@ -910,21 +1037,29 @@ function setFieldValue(fieldName, value) {
910
1037
  }
911
1038
  function getComponentModelValue(field) {
912
1039
  const value = getFieldValue(field);
913
- if (field.type === "date" || field.type === "time" || field.type === "datetime") {
1040
+ const temporalMode = resolveTemporalMode(field);
1041
+ if (field.type === "temporal" || field.type === "date" || field.type === "time") {
914
1042
  if (typeof value === "string") {
915
- return parseSingleDateValue(field.type, value);
1043
+ return parseSingleDateValue(temporalMode, value);
916
1044
  }
917
1045
  return null;
918
1046
  }
919
- if (field.type === "date_range" || field.type === "datetime_range") {
920
- return parseRangeValue(field.type, value);
921
- }
922
- if (field.type === "checkbox" || field.type === "switch") {
1047
+ if (field.type === "checkbox" || field.type === "consent" || field.type === "switch") {
923
1048
  return typeof value === "boolean" ? value : false;
924
1049
  }
925
1050
  if (field.type === "checkbox_group") {
926
1051
  return Array.isArray(value) ? value : [];
927
1052
  }
1053
+ if (field.type === "address") {
1054
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1055
+ return value;
1056
+ }
1057
+ const nextValue = {};
1058
+ for (const addressField of addressFieldDefinitions(field)) {
1059
+ nextValue[addressField.key] = null;
1060
+ }
1061
+ return nextValue;
1062
+ }
928
1063
  if (field.type === "file") {
929
1064
  if (field.multiple === true) {
930
1065
  if (Array.isArray(value)) {
@@ -965,14 +1100,15 @@ function getComponentProps(field, page) {
965
1100
  componentProps.max = field.max ?? void 0;
966
1101
  componentProps.step = field.step ?? void 0;
967
1102
  }
1103
+ if (field.type === "consent") {
1104
+ componentProps.label = field.consent_label ?? field.label ?? "";
1105
+ componentProps.required = false;
1106
+ }
968
1107
  if (field.type === "select" || field.type === "select_menu" || field.type === "radio" || field.type === "checkbox_group") {
969
1108
  componentProps.items = normalizeSelectOptions(field.options);
970
- }
971
- if (field.type === "date_range" || field.type === "datetime_range") {
972
- componentProps.range = true;
973
- }
974
- if (field.type === "datetime" || field.type === "datetime_range") {
975
- componentProps.granularity = "second";
1109
+ if (field.type === "checkbox_group" && choiceDisplayValue(field) === "menu") {
1110
+ componentProps.multiple = true;
1111
+ }
976
1112
  }
977
1113
  if (field.type === "file") {
978
1114
  componentProps.multiple = field.multiple === true;
@@ -987,12 +1123,9 @@ function getComponentProps(field, page) {
987
1123
  return componentProps;
988
1124
  }
989
1125
  function onFieldUpdate(field, value) {
990
- if (field.type === "date" || field.type === "time" || field.type === "datetime") {
991
- setFieldValue(field.name, serializeSingleDateValue(field.type, value, props.datetimeMode));
992
- return;
993
- }
994
- if (field.type === "date_range" || field.type === "datetime_range") {
995
- setFieldValue(field.name, serializeRangeValue(field.type, value));
1126
+ const temporalMode = resolveTemporalMode(field);
1127
+ if (field.type === "temporal" || field.type === "date" || field.type === "time") {
1128
+ setFieldValue(field.name, serializeSingleDateValue(temporalMode, value));
996
1129
  return;
997
1130
  }
998
1131
  setFieldValue(field.name, value);
@@ -1017,7 +1150,8 @@ async function onSubmit() {
1017
1150
  const response = await internalSubmit.submit({
1018
1151
  test: props.simulation,
1019
1152
  version: props.formVersion,
1020
- mode: props.uploadMode
1153
+ mode: props.uploadMode,
1154
+ meta: requiresSubmissionCode.value ? { submission_code: submissionCode.value } : submissionCode.value.trim() !== "" ? { submission_code: submissionCode.value } : void 0
1021
1155
  });
1022
1156
  submittedResponse.value = response;
1023
1157
  if (props.clearAfterSubmit) {
@@ -1058,20 +1192,10 @@ async function onSubmit() {
1058
1192
  @error="onFormError"
1059
1193
  >
1060
1194
  <div class="space-y-6">
1061
- <UStepper
1062
- v-if="shouldShowProgress && progressVariant === 'stepper'"
1063
- class="w-full"
1064
- :items="stepperItems"
1065
- :model-value="activePageIndex + 1"
1066
- :linear="false"
1067
- @update:model-value="onStepperModelUpdate"
1068
- />
1069
-
1070
1195
  <UProgress
1071
- v-if="shouldShowProgress && progressVariant === 'progress'"
1072
- :model-value="activePageIndex + 1"
1073
- :max="visiblePages.length"
1074
- status
1196
+ v-if="shouldShowProgress"
1197
+ :model-value="progressValue"
1198
+ :max="100"
1075
1199
  />
1076
1200
 
1077
1201
  <UAlert
@@ -1123,132 +1247,54 @@ async function onSubmit() {
1123
1247
  :title="t('renderer.alert.submitted')"
1124
1248
  />
1125
1249
 
1126
- <section
1250
+ <UAlert
1251
+ v-if="availabilityAlert !== null"
1252
+ color="warning"
1253
+ variant="soft"
1254
+ :title="availabilityAlert"
1255
+ />
1256
+
1257
+ <div
1258
+ v-if="requiresSubmissionCode"
1259
+ class="space-y-2 rounded-xl border border-muted bg-elevated/40 p-4"
1260
+ >
1261
+ <p class="text-sm font-medium text-default">
1262
+ {{ t("renderer.submissionCodeLabel") }}
1263
+ </p>
1264
+ <UInput
1265
+ v-model="submissionCode"
1266
+ type="password"
1267
+ autocomplete="one-time-code"
1268
+ :placeholder="t('renderer.submissionCodePlaceholder')"
1269
+ />
1270
+ </div>
1271
+
1272
+ <FormForgeRendererPage
1127
1273
  v-for="page in renderedPages"
1128
1274
  :key="page.page_key"
1275
+ :page="page"
1129
1276
  class="space-y-4"
1130
1277
  @focusin="setActivePage(page.page_key)"
1131
1278
  @pointerdown="setActivePage(page.page_key)"
1132
1279
  >
1133
- <div
1134
- v-if="page.title !== '' || typeof page.description === 'string' && page.description !== ''"
1135
- class="space-y-1"
1136
- >
1137
- <h3
1138
- v-if="page.title !== ''"
1139
- class="text-base font-semibold text-default"
1140
- >
1141
- {{ page.title }}
1142
- </h3>
1143
- <p
1144
- v-if="typeof page.description === 'string' && page.description !== ''"
1145
- class="text-sm text-muted"
1146
- >
1147
- {{ page.description }}
1148
- </p>
1149
- </div>
1150
-
1151
- <div class="space-y-4">
1152
- <UFormField
1153
- v-for="field in page.fields"
1154
- v-show="isFieldVisible(field)"
1155
- :key="field.field_key"
1156
- :name="field.name"
1157
- :label="field.label"
1158
- :help="field.help_text"
1159
- :required="isFieldRequired(field)"
1160
- :ui="getResolvedFormFieldUi(field)"
1161
- @focusout="() => onFieldBlur(field.name)"
1162
- >
1163
- <UInput
1164
- v-if="field.type === 'text' || field.type === 'email'"
1165
- :model-value="getLooseModelValue(field)"
1166
- v-bind="getComponentProps(field, page)"
1167
- @update:model-value="(nextValue) => onFieldModelUpdate(field, nextValue)"
1168
- />
1169
-
1170
- <UTextarea
1171
- v-else-if="field.type === 'textarea'"
1172
- :model-value="getLooseModelValue(field)"
1173
- v-bind="getComponentProps(field, page)"
1174
- @update:model-value="(nextValue) => onFieldModelUpdate(field, nextValue)"
1175
- />
1176
-
1177
- <UInputNumber
1178
- v-else-if="field.type === 'number'"
1179
- :model-value="getLooseModelValue(field)"
1180
- v-bind="getComponentProps(field, page)"
1181
- @update:model-value="(nextValue) => onFieldModelUpdate(field, nextValue)"
1182
- />
1183
-
1184
- <USelect
1185
- v-else-if="field.type === 'select'"
1186
- :model-value="getLooseModelValue(field)"
1187
- v-bind="getComponentProps(field, page)"
1188
- @update:model-value="(nextValue) => onFieldModelUpdate(field, nextValue)"
1189
- />
1190
-
1191
- <USelectMenu
1192
- v-else-if="field.type === 'select_menu'"
1193
- :model-value="getLooseModelValue(field)"
1194
- v-bind="getComponentProps(field, page)"
1195
- @update:model-value="(nextValue) => onFieldModelUpdate(field, nextValue)"
1196
- />
1197
-
1198
- <URadioGroup
1199
- v-else-if="field.type === 'radio'"
1200
- :model-value="getLooseModelValue(field)"
1201
- v-bind="getComponentProps(field, page)"
1202
- @update:model-value="(nextValue) => onFieldModelUpdate(field, nextValue)"
1203
- />
1204
-
1205
- <UCheckbox
1206
- v-else-if="field.type === 'checkbox'"
1207
- :model-value="getLooseModelValue(field)"
1208
- v-bind="getComponentProps(field, page)"
1209
- @update:model-value="(nextValue) => onFieldModelUpdate(field, nextValue)"
1210
- />
1211
-
1212
- <UCheckboxGroup
1213
- v-else-if="field.type === 'checkbox_group'"
1214
- :model-value="getLooseModelValue(field)"
1215
- v-bind="getComponentProps(field, page)"
1216
- @update:model-value="(nextValue) => onFieldModelUpdate(field, nextValue)"
1217
- />
1218
-
1219
- <USwitch
1220
- v-else-if="field.type === 'switch'"
1221
- :model-value="getLooseModelValue(field)"
1222
- v-bind="getComponentProps(field, page)"
1223
- @update:model-value="(nextValue) => onFieldModelUpdate(field, nextValue)"
1224
- />
1225
-
1226
- <UInputDate
1227
- v-else-if="field.type === 'date' || field.type === 'datetime' || field.type === 'date_range' || field.type === 'datetime_range'"
1228
- :model-value="getLooseModelValue(field)"
1229
- v-bind="getComponentProps(field, page)"
1230
- @update:model-value="(nextValue) => onFieldModelUpdate(field, nextValue)"
1231
- />
1232
-
1233
- <UInputTime
1234
- v-else-if="field.type === 'time'"
1235
- :model-value="getLooseModelValue(field)"
1236
- v-bind="getComponentProps(field, page)"
1237
- @update:model-value="(nextValue) => onFieldModelUpdate(field, nextValue)"
1238
- />
1239
-
1240
- <UFileUpload
1241
- v-else
1242
- :model-value="getLooseModelValue(field)"
1243
- v-bind="getComponentProps(field, page)"
1244
- @update:model-value="(nextValue) => onFieldModelUpdate(field, nextValue)"
1245
- />
1246
- </UFormField>
1247
- </div>
1248
- </section>
1280
+ <FormForgeRendererField
1281
+ v-for="field in page.fields"
1282
+ v-show="isFieldVisible(field)"
1283
+ :key="field.field_key"
1284
+ :field="field"
1285
+ :model-value="getLooseModelValue(field)"
1286
+ :component-props="getComponentProps(field, page)"
1287
+ :field-ui="getResolvedFormFieldUi(field)"
1288
+ :address-fields="addressFieldDefinitions(field)"
1289
+ :required="isFieldRequired(field)"
1290
+ :disabled="isFieldDisabled(field, page)"
1291
+ @update:model-value="(nextValue) => onFieldModelUpdate(field, nextValue)"
1292
+ @blur="() => onFieldBlur(field.name)"
1293
+ />
1294
+ </FormForgeRendererPage>
1249
1295
 
1250
1296
  <div
1251
- v-if="pagedMode"
1297
+ v-if="shouldShowNavigation"
1252
1298
  class="flex items-center justify-between gap-2"
1253
1299
  >
1254
1300
  <UButton
@@ -1271,23 +1317,23 @@ async function onSubmit() {
1271
1317
  </UButton>
1272
1318
 
1273
1319
  <UButton
1274
- v-else-if="!usesExternalModel && showSubmit"
1320
+ v-else-if="showSubmit"
1275
1321
  type="submit"
1276
1322
  :loading="internalSubmit.submitting.value"
1277
- :disabled="internalForm.loading.value || getResolvedSchema() === null"
1323
+ :disabled="internalForm.loading.value || getResolvedSchema() === null || availabilityAlert !== null || requiresSubmissionCode && submissionCode.trim() === ''"
1278
1324
  >
1279
1325
  {{ resolvedSubmitLabel }}
1280
1326
  </UButton>
1281
1327
  </div>
1282
1328
 
1283
1329
  <div
1284
- v-else-if="!usesExternalModel && showSubmit"
1330
+ v-else-if="showSubmit"
1285
1331
  class="flex justify-end"
1286
1332
  >
1287
1333
  <UButton
1288
1334
  type="submit"
1289
1335
  :loading="internalSubmit.submitting.value"
1290
- :disabled="internalForm.loading.value || getResolvedSchema() === null"
1336
+ :disabled="internalForm.loading.value || getResolvedSchema() === null || availabilityAlert !== null"
1291
1337
  >
1292
1338
  {{ resolvedSubmitLabel }}
1293
1339
  </UButton>
@@ -1295,3 +1341,7 @@ async function onSubmit() {
1295
1341
  </div>
1296
1342
  </UForm>
1297
1343
  </template>
1344
+
1345
+ <style scoped>
1346
+ .formforge-rich-text :deep(p){margin:.25rem 0}.formforge-rich-text :deep(p:first-child){margin-top:0}.formforge-rich-text :deep(p:last-child){margin-bottom:0}.formforge-rich-text :deep(ol),.formforge-rich-text :deep(ul){margin:.25rem 0;padding-left:1.25rem}.formforge-rich-text :deep(a){text-decoration:underline}
1347
+ </style>