@evanschleret/formforgeclient 1.0.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "1.0.0",
7
+ "version": "1.1.2",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -24,6 +24,15 @@ class FormForgeClientImpl {
24
24
  }
25
25
  return input;
26
26
  }
27
+ resolveBaseURLParams(input) {
28
+ if (input === void 0) {
29
+ return {};
30
+ }
31
+ if (typeof input === "function") {
32
+ return input();
33
+ }
34
+ return input;
35
+ }
27
36
  resolveNamedScope(name) {
28
37
  const scopedRoutes = this.config.scopedRoutes ?? {};
29
38
  const routeDefinition = scopedRoutes[name];
@@ -31,9 +40,10 @@ class FormForgeClientImpl {
31
40
  throw new Error(`Unknown FormForge scope "${name}"`);
32
41
  }
33
42
  const sourceParams = this.resolveScopeParams(this.config.scopeParams);
43
+ const baseURLParams = this.resolveBaseURLParams(this.config.baseURLParams);
34
44
  const params = {};
35
45
  for (const [scopeParam, sourceParam] of Object.entries(routeDefinition.paramsFromRoute)) {
36
- const value = sourceParams[sourceParam];
46
+ const value = sourceParams[sourceParam] ?? baseURLParams[sourceParam];
37
47
  if (value === void 0 || value === "") {
38
48
  throw new Error(`Missing scope param source "${sourceParam}" for named scope "${name}"`);
39
49
  }
@@ -58,12 +58,25 @@ function withMutationHeaders(options = {}) {
58
58
  function toJsonObject(input) {
59
59
  return JSON.parse(JSON.stringify(input));
60
60
  }
61
+ function shouldAutoPublish(input) {
62
+ return input.auto_publish === true || input.autoPublish === true;
63
+ }
64
+ function toManagementMutationPayload(input) {
65
+ const payload = toJsonObject(input);
66
+ if ("autoPublish" in payload) {
67
+ delete payload.autoPublish;
68
+ }
69
+ if (shouldAutoPublish(input)) {
70
+ payload.auto_publish = true;
71
+ }
72
+ return payload;
73
+ }
61
74
  export async function createFormForgeForm(http, input, options = {}) {
62
75
  const response = await http({
63
76
  path: resolveEndpointPath(options.endpoint, "/forms", {}, options.scope),
64
77
  method: "POST",
65
78
  headers: withMutationHeaders(options),
66
- json: toJsonObject(input)
79
+ json: toManagementMutationPayload(input)
67
80
  });
68
81
  return normalizeManagementForm(pickFormForgeDataEnvelope(response.data)) ?? {};
69
82
  }
@@ -86,7 +99,7 @@ export async function patchFormForgeForm(http, key, input, options = {}) {
86
99
  }, options.scope),
87
100
  method: "PATCH",
88
101
  headers: withMutationHeaders(options),
89
- json: toJsonObject(input)
102
+ json: toManagementMutationPayload(input)
90
103
  });
91
104
  return normalizeManagementForm(pickFormForgeDataEnvelope(response.data)) ?? {};
92
105
  }
@@ -19,6 +19,10 @@ export interface UseFormForgeBuilderOptions {
19
19
  client?: FormForgeClient;
20
20
  clientConfig?: FormForgeClientConfig;
21
21
  }
22
+ export interface FormForgeBuilderSaveOptions {
23
+ idempotencyKey?: string;
24
+ autoPublish?: boolean;
25
+ }
22
26
  export declare const FORM_FORGE_BUILDER_FIELD_TYPES: FormForgeFieldType[];
23
27
  export declare const FORM_FORGE_BUILDER_CONDITION_TARGET_TYPES: FormForgeConditionTargetType[];
24
28
  export declare const FORM_FORGE_BUILDER_CONDITION_ACTIONS: FormForgeConditionAction[];
@@ -48,7 +52,7 @@ export declare function useFormForgeBuilder(options?: UseFormForgeBuilderOptions
48
52
  addConditionClause: (conditionKey: string) => void;
49
53
  removeConditionClause: (conditionKey: string, index: number) => void;
50
54
  normalizeFieldLocations: () => void;
51
- save: (idempotencyKey?: string) => Promise<void>;
55
+ save: (saveOptions?: FormForgeBuilderSaveOptions | string) => Promise<void>;
52
56
  publish: (idempotencyKey?: string) => Promise<void>;
53
57
  unpublish: (idempotencyKey?: string) => Promise<void>;
54
58
  clearAutosave: () => void;
@@ -234,7 +234,7 @@ export function useFormForgeBuilder(options = {}) {
234
234
  }
235
235
  }
236
236
  }
237
- function toManagementInput() {
237
+ function toManagementInput(autoPublish = false) {
238
238
  normalizeFieldLocations();
239
239
  const pages = draft.value.pages;
240
240
  const fields = [];
@@ -251,16 +251,23 @@ export function useFormForgeBuilder(options = {}) {
251
251
  conditions: draft.value.conditions,
252
252
  drafts: draft.value.drafts
253
253
  };
254
+ if (autoPublish) {
255
+ input.auto_publish = true;
256
+ }
254
257
  return input;
255
258
  }
256
- async function save(idempotencyKey) {
259
+ async function save(saveOptions = {}) {
260
+ const resolvedSaveOptions = typeof saveOptions === "string" ? { idempotencyKey: saveOptions, autoPublish: false } : {
261
+ idempotencyKey: saveOptions.idempotencyKey,
262
+ autoPublish: saveOptions.autoPublish === true
263
+ };
257
264
  if (draft.value.title.trim() === "") {
258
265
  throw new Error("Title is required to save");
259
266
  }
260
267
  saving.value = true;
261
268
  error.value = null;
262
269
  try {
263
- const input = toManagementInput();
270
+ const input = toManagementInput(resolvedSaveOptions.autoPublish);
264
271
  const mutationIdentifier = resolveMutationIdentifier(draft.value);
265
272
  if (mutationIdentifier === null) {
266
273
  const hasExistingSlug = typeof draft.value.key === "string" && draft.value.key !== "";
@@ -268,7 +275,7 @@ export function useFormForgeBuilder(options = {}) {
268
275
  throw new Error("Form uuid is required for update");
269
276
  }
270
277
  const created = await client.createForm(input, {
271
- idempotencyKey,
278
+ idempotencyKey: resolvedSaveOptions.idempotencyKey,
272
279
  endpoint: options.endpoint,
273
280
  scope: options.scope
274
281
  });
@@ -283,7 +290,7 @@ export function useFormForgeBuilder(options = {}) {
283
290
  } else {
284
291
  const patchInput = input;
285
292
  const patched = await client.patchForm(mutationIdentifier, patchInput, {
286
- idempotencyKey,
293
+ idempotencyKey: resolvedSaveOptions.idempotencyKey,
287
294
  endpoint: options.endpoint,
288
295
  scope: options.scope
289
296
  });
@@ -32,7 +32,7 @@ function mergeRouteValues(primaryValues, secondaryValues) {
32
32
  return merged;
33
33
  }
34
34
  function templateSegmentKey(value) {
35
- const matched = value.match(/^\{([a-zA-Z0-9_]+)\}$/);
35
+ const matched = value.match(/^\{([a-zA-Z0-9_]+)(?::[^}]+)?\}$/);
36
36
  return matched?.[1] ?? null;
37
37
  }
38
38
  function toPathSegments(path) {
@@ -104,6 +104,19 @@ function withInferredMissingValues(base, inferred) {
104
104
  }
105
105
  return resolved;
106
106
  }
107
+ function inferScopeSourcesFromPath(scopedRoutes, currentPath) {
108
+ const inferredSources = {};
109
+ for (const scopedRoute of Object.values(scopedRoutes ?? {})) {
110
+ const inferredScopeParams = inferParamsFromPath(scopedRoute.prefix, currentPath);
111
+ for (const [scopeParam, sourceParam] of Object.entries(scopedRoute.paramsFromRoute)) {
112
+ const inferredValue = inferredScopeParams[scopeParam];
113
+ if (inferredValue !== void 0 && inferredValue !== "" && inferredSources[sourceParam] === void 0) {
114
+ inferredSources[sourceParam] = inferredValue;
115
+ }
116
+ }
117
+ }
118
+ return inferredSources;
119
+ }
107
120
  export function useFormForgeClient(config = {}) {
108
121
  const nuxtApp = useNuxtApp();
109
122
  const route = useRoute();
@@ -130,8 +143,13 @@ export function useFormForgeClient(config = {}) {
130
143
  ...appRouteParams
131
144
  }
132
145
  );
133
- const inferredFromPath = inferParamsFromPath(config.baseURL ?? runtimePublicConfig?.baseURL, composableRoutePath || appRoutePath);
134
- return withInferredMissingValues(mergedRouteValues, inferredFromPath);
146
+ const resolvedPath = composableRoutePath || appRoutePath;
147
+ const resolvedBaseURL = config.baseURL ?? runtimePublicConfig?.baseURL;
148
+ const resolvedScopedRoutes = config.scopedRoutes ?? runtimePublicConfig?.scopedRoutes;
149
+ const inferredFromBaseURL = inferParamsFromPath(resolvedBaseURL, resolvedPath);
150
+ const withBaseURLValues = withInferredMissingValues(mergedRouteValues, inferredFromBaseURL);
151
+ const inferredFromScopes = inferScopeSourcesFromPath(resolvedScopedRoutes, resolvedPath);
152
+ return withInferredMissingValues(withBaseURLValues, inferredFromScopes);
135
153
  };
136
154
  const baseConfig = runtimePublicConfig;
137
155
  const mergedConfig = {
@@ -84,6 +84,9 @@ declare const TRANSLATIONS: {
84
84
  readonly 'builder.error.publish': "Publish failed";
85
85
  readonly 'builder.error.unpublish': "Unpublish failed";
86
86
  readonly 'builder.error.categoryCreate': "Category creation failed";
87
+ readonly 'builder.toast.saveSuccess': "Form saved";
88
+ readonly 'builder.toast.publishSuccess': "Form published";
89
+ readonly 'builder.toast.unpublishSuccess': "Form unpublished";
87
90
  readonly 'builder.optionDefaultLabel': "Option";
88
91
  readonly 'builder.optionsCount': "{count} options";
89
92
  readonly 'builder.categoryModal.title': "Create category";
@@ -202,6 +205,9 @@ declare const TRANSLATIONS: {
202
205
  readonly 'builder.error.publish': "Échec de la publication";
203
206
  readonly 'builder.error.unpublish': "Échec de la dépublication";
204
207
  readonly 'builder.error.categoryCreate': "Échec de la création de catégorie";
208
+ readonly 'builder.toast.saveSuccess': "Formulaire sauvegardé";
209
+ readonly 'builder.toast.publishSuccess': "Formulaire publié";
210
+ readonly 'builder.toast.unpublishSuccess': "Formulaire dépublié";
205
211
  readonly 'builder.optionDefaultLabel': "Option";
206
212
  readonly 'builder.optionsCount': "{count} options";
207
213
  readonly 'builder.categoryModal.title': "Créer une catégorie";
@@ -80,6 +80,9 @@ const TRANSLATIONS = {
80
80
  "builder.error.publish": "Publish failed",
81
81
  "builder.error.unpublish": "Unpublish failed",
82
82
  "builder.error.categoryCreate": "Category creation failed",
83
+ "builder.toast.saveSuccess": "Form saved",
84
+ "builder.toast.publishSuccess": "Form published",
85
+ "builder.toast.unpublishSuccess": "Form unpublished",
83
86
  "builder.optionDefaultLabel": "Option",
84
87
  "builder.optionsCount": "{count} options",
85
88
  "builder.categoryModal.title": "Create category",
@@ -198,6 +201,9 @@ const TRANSLATIONS = {
198
201
  "builder.error.publish": "\xC9chec de la publication",
199
202
  "builder.error.unpublish": "\xC9chec de la d\xE9publication",
200
203
  "builder.error.categoryCreate": "\xC9chec de la cr\xE9ation de cat\xE9gorie",
204
+ "builder.toast.saveSuccess": "Formulaire sauvegard\xE9",
205
+ "builder.toast.publishSuccess": "Formulaire publi\xE9",
206
+ "builder.toast.unpublishSuccess": "Formulaire d\xE9publi\xE9",
201
207
  "builder.optionDefaultLabel": "Option",
202
208
  "builder.optionsCount": "{count} options",
203
209
  "builder.categoryModal.title": "Cr\xE9er une cat\xE9gorie",
@@ -29,7 +29,7 @@ function mergeRouteValues(primaryValues, secondaryValues) {
29
29
  return merged;
30
30
  }
31
31
  function templateSegmentKey(value) {
32
- const matched = value.match(/^\{([a-zA-Z0-9_]+)\}$/);
32
+ const matched = value.match(/^\{([a-zA-Z0-9_]+)(?::[^}]+)?\}$/);
33
33
  return matched?.[1] ?? null;
34
34
  }
35
35
  function toPathSegments(path) {
@@ -101,6 +101,19 @@ function withInferredMissingValues(base, inferred) {
101
101
  }
102
102
  return resolved;
103
103
  }
104
+ function inferScopeSourcesFromPath(scopedRoutes, currentPath) {
105
+ const inferredSources = {};
106
+ for (const scopedRoute of Object.values(scopedRoutes ?? {})) {
107
+ const inferredScopeParams = inferParamsFromPath(scopedRoute.prefix, currentPath);
108
+ for (const [scopeParam, sourceParam] of Object.entries(scopedRoute.paramsFromRoute)) {
109
+ const inferredValue = inferredScopeParams[scopeParam];
110
+ if (inferredValue !== void 0 && inferredValue !== "" && inferredSources[sourceParam] === void 0) {
111
+ inferredSources[sourceParam] = inferredValue;
112
+ }
113
+ }
114
+ }
115
+ return inferredSources;
116
+ }
104
117
  export default defineNuxtPlugin((nuxtApp) => {
105
118
  const runtimeConfig = useRuntimeConfig();
106
119
  const route = useRoute();
@@ -123,8 +136,11 @@ export default defineNuxtPlugin((nuxtApp) => {
123
136
  ...appRouteParams
124
137
  }
125
138
  );
126
- const inferredFromPath = inferParamsFromPath(publicConfig?.baseURL, composableRoutePath || appRoutePath);
127
- return withInferredMissingValues(mergedRouteValues, inferredFromPath);
139
+ const resolvedPath = composableRoutePath || appRoutePath;
140
+ const inferredFromBaseURL = inferParamsFromPath(publicConfig?.baseURL, resolvedPath);
141
+ const withBaseURLValues = withInferredMissingValues(mergedRouteValues, inferredFromBaseURL);
142
+ const inferredFromScopes = inferScopeSourcesFromPath(publicConfig?.scopedRoutes, resolvedPath);
143
+ return withInferredMissingValues(withBaseURLValues, inferredFromScopes);
128
144
  };
129
145
  const runtimeBaseURLParams = publicConfig?.baseURLParams;
130
146
  const runtimeScopeParams = publicConfig?.scopeParams;
@@ -10,6 +10,10 @@ interface Props {
10
10
  autosave?: boolean;
11
11
  autosaveDelay?: number;
12
12
  readonly?: boolean;
13
+ disableTitleInput?: boolean;
14
+ disableCategoryControl?: boolean;
15
+ disablePublishAction?: boolean;
16
+ defaultPublished?: boolean;
13
17
  }
14
18
  declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
15
19
  error: (value: string) => any;
@@ -34,6 +38,10 @@ declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {},
34
38
  modelValue: Partial<FormForgeBuilderDraft>;
35
39
  autosave: boolean;
36
40
  autosaveDelay: number;
41
+ disableTitleInput: boolean;
42
+ disableCategoryControl: boolean;
43
+ disablePublishAction: boolean;
44
+ defaultPublished: boolean;
37
45
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
46
  declare const _default: typeof __VLS_export;
39
47
  export default _default;
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "#imports";
3
3
  import { useOverlay } from "@nuxt/ui/composables/useOverlay";
4
+ import { useToast } from "@nuxt/ui/composables/useToast";
4
5
  import Draggable from "vuedraggable";
5
6
  import {
6
7
  FORM_FORGE_BUILDER_CONDITION_ACTIONS,
@@ -22,7 +23,11 @@ const props = defineProps({
22
23
  modelValue: { type: Object, required: false, default: void 0 },
23
24
  autosave: { type: Boolean, required: false, default: true },
24
25
  autosaveDelay: { type: Number, required: false, default: 5e3 },
25
- readonly: { type: Boolean, required: false, default: false }
26
+ readonly: { type: Boolean, required: false, default: false },
27
+ disableTitleInput: { type: Boolean, required: false, default: false },
28
+ disableCategoryControl: { type: Boolean, required: false, default: false },
29
+ disablePublishAction: { type: Boolean, required: false, default: false },
30
+ defaultPublished: { type: Boolean, required: false, default: false }
26
31
  });
27
32
  const emit = defineEmits(["update:modelValue", "save", "publish", "unpublish", "error"]);
28
33
  const builderOptions = {
@@ -37,6 +42,7 @@ const builder = useFormForgeBuilder(builderOptions);
37
42
  const { t } = useFormForgeI18n({
38
43
  locale: () => props.locale
39
44
  });
45
+ const toast = useToast();
40
46
  const draft = builder.draft;
41
47
  const isClientReady = ref(false);
42
48
  const saving = builder.saving;
@@ -46,7 +52,7 @@ const lastSavedAt = builder.lastSavedAt;
46
52
  const builderError = builder.error;
47
53
  const overlay = useOverlay();
48
54
  const categoryManager = useFormForgeCategory({
49
- immediate: true,
55
+ immediate: !props.disableCategoryControl,
50
56
  initialQuery: {
51
57
  per_page: 200
52
58
  },
@@ -63,6 +69,7 @@ const builderColumnElement = ref(null);
63
69
  const railTop = ref(120);
64
70
  const railLeft = ref(0);
65
71
  const loadingRemoteForm = ref(false);
72
+ const isPublished = ref(props.defaultPublished);
66
73
  let loadRequestId = 0;
67
74
  const safePages = computed(() => {
68
75
  const pages = draft.value?.pages;
@@ -143,6 +150,9 @@ const draftMutationIdentifier = computed(() => {
143
150
  }
144
151
  return null;
145
152
  });
153
+ const showTopControls = computed(() => {
154
+ return !props.disableTitleInput || !props.disableCategoryControl;
155
+ });
146
156
  function twoDigits(value) {
147
157
  return String(value).padStart(2, "0");
148
158
  }
@@ -438,6 +448,7 @@ function applyLoadedForm(schema) {
438
448
  conditions: cloneValue(schema.conditions),
439
449
  drafts: cloneValue(schema.drafts)
440
450
  };
451
+ isPublished.value = schema.is_published === true;
441
452
  }
442
453
  async function loadFormIntoBuilder(key, version) {
443
454
  const requestId = ++loadRequestId;
@@ -533,8 +544,22 @@ function setOptionLabel(field, optionIndex, value) {
533
544
  }
534
545
  async function save() {
535
546
  try {
536
- await builder.save();
547
+ const shouldAutoPublish = props.defaultPublished;
548
+ await builder.save({
549
+ autoPublish: shouldAutoPublish
550
+ });
551
+ if (shouldAutoPublish) {
552
+ const wasPublished = isPublished.value;
553
+ isPublished.value = true;
554
+ if (!wasPublished) {
555
+ emit("publish", draft.value);
556
+ }
557
+ }
537
558
  emit("save", draft.value);
559
+ toast.add({
560
+ title: t("builder.toast.saveSuccess"),
561
+ color: "success"
562
+ });
538
563
  } catch (caughtError) {
539
564
  const message = caughtError instanceof Error ? caughtError.message : t("builder.error.save");
540
565
  emit("error", message);
@@ -543,7 +568,12 @@ async function save() {
543
568
  async function publish() {
544
569
  try {
545
570
  await builder.publish();
571
+ isPublished.value = true;
546
572
  emit("publish", draft.value);
573
+ toast.add({
574
+ title: t("builder.toast.publishSuccess"),
575
+ color: "success"
576
+ });
547
577
  } catch (caughtError) {
548
578
  const message = caughtError instanceof Error ? caughtError.message : t("builder.error.publish");
549
579
  emit("error", message);
@@ -552,12 +582,63 @@ async function publish() {
552
582
  async function unpublish() {
553
583
  try {
554
584
  await builder.unpublish();
585
+ isPublished.value = false;
555
586
  emit("unpublish", draft.value);
587
+ toast.add({
588
+ title: t("builder.toast.unpublishSuccess"),
589
+ color: "success"
590
+ });
556
591
  } catch (caughtError) {
557
592
  const message = caughtError instanceof Error ? caughtError.message : t("builder.error.unpublish");
558
593
  emit("error", message);
559
594
  }
560
595
  }
596
+ const publishButtonLabel = computed(() => {
597
+ if (props.defaultPublished) {
598
+ return t("builder.publish");
599
+ }
600
+ return isPublished.value ? t("builder.unpublish") : t("builder.publish");
601
+ });
602
+ const publishButtonColor = computed(() => {
603
+ if (props.defaultPublished) {
604
+ return "primary";
605
+ }
606
+ return isPublished.value ? "neutral" : "primary";
607
+ });
608
+ const publishButtonVariant = computed(() => {
609
+ if (props.defaultPublished) {
610
+ return "solid";
611
+ }
612
+ return isPublished.value ? "soft" : "solid";
613
+ });
614
+ const canTogglePublish = computed(() => {
615
+ if (props.readonly) {
616
+ return false;
617
+ }
618
+ if (props.defaultPublished && isPublished.value) {
619
+ return false;
620
+ }
621
+ if (draftMutationIdentifier.value === null) {
622
+ return false;
623
+ }
624
+ if (!isPublished.value && !publishable.value) {
625
+ return false;
626
+ }
627
+ return true;
628
+ });
629
+ const toolbarActionsClass = computed(() => {
630
+ if (showTopControls.value) {
631
+ return ["builder-toolbar-actions"];
632
+ }
633
+ return ["builder-toolbar-actions", "builder-toolbar-actions--compact"];
634
+ });
635
+ async function togglePublishState() {
636
+ if (!props.defaultPublished && isPublished.value) {
637
+ await unpublish();
638
+ return;
639
+ }
640
+ await publish();
641
+ }
561
642
  function removeField(page, fieldKey) {
562
643
  builder.removeField(page.page_key, fieldKey);
563
644
  if (selectedFieldKey.value === fieldKey) {
@@ -609,6 +690,31 @@ function duplicateField(page, fieldKey) {
609
690
  builder.duplicateField(page.page_key, fieldKey);
610
691
  builder.normalizeFieldLocations();
611
692
  }
693
+ watch(() => props.modelValue, (value) => {
694
+ if (value === null || value === void 0 || typeof value !== "object") {
695
+ isPublished.value = props.defaultPublished;
696
+ return;
697
+ }
698
+ const candidate = value;
699
+ if (candidate.is_published === true) {
700
+ isPublished.value = true;
701
+ return;
702
+ }
703
+ if (candidate.is_published === false) {
704
+ isPublished.value = false;
705
+ return;
706
+ }
707
+ isPublished.value = props.defaultPublished;
708
+ }, {
709
+ immediate: true,
710
+ deep: false
711
+ });
712
+ watch(() => props.defaultPublished, (value) => {
713
+ if (typeof props.modelValue === "object" && props.modelValue !== null && "is_published" in props.modelValue) {
714
+ return;
715
+ }
716
+ isPublished.value = value;
717
+ });
612
718
  watch(() => draft.value, (value) => {
613
719
  emit("update:modelValue", value);
614
720
  }, {
@@ -667,13 +773,20 @@ watch(() => selectedFieldKey.value, () => {
667
773
  class="builder-card"
668
774
  >
669
775
  <div class="builder-toolbar">
670
- <div class="builder-toolbar-grid">
776
+ <div
777
+ v-if="showTopControls"
778
+ class="builder-toolbar-grid"
779
+ >
671
780
  <UInput
781
+ v-if="!disableTitleInput"
672
782
  v-model="draftTitle"
673
783
  :disabled="readonly"
674
784
  :placeholder="t('builder.formTitlePlaceholder')"
675
785
  />
676
- <div class="builder-category-control">
786
+ <div
787
+ v-if="!disableCategoryControl"
788
+ class="builder-category-control"
789
+ >
677
790
  <USelect
678
791
  v-model="draftCategorySelectValue"
679
792
  :items="categorySelectItems"
@@ -692,7 +805,7 @@ watch(() => selectedFieldKey.value, () => {
692
805
  </div>
693
806
  </div>
694
807
 
695
- <div class="builder-toolbar-actions">
808
+ <div :class="toolbarActionsClass">
696
809
  <div class="builder-status">
697
810
  <span v-if="loadingRemoteForm">{{ t("builder.loadingForm") }}</span>
698
811
  <span v-if="formattedLastSavedAt !== null">{{ t("builder.lastSave", { value: formattedLastSavedAt }) }}</span>
@@ -711,21 +824,14 @@ watch(() => selectedFieldKey.value, () => {
711
824
  {{ t("builder.save") }}
712
825
  </UButton>
713
826
  <UButton
714
- color="primary"
715
- :loading="publishing"
716
- :disabled="readonly || !publishable"
717
- @click="publish"
718
- >
719
- {{ t("builder.publish") }}
720
- </UButton>
721
- <UButton
722
- color="neutral"
723
- variant="soft"
827
+ v-if="!disablePublishAction && !defaultPublished"
828
+ :color="publishButtonColor"
829
+ :variant="publishButtonVariant"
724
830
  :loading="publishing"
725
- :disabled="readonly || draftMutationIdentifier === null"
726
- @click="unpublish"
831
+ :disabled="!canTogglePublish"
832
+ @click="togglePublishState"
727
833
  >
728
- {{ t("builder.unpublish") }}
834
+ {{ publishButtonLabel }}
729
835
  </UButton>
730
836
  </div>
731
837
  </div>
@@ -1155,5 +1261,5 @@ watch(() => selectedFieldKey.value, () => {
1155
1261
  </template>
1156
1262
 
1157
1263
  <style scoped>
1158
- .builder-root{width:100%}.builder-layout{align-items:start;display:grid;gap:1rem;grid-template-columns:minmax(0,1fr) auto;width:100%}.builder-column{display:grid;gap:1.5rem;min-width:0;width:100%}.builder-card{border-radius:16px}.builder-toolbar{display:grid;gap:1rem}.builder-toolbar-grid{display:grid;gap:.85rem;grid-template-columns:1fr}.builder-category-control{align-items:center;display:grid;gap:.5rem;grid-template-columns:minmax(0,1fr) auto}.builder-toolbar-actions{align-items:center;border-top:1px solid var(--ui-border-muted);display:flex;flex-wrap:wrap;gap:.75rem;justify-content:space-between;padding-top:.85rem}.builder-status{color:var(--ui-text-muted);font-size:.82rem}.builder-error{color:var(--ui-error);margin-left:.5rem}.builder-actions{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem}.builder-stack{display:grid;gap:1rem}.pages-stack{gap:2.25rem}.drag-handle{color:var(--ui-text-muted);cursor:grab;padding-top:.4rem;-webkit-user-select:none;-moz-user-select:none;user-select:none}.page-block{display:grid;gap:1rem}.page-meta-card{border-radius:16px}.page-chip-row{margin-bottom:.65rem}.page-header{align-items:start;display:grid;gap:.75rem;grid-template-columns:auto minmax(0,1fr) auto}.page-header-main{display:grid;gap:.6rem;min-width:0}.page-actions{align-items:center;display:inline-flex;gap:.25rem}.page-questions-stack{gap:1rem}.field-card{border-left:4px solid transparent;border-radius:14px;cursor:pointer}.field-card--active{border-left-color:var(--ui-primary)}.field-shell{display:grid;gap:.85rem}.field-head{align-items:center;display:grid;gap:.75rem;grid-template-columns:auto minmax(0,1fr) minmax(220px,280px)}.field-controls{align-items:center;display:flex;flex-wrap:wrap;gap:.35rem}.field-required-switch{align-items:center;color:var(--ui-text-toned);display:inline-flex;font-size:.82rem;gap:.5rem;margin-left:auto}.field-preview{color:var(--ui-text-muted);font-size:.84rem;margin:0}.field-inline-preview{background:var(--ui-bg-muted);border-radius:10px;display:grid;gap:.5rem;padding:.75rem}.field-inline-preview-label{color:var(--ui-text-muted);font-size:.8rem;margin:0}.field-inline-preview-line{border-bottom:1px dashed var(--ui-border-accented);height:1.4rem;width:min(32rem,100%)}.field-inline-preview-textarea{display:grid;gap:.5rem;width:min(32rem,100%)}.field-inline-preview-textarea>span{border-bottom:1px dashed var(--ui-border-accented);display:block;height:1.2rem}.field-advanced{border-top:1px solid var(--ui-border-muted);padding-top:.75rem}.field-advanced-summary{color:var(--ui-text-toned);cursor:pointer;font-size:.82rem;font-weight:500}.field-advanced-body{background:var(--ui-bg-elevated);border-radius:12px;display:grid;gap:.75rem;margin-top:.75rem;padding:.75rem}.field-advanced-grid{display:grid;gap:.65rem}.field-toggle-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.field-options,.field-toggle-grid{display:grid;gap:.6rem}.field-option-row{align-items:center;display:grid;gap:.45rem;grid-template-columns:minmax(0,1fr) auto}.field-numbers{grid-template-columns:repeat(3,minmax(0,1fr))}.field-file,.field-numbers{display:grid;gap:.55rem}.conditions-card{border-radius:16px}.builder-row{align-items:center;display:flex;gap:.75rem;justify-content:space-between}.conditions-title{font-size:.95rem;font-weight:600}.condition-card{border:1px solid var(--ui-border-muted);border-radius:12px;display:grid;gap:.75rem;padding:.75rem}.condition-actions{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem}.builder-rail-column{display:flex;justify-content:center;width:4.25rem}.builder-rail{background:var(--ui-bg);border:1px solid var(--ui-border-muted);border-radius:14px;box-shadow:0 8px 24px color-mix(in srgb,var(--ui-text) 8%,transparent);display:flex;flex-direction:column;gap:.5rem;padding:.5rem;position:fixed;transition:top .12s ease,left .12s ease;z-index:20}.rail-action{height:2.75rem;justify-content:center;width:2.75rem}@media (min-width:1024px){.builder-toolbar-grid{grid-template-columns:1.2fr .8fr}}@media (max-width:1024px){.builder-layout{display:block}.builder-rail{bottom:.85rem;flex-direction:row;left:auto!important;position:fixed;right:.85rem;top:auto!important}}@media (max-width:768px){.page-header{grid-template-columns:minmax(0,1fr) auto}.page-drag-handle{display:none}.field-head{grid-template-columns:minmax(0,1fr)}.field-drag-handle{display:none}.field-numbers,.field-toggle-grid{grid-template-columns:1fr}.field-required-switch{margin-left:0}}
1264
+ .builder-root{width:100%}.builder-layout{align-items:start;display:grid;gap:1rem;grid-template-columns:minmax(0,1fr) auto;width:100%}.builder-column{display:grid;gap:1.5rem;min-width:0;width:100%}.builder-card{border-radius:16px}.builder-toolbar{display:grid;gap:1rem}.builder-toolbar-grid{display:grid;gap:.85rem;grid-template-columns:1fr}.builder-category-control{align-items:center;display:grid;gap:.5rem;grid-template-columns:minmax(0,1fr) auto}.builder-toolbar-actions{align-items:center;border-top:1px solid var(--ui-border-muted);display:flex;flex-wrap:wrap;gap:.75rem;justify-content:space-between;padding-top:.85rem}.builder-toolbar-actions--compact{border-top:none;padding-top:0}.builder-status{color:var(--ui-text-muted);font-size:.82rem}.builder-error{color:var(--ui-error);margin-left:.5rem}.builder-actions{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem}.builder-stack{display:grid;gap:1rem}.pages-stack{gap:2.25rem}.drag-handle{color:var(--ui-text-muted);cursor:grab;padding-top:.4rem;-webkit-user-select:none;-moz-user-select:none;user-select:none}.page-block{display:grid;gap:1rem}.page-meta-card{border-radius:16px}.page-chip-row{margin-bottom:.65rem}.page-header{align-items:start;display:grid;gap:.75rem;grid-template-columns:auto minmax(0,1fr) auto}.page-header-main{display:grid;gap:.6rem;min-width:0}.page-actions{align-items:center;display:inline-flex;gap:.25rem}.page-questions-stack{gap:1rem}.field-card{border-left:4px solid transparent;border-radius:14px;cursor:pointer}.field-card--active{border-left-color:var(--ui-primary)}.field-shell{display:grid;gap:.85rem}.field-head{align-items:center;display:grid;gap:.75rem;grid-template-columns:auto minmax(0,1fr) minmax(220px,280px)}.field-controls{align-items:center;display:flex;flex-wrap:wrap;gap:.35rem}.field-required-switch{align-items:center;color:var(--ui-text-toned);display:inline-flex;font-size:.82rem;gap:.5rem;margin-left:auto}.field-preview{color:var(--ui-text-muted);font-size:.84rem;margin:0}.field-inline-preview{background:var(--ui-bg-muted);border-radius:10px;display:grid;gap:.5rem;padding:.75rem}.field-inline-preview-label{color:var(--ui-text-muted);font-size:.8rem;margin:0}.field-inline-preview-line{border-bottom:1px dashed var(--ui-border-accented);height:1.4rem;width:min(32rem,100%)}.field-inline-preview-textarea{display:grid;gap:.5rem;width:min(32rem,100%)}.field-inline-preview-textarea>span{border-bottom:1px dashed var(--ui-border-accented);display:block;height:1.2rem}.field-advanced{border-top:1px solid var(--ui-border-muted);padding-top:.75rem}.field-advanced-summary{color:var(--ui-text-toned);cursor:pointer;font-size:.82rem;font-weight:500}.field-advanced-body{background:var(--ui-bg-elevated);border-radius:12px;display:grid;gap:.75rem;margin-top:.75rem;padding:.75rem}.field-advanced-grid{display:grid;gap:.65rem}.field-toggle-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.field-options,.field-toggle-grid{display:grid;gap:.6rem}.field-option-row{align-items:center;display:grid;gap:.45rem;grid-template-columns:minmax(0,1fr) auto}.field-numbers{grid-template-columns:repeat(3,minmax(0,1fr))}.field-file,.field-numbers{display:grid;gap:.55rem}.conditions-card{border-radius:16px}.builder-row{align-items:center;display:flex;gap:.75rem;justify-content:space-between}.conditions-title{font-size:.95rem;font-weight:600}.condition-card{border:1px solid var(--ui-border-muted);border-radius:12px;display:grid;gap:.75rem;padding:.75rem}.condition-actions{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem}.builder-rail-column{display:flex;justify-content:center;width:4.25rem}.builder-rail{background:var(--ui-bg);border:1px solid var(--ui-border-muted);border-radius:14px;box-shadow:0 8px 24px color-mix(in srgb,var(--ui-text) 8%,transparent);display:flex;flex-direction:column;gap:.5rem;padding:.5rem;position:fixed;transition:top .12s ease,left .12s ease;z-index:20}.rail-action{height:2.75rem;justify-content:center;width:2.75rem}@media (min-width:1024px){.builder-toolbar-grid{grid-template-columns:1.2fr .8fr}}@media (max-width:1024px){.builder-layout{display:block}.builder-rail{bottom:.85rem;flex-direction:row;left:auto!important;position:fixed;right:.85rem;top:auto!important}}@media (max-width:768px){.page-header{grid-template-columns:minmax(0,1fr) auto}.page-drag-handle{display:none}.field-head{grid-template-columns:minmax(0,1fr)}.field-drag-handle{display:none}.field-numbers,.field-toggle-grid{grid-template-columns:1fr}.field-required-switch{margin-left:0}}
1159
1265
  </style>
@@ -10,6 +10,10 @@ interface Props {
10
10
  autosave?: boolean;
11
11
  autosaveDelay?: number;
12
12
  readonly?: boolean;
13
+ disableTitleInput?: boolean;
14
+ disableCategoryControl?: boolean;
15
+ disablePublishAction?: boolean;
16
+ defaultPublished?: boolean;
13
17
  }
14
18
  declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
15
19
  error: (value: string) => any;
@@ -34,6 +38,10 @@ declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {},
34
38
  modelValue: Partial<FormForgeBuilderDraft>;
35
39
  autosave: boolean;
36
40
  autosaveDelay: number;
41
+ disableTitleInput: boolean;
42
+ disableCategoryControl: boolean;
43
+ disablePublishAction: boolean;
44
+ defaultPublished: boolean;
37
45
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
38
46
  declare const _default: typeof __VLS_export;
39
47
  export default _default;
@@ -5,6 +5,12 @@ interface FormForgeValidationError {
5
5
  }
6
6
  type FormForgeValidationHandler = (state: FormForgeSubmissionPayload) => FormForgeValidationError[] | Promise<FormForgeValidationError[]>;
7
7
  type FormForgeProgressVariant = 'stepper' | 'progress';
8
+ type FormForgeValidateEvent = 'input' | 'change' | 'blur';
9
+ interface FormForgeExposedError {
10
+ id?: string;
11
+ name?: string;
12
+ message: string;
13
+ }
8
14
  interface Props {
9
15
  schema?: FormForgeFormSchema | {
10
16
  value: FormForgeFormSchema | null;
@@ -30,8 +36,25 @@ interface Props {
30
36
  showProgress?: boolean;
31
37
  progressVariant?: FormForgeProgressVariant;
32
38
  showAlertOnError?: boolean;
39
+ validateOn?: FormForgeValidateEvent[];
40
+ validateOnBlur?: boolean;
33
41
  }
34
- declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
42
+ type FormForgeValidateOptions = {
43
+ name?: string | string[];
44
+ silent?: boolean;
45
+ nested?: boolean;
46
+ transform?: boolean;
47
+ };
48
+ declare function validateForm(options?: FormForgeValidateOptions): Promise<boolean>;
49
+ declare function validateField(name: string): Promise<boolean>;
50
+ declare function clearErrors(path?: string | RegExp): void;
51
+ declare function getErrors(path?: string | RegExp): FormForgeExposedError[];
52
+ declare const __VLS_export: import("vue").DefineComponent<Props, {
53
+ validate: typeof validateForm;
54
+ validateField: typeof validateField;
55
+ clearErrors: typeof clearErrors;
56
+ getErrors: typeof getErrors;
57
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
35
58
  submit: (value: FormForgeSubmissionPayload) => any;
36
59
  error: (value: string) => any;
37
60
  "update:modelValue": (value: FormForgeSubmissionPayload) => any;
@@ -66,6 +89,8 @@ declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {},
66
89
  showProgress: boolean;
67
90
  progressVariant: FormForgeProgressVariant;
68
91
  showAlertOnError: boolean;
92
+ validateOn: FormForgeValidateEvent[];
93
+ validateOnBlur: boolean;
69
94
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
70
95
  declare const _default: typeof __VLS_export;
71
96
  export default _default;
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed, ref, watch } from "#imports";
2
+ import { computed, ref, useTemplateRef, watch } from "#imports";
3
3
  import { getLocalTimeZone, parseAbsoluteToLocal, parseDate, parseDateTime, parseTime } from "@internationalized/date";
4
4
  import UCheckbox from "@nuxt/ui/components/Checkbox.vue";
5
5
  import UCheckboxGroup from "@nuxt/ui/components/CheckboxGroup.vue";
@@ -37,12 +37,15 @@ const props = defineProps({
37
37
  clearAfterSubmit: { type: Boolean, required: false, default: false },
38
38
  showProgress: { type: Boolean, required: false, default: false },
39
39
  progressVariant: { type: String, required: false, default: "stepper" },
40
- showAlertOnError: { type: Boolean, required: false, default: false }
40
+ showAlertOnError: { type: Boolean, required: false, default: false },
41
+ validateOn: { type: Array, required: false, default: void 0 },
42
+ validateOnBlur: { type: Boolean, required: false, default: void 0 }
41
43
  });
42
44
  const { t } = useFormForgeI18n({
43
45
  locale: () => props.clientConfig?.locale
44
46
  });
45
47
  const emit = defineEmits(["update:modelValue", "submit", "submitted", "error"]);
48
+ const rendererForm = useTemplateRef("rendererForm");
46
49
  function isRefLike(value) {
47
50
  if (typeof value !== "object" || value === null) {
48
51
  return false;
@@ -79,8 +82,11 @@ function unwrapZodSchemaProp(value) {
79
82
  }
80
83
  return value;
81
84
  }
82
- const usesExternalState = computed(() => {
83
- return props.schema !== void 0 && props.modelValue !== void 0;
85
+ const usesExternalModel = computed(() => {
86
+ return props.modelValue !== void 0;
87
+ });
88
+ const usesExternalSchema = computed(() => {
89
+ return props.schema !== void 0;
84
90
  });
85
91
  const internalFormKey = computed(() => {
86
92
  if (props.formKey === void 0 || props.formKey.trim() === "") {
@@ -106,10 +112,10 @@ const internalSubmit = useFormForgeSubmit({
106
112
  const submittedResponse = ref(null);
107
113
  const rendererErrors = ref([]);
108
114
  watch(
109
- () => [usesExternalState.value, internalFormKey.value, props.formVersion],
110
- async ([externalState, formKey]) => {
115
+ () => [usesExternalSchema.value, internalFormKey.value, props.formVersion],
116
+ async ([externalSchema, formKey]) => {
111
117
  submittedResponse.value = null;
112
- if (externalState || formKey === "") {
118
+ if (externalSchema || formKey === "") {
113
119
  return;
114
120
  }
115
121
  await internalForm.fetchSchema().catch(() => {
@@ -120,33 +126,93 @@ watch(
120
126
  }
121
127
  );
122
128
  function getResolvedSchema() {
123
- if (usesExternalState.value) {
129
+ if (usesExternalSchema.value) {
124
130
  return unwrapSchemaProp(props.schema);
125
131
  }
126
132
  return internalForm.schema.value;
127
133
  }
128
134
  function getResolvedZodSchema() {
129
- if (usesExternalState.value) {
135
+ if (usesExternalSchema.value) {
130
136
  return unwrapZodSchemaProp(props.zodSchema);
131
137
  }
132
138
  return internalForm.zodSchema.value;
133
139
  }
140
+ function resolveSchemaFieldNames(schema) {
141
+ const names = /* @__PURE__ */ new Set();
142
+ if (Array.isArray(schema.fields)) {
143
+ for (const field of schema.fields) {
144
+ if (typeof field.name === "string" && field.name !== "") {
145
+ names.add(field.name);
146
+ }
147
+ }
148
+ }
149
+ if (Array.isArray(schema.pages)) {
150
+ for (const page of schema.pages) {
151
+ if (!Array.isArray(page.fields)) {
152
+ continue;
153
+ }
154
+ for (const field of page.fields) {
155
+ if (typeof field.name === "string" && field.name !== "") {
156
+ names.add(field.name);
157
+ }
158
+ }
159
+ }
160
+ }
161
+ return names;
162
+ }
163
+ function sanitizePayloadWithSchema(value, schema) {
164
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
165
+ return {};
166
+ }
167
+ const allowedFieldNames = resolveSchemaFieldNames(schema);
168
+ const sanitizedPayload = {};
169
+ for (const [key, entryValue] of Object.entries(value)) {
170
+ if (!allowedFieldNames.has(key)) {
171
+ continue;
172
+ }
173
+ sanitizedPayload[key] = entryValue;
174
+ }
175
+ return sanitizedPayload;
176
+ }
134
177
  const formState = computed({
135
178
  get: () => {
136
- const value = usesExternalState.value ? unwrapModelValueProp(props.modelValue) : internalForm.state.value;
179
+ const value = usesExternalModel.value ? unwrapModelValueProp(props.modelValue) : internalForm.state.value;
137
180
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
138
181
  return {};
139
182
  }
183
+ if (usesExternalModel.value) {
184
+ const schema = getResolvedSchema();
185
+ if (schema !== null) {
186
+ return sanitizePayloadWithSchema(value, schema);
187
+ }
188
+ }
140
189
  return value;
141
190
  },
142
191
  set: (value) => {
143
- if (usesExternalState.value) {
192
+ if (usesExternalModel.value) {
144
193
  emit("update:modelValue", value);
145
194
  return;
146
195
  }
147
196
  internalForm.replaceState(value);
148
197
  }
149
198
  });
199
+ watch(
200
+ () => [usesExternalModel.value, getResolvedSchema(), unwrapModelValueProp(props.modelValue)],
201
+ ([externalModel, schema, modelValue]) => {
202
+ if (!externalModel || schema === null) {
203
+ return;
204
+ }
205
+ const sanitizedValue = sanitizePayloadWithSchema(modelValue, schema);
206
+ if (areValuesEqual(modelValue, sanitizedValue)) {
207
+ return;
208
+ }
209
+ emit("update:modelValue", sanitizedValue);
210
+ },
211
+ {
212
+ immediate: true,
213
+ deep: true
214
+ }
215
+ );
150
216
  const displayPages = computed(() => {
151
217
  const schema = getResolvedSchema();
152
218
  if (schema === null) {
@@ -635,6 +701,68 @@ function onFormError(event) {
635
701
  rendererErrors.value = errors;
636
702
  navigateToFirstErrorPage(errors);
637
703
  }
704
+ async function validateForm(options = {}) {
705
+ const formInstance = rendererForm.value;
706
+ if (formInstance === void 0) {
707
+ return true;
708
+ }
709
+ try {
710
+ await formInstance.validate(options);
711
+ rendererErrors.value = [];
712
+ return true;
713
+ } catch {
714
+ const errors = formInstance.getErrors();
715
+ rendererErrors.value = errors.map((error) => ({
716
+ id: error.id,
717
+ name: error.name,
718
+ message: error.message
719
+ }));
720
+ navigateToFirstErrorPage(rendererErrors.value);
721
+ return false;
722
+ }
723
+ }
724
+ async function validateField(name) {
725
+ return validateForm({
726
+ name
727
+ });
728
+ }
729
+ function clearErrors(path) {
730
+ const formInstance = rendererForm.value;
731
+ if (formInstance === void 0) {
732
+ return;
733
+ }
734
+ formInstance.clear(path);
735
+ rendererErrors.value = [];
736
+ }
737
+ function getErrors(path) {
738
+ const formInstance = rendererForm.value;
739
+ if (formInstance === void 0) {
740
+ return [];
741
+ }
742
+ return formInstance.getErrors(path);
743
+ }
744
+ const shouldValidateFieldOnBlur = computed(() => {
745
+ if (props.validateOnBlur !== void 0) {
746
+ return props.validateOnBlur;
747
+ }
748
+ if (Array.isArray(props.validateOn)) {
749
+ return props.validateOn.includes("blur");
750
+ }
751
+ return usesExternalModel.value;
752
+ });
753
+ function onFieldBlur(fieldName) {
754
+ if (!shouldValidateFieldOnBlur.value) {
755
+ return;
756
+ }
757
+ validateField(fieldName).catch(() => {
758
+ });
759
+ }
760
+ defineExpose({
761
+ validate: validateForm,
762
+ validateField,
763
+ clearErrors,
764
+ getErrors
765
+ });
638
766
  function hasDateMethod(value) {
639
767
  if (value === null || Array.isArray(value) || typeof value !== "object") {
640
768
  return false;
@@ -793,6 +921,20 @@ function getFieldMetaUi(field) {
793
921
  component: componentUi
794
922
  };
795
923
  }
924
+ function mergeUiClass(defaultClass, value) {
925
+ if (typeof value !== "string" || value.trim() === "") {
926
+ return defaultClass;
927
+ }
928
+ return `${defaultClass} ${value}`;
929
+ }
930
+ function getResolvedFormFieldUi(field) {
931
+ const metaUi = getFieldMetaUi(field).formField;
932
+ return {
933
+ ...metaUi,
934
+ label: mergeUiClass("text-default", metaUi?.label),
935
+ help: mergeUiClass("text-muted", metaUi?.help)
936
+ };
937
+ }
796
938
  function getFieldValue(field) {
797
939
  return formState.value[field.name];
798
940
  }
@@ -841,7 +983,7 @@ function getComponentModelValue(field) {
841
983
  }
842
984
  function getComponentProps(field, page) {
843
985
  const metaUi = getFieldMetaUi(field);
844
- const isDisabled = props.disabled || !usesExternalState.value && internalSubmit.submitting.value || isFieldDisabled(field, page);
986
+ const isDisabled = props.disabled || !usesExternalModel.value && internalSubmit.submitting.value || isFieldDisabled(field, page);
845
987
  const componentProps = {
846
988
  name: field.name,
847
989
  required: isFieldRequired(field),
@@ -900,7 +1042,7 @@ function onFieldModelUpdate(field, nextValue) {
900
1042
  async function onSubmit() {
901
1043
  rendererErrors.value = [];
902
1044
  emit("submit", formState.value);
903
- if (usesExternalState.value) {
1045
+ if (usesExternalModel.value) {
904
1046
  return;
905
1047
  }
906
1048
  if (internalFormKey.value === "") {
@@ -943,9 +1085,11 @@ async function onSubmit() {
943
1085
 
944
1086
  <template>
945
1087
  <UForm
1088
+ ref="rendererForm"
946
1089
  :state="formState"
947
1090
  :schema="getResolvedZodSchema()"
948
- :validate="validate"
1091
+ :validate="props.validate"
1092
+ :validate-on="props.validateOn"
949
1093
  @submit="onSubmit"
950
1094
  @error="onFormError"
951
1095
  >
@@ -967,14 +1111,14 @@ async function onSubmit() {
967
1111
  />
968
1112
 
969
1113
  <UAlert
970
- v-if="!usesExternalState && internalForm.loading.value"
1114
+ v-if="!usesExternalSchema && internalForm.loading.value"
971
1115
  color="neutral"
972
1116
  variant="soft"
973
1117
  :title="t('renderer.loadingForm')"
974
1118
  />
975
1119
 
976
1120
  <UAlert
977
- v-if="!usesExternalState && internalForm.error.value"
1121
+ v-if="!usesExternalSchema && internalForm.error.value"
978
1122
  color="error"
979
1123
  variant="soft"
980
1124
  :title="t('renderer.error.loadForm')"
@@ -982,7 +1126,7 @@ async function onSubmit() {
982
1126
  />
983
1127
 
984
1128
  <UAlert
985
- v-if="!usesExternalState && internalSubmit.error.value"
1129
+ v-if="!usesExternalModel && internalSubmit.error.value"
986
1130
  color="error"
987
1131
  variant="soft"
988
1132
  :title="t('renderer.error.submit')"
@@ -1009,7 +1153,7 @@ async function onSubmit() {
1009
1153
  </UAlert>
1010
1154
 
1011
1155
  <UAlert
1012
- v-if="!usesExternalState && submittedResponse !== null"
1156
+ v-if="!usesExternalModel && submittedResponse !== null"
1013
1157
  color="success"
1014
1158
  variant="soft"
1015
1159
  :title="t('renderer.alert.submitted')"
@@ -1028,7 +1172,7 @@ async function onSubmit() {
1028
1172
  >
1029
1173
  <h3
1030
1174
  v-if="page.title !== ''"
1031
- class="text-base font-semibold"
1175
+ class="text-base font-semibold text-default"
1032
1176
  >
1033
1177
  {{ page.title }}
1034
1178
  </h3>
@@ -1049,7 +1193,8 @@ async function onSubmit() {
1049
1193
  :label="field.label"
1050
1194
  :help="field.help_text"
1051
1195
  :required="isFieldRequired(field)"
1052
- :ui="getFieldMetaUi(field).formField"
1196
+ :ui="getResolvedFormFieldUi(field)"
1197
+ @focusout="() => onFieldBlur(field.name)"
1053
1198
  >
1054
1199
  <UInput
1055
1200
  v-if="field.type === 'text' || field.type === 'email'"
@@ -1162,7 +1307,7 @@ async function onSubmit() {
1162
1307
  </UButton>
1163
1308
 
1164
1309
  <UButton
1165
- v-else-if="!usesExternalState && showSubmit"
1310
+ v-else-if="!usesExternalModel && showSubmit"
1166
1311
  type="submit"
1167
1312
  :loading="internalSubmit.submitting.value"
1168
1313
  :disabled="internalForm.loading.value || getResolvedSchema() === null"
@@ -1172,7 +1317,7 @@ async function onSubmit() {
1172
1317
  </div>
1173
1318
 
1174
1319
  <div
1175
- v-else-if="!usesExternalState && showSubmit"
1320
+ v-else-if="!usesExternalModel && showSubmit"
1176
1321
  class="flex justify-end"
1177
1322
  >
1178
1323
  <UButton
@@ -5,6 +5,12 @@ interface FormForgeValidationError {
5
5
  }
6
6
  type FormForgeValidationHandler = (state: FormForgeSubmissionPayload) => FormForgeValidationError[] | Promise<FormForgeValidationError[]>;
7
7
  type FormForgeProgressVariant = 'stepper' | 'progress';
8
+ type FormForgeValidateEvent = 'input' | 'change' | 'blur';
9
+ interface FormForgeExposedError {
10
+ id?: string;
11
+ name?: string;
12
+ message: string;
13
+ }
8
14
  interface Props {
9
15
  schema?: FormForgeFormSchema | {
10
16
  value: FormForgeFormSchema | null;
@@ -30,8 +36,25 @@ interface Props {
30
36
  showProgress?: boolean;
31
37
  progressVariant?: FormForgeProgressVariant;
32
38
  showAlertOnError?: boolean;
39
+ validateOn?: FormForgeValidateEvent[];
40
+ validateOnBlur?: boolean;
33
41
  }
34
- declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
42
+ type FormForgeValidateOptions = {
43
+ name?: string | string[];
44
+ silent?: boolean;
45
+ nested?: boolean;
46
+ transform?: boolean;
47
+ };
48
+ declare function validateForm(options?: FormForgeValidateOptions): Promise<boolean>;
49
+ declare function validateField(name: string): Promise<boolean>;
50
+ declare function clearErrors(path?: string | RegExp): void;
51
+ declare function getErrors(path?: string | RegExp): FormForgeExposedError[];
52
+ declare const __VLS_export: import("vue").DefineComponent<Props, {
53
+ validate: typeof validateForm;
54
+ validateField: typeof validateField;
55
+ clearErrors: typeof clearErrors;
56
+ getErrors: typeof getErrors;
57
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
35
58
  submit: (value: FormForgeSubmissionPayload) => any;
36
59
  error: (value: string) => any;
37
60
  "update:modelValue": (value: FormForgeSubmissionPayload) => any;
@@ -66,6 +89,8 @@ declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {},
66
89
  showProgress: boolean;
67
90
  progressVariant: FormForgeProgressVariant;
68
91
  showAlertOnError: boolean;
92
+ validateOn: FormForgeValidateEvent[];
93
+ validateOnBlur: boolean;
69
94
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
70
95
  declare const _default: typeof __VLS_export;
71
96
  export default _default;
@@ -10,6 +10,8 @@ export interface FormForgeManagementCreateInput {
10
10
  category?: string | null;
11
11
  meta?: FormForgeJsonObject;
12
12
  api?: FormForgeJsonObject;
13
+ auto_publish?: boolean;
14
+ autoPublish?: boolean;
13
15
  }
14
16
  export interface FormForgeManagementPatchInput {
15
17
  title?: string;
@@ -20,6 +22,8 @@ export interface FormForgeManagementPatchInput {
20
22
  category?: string | null;
21
23
  meta?: FormForgeJsonObject;
22
24
  api?: FormForgeJsonObject;
25
+ auto_publish?: boolean;
26
+ autoPublish?: boolean;
23
27
  }
24
28
  export type FormForgeManagementForm = FormForgeJsonObject & {
25
29
  id?: string | number;
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@evanschleret/formforgeclient",
3
- "version": "1.0.0",
3
+ "version": "1.1.2",
4
4
  "description": "Nuxt module and runtime client for FormForge",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/EvanSchleret/formforgeclient.git"
10
+ },
7
11
  "main": "./dist/module.cjs",
8
12
  "module": "./dist/module.mjs",
9
13
  "types": "./dist/module.d.ts",