@narrative.io/jsonforms-provider-protocols 1.1.0-beta.1 → 1.1.0-beta.10

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 (61) hide show
  1. package/README.md +61 -0
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +24 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/jsonforms-provider-protocols.css +0 -4
  7. package/dist/protocols/rest_api.d.ts +1 -0
  8. package/dist/protocols/rest_api.d.ts.map +1 -1
  9. package/dist/protocols/rest_api.js +6 -1
  10. package/dist/protocols/rest_api.js.map +1 -1
  11. package/dist/vue/composables/useDerive.d.ts +13 -0
  12. package/dist/vue/composables/useDerive.d.ts.map +1 -0
  13. package/dist/vue/composables/useDerive.js +71 -0
  14. package/dist/vue/composables/useDerive.js.map +1 -0
  15. package/dist/vue/index.d.ts +1 -1
  16. package/dist/vue/index.d.ts.map +1 -1
  17. package/dist/vue/index.js +6 -6
  18. package/dist/vue/index.js.map +1 -1
  19. package/dist/vue/primevue/JfBoolean.vue.d.ts +20 -32
  20. package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
  21. package/dist/vue/primevue/JfBoolean.vue.js +38 -6
  22. package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
  23. package/dist/vue/primevue/JfEnum.vue.d.ts +20 -32
  24. package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
  25. package/dist/vue/primevue/JfEnum.vue.js +152 -5
  26. package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
  27. package/dist/vue/primevue/JfEnum.vue2.js +1 -61
  28. package/dist/vue/primevue/JfEnum.vue2.js.map +1 -1
  29. package/dist/vue/primevue/JfEnumArray.vue.d.ts +20 -32
  30. package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
  31. package/dist/vue/primevue/JfEnumArray.vue.js +93 -13
  32. package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
  33. package/dist/vue/primevue/JfNumber.vue.d.ts +20 -32
  34. package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
  35. package/dist/vue/primevue/JfNumber.vue.js +87 -13
  36. package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
  37. package/dist/vue/primevue/JfText.vue.d.ts +20 -32
  38. package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
  39. package/dist/vue/primevue/JfText.vue.js +131 -16
  40. package/dist/vue/primevue/JfText.vue.js.map +1 -1
  41. package/dist/vue/primevue/JfTextArea.vue.d.ts +20 -32
  42. package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
  43. package/dist/vue/primevue/JfTextArea.vue.js +49 -11
  44. package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
  45. package/dist/vue/primevue/index.d.ts +2 -75
  46. package/dist/vue/primevue/index.d.ts.map +1 -1
  47. package/dist/vue/primevue/index.js +46 -28
  48. package/dist/vue/primevue/index.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/index.ts +15 -2
  51. package/src/protocols/rest_api.ts +8 -1
  52. package/src/vue/composables/useDerive.ts +105 -0
  53. package/src/vue/index.ts +10 -1
  54. package/src/vue/primevue/JfBoolean.vue +42 -5
  55. package/src/vue/primevue/JfEnum.vue +131 -20
  56. package/src/vue/primevue/JfEnumArray.vue +118 -14
  57. package/src/vue/primevue/JfNumber.vue +104 -13
  58. package/src/vue/primevue/JfText.vue +156 -13
  59. package/src/vue/primevue/JfTextArea.vue +57 -10
  60. package/src/vue/primevue/index.ts +48 -37
  61. package/src/vue/styles.css +5 -0
@@ -1,12 +1,52 @@
1
+ <script lang="ts">
2
+ // Define props manually to avoid any potential circular dependency issues
3
+ export default {
4
+ name: "JfEnum",
5
+ props: {
6
+ uischema: {
7
+ type: Object,
8
+ required: true,
9
+ },
10
+ schema: {
11
+ type: Object,
12
+ required: true,
13
+ },
14
+ path: {
15
+ type: String,
16
+ required: true,
17
+ },
18
+ enabled: {
19
+ type: Boolean,
20
+ default: undefined,
21
+ },
22
+ renderers: {
23
+ type: Array,
24
+ required: false,
25
+ },
26
+ cells: {
27
+ type: Array,
28
+ required: false,
29
+ },
30
+ config: {
31
+ type: Object,
32
+ required: false,
33
+ },
34
+ },
35
+ };
36
+ </script>
37
+
1
38
  <script setup lang="ts">
2
- import type { ControlElement, JsonSchema } from "@jsonforms/core";
3
- import { rendererProps, useJsonFormsControl } from "@jsonforms/vue";
4
- import { computed } from "vue";
39
+ import type { JsonSchema } from "@jsonforms/core";
40
+ import type { ControlProps } from "@jsonforms/vue";
41
+ import { useJsonFormsControl } from "@jsonforms/vue";
42
+ import { computed, ref, inject, getCurrentInstance } from "vue";
43
+ import { useProvider } from "../composables/useProvider";
44
+ import { useDerive } from "../composables/useDerive";
5
45
  import Dropdown from "primevue/dropdown";
6
46
 
7
- defineOptions({ name: "JfEnum" });
8
-
9
- const props = defineProps(rendererProps<ControlElement>());
47
+ // Access props from the component instance
48
+ const instance = getCurrentInstance()!;
49
+ const props = instance.props as unknown as ControlProps;
10
50
  const { control, handleChange } = useJsonFormsControl(props);
11
51
 
12
52
  type Opt = { label: string; value: unknown };
@@ -26,14 +66,87 @@ const toOptions = (schema?: JsonSchema): Opt[] => {
26
66
  return [];
27
67
  };
28
68
 
29
- const placeholder = computed<string | undefined>(
69
+ // Provider support
70
+ const binding = computed(() => {
71
+ const provider = control.value.uischema?.options?.provider;
72
+ // Ensure load property is set to 'mount' by default
73
+ if (provider && typeof provider === "object" && !provider.load) {
74
+ return { ...provider, load: "mount" };
75
+ }
76
+ return provider;
77
+ });
78
+
79
+ const deps = computed(
30
80
  () =>
31
- (control.value.uischema as { options?: { placeholder?: string } })?.options
32
- ?.placeholder ?? control.value.description,
81
+ ((
82
+ (control.value.schema as Record<string, unknown>)?.[
83
+ "x-provider"
84
+ ] as Record<string, unknown>
85
+ )?.dependsOn as string[]) ?? [],
33
86
  );
87
+ const depValues = computed(() => {
88
+ return deps.value.map((dep) => {
89
+ // Resolve dependency value from form data using JSON pointer-like path
90
+ const path = dep.startsWith("#/") ? dep.slice(2) : dep;
91
+ const keys = path.replace(/\//g, ".").split(".");
92
+ let value: unknown = rootData.value;
93
+ for (const key of keys) {
94
+ if (value && typeof value === "object" && key in value) {
95
+ value = (value as Record<string, unknown>)[key];
96
+ } else {
97
+ return null;
98
+ }
99
+ }
100
+ return value;
101
+ });
102
+ });
103
+
104
+ // Get the root form data from JSONForms context for template URL resolution
105
+ const injectedFormData = inject<{ value: unknown }>("formData", { value: {} });
106
+ const rootData = computed(() => injectedFormData.value || {});
107
+
108
+ // Use provider if available, otherwise fall back to schema enum/oneOf
109
+ const {
110
+ items: providerItems,
111
+ loading,
112
+ error,
113
+ } = useProvider(binding, {
114
+ data: rootData,
115
+ path: control.value.path,
116
+ dependsOnValues: depValues.value,
117
+ });
118
+
119
+ const placeholder = computed<string | undefined>(() => {
120
+ if (loading.value) return "Loading…";
121
+ return (
122
+ (control.value.uischema as { options?: { placeholder?: string } })?.options
123
+ ?.placeholder ?? control.value.description
124
+ );
125
+ });
126
+
127
+ const options = computed(() => {
128
+ // Use provider items if available, otherwise fall back to schema enum/oneOf
129
+ if (binding.value && providerItems.value.length > 0) {
130
+ return providerItems.value;
131
+ }
132
+ return toOptions(control.value.schema);
133
+ });
134
+
135
+ // Add derive functionality
136
+ useDerive({ control, handleChange });
137
+
138
+ // Track user interaction
139
+ const hasInteracted = ref(false);
140
+
141
+ const showErrors = computed(() => hasInteracted.value && control.value.errors);
34
142
 
35
- const options = computed(() => toOptions(control.value.schema));
36
- const onSelect = (val: unknown) => handleChange(control.value.path, val);
143
+ const onSelect = (val: unknown) => {
144
+ handleChange(control.value.path, val);
145
+ };
146
+
147
+ const onBlur = () => {
148
+ hasInteracted.value = true;
149
+ };
37
150
  </script>
38
151
 
39
152
  <template>
@@ -51,17 +164,15 @@ const onSelect = (val: unknown) => handleChange(control.value.path, val);
51
164
  option-value="value"
52
165
  :model-value="control.data ?? null"
53
166
  :placeholder="placeholder"
54
- :disabled="!control.enabled"
55
- :aria-invalid="!!control.errors || undefined"
167
+ :disabled="!control.enabled || loading"
168
+ :aria-invalid="!!showErrors || undefined"
56
169
  :show-clear="true"
57
170
  @update:model-value="onSelect"
171
+ @blur="onBlur"
58
172
  />
59
- <small v-if="control.errors" class="p-error">{{ control.errors }}</small>
173
+ <small v-if="error" class="p-error" role="alert"
174
+ >Failed to load: {{ error }}</small
175
+ >
176
+ <small v-else-if="showErrors" class="p-error">{{ control.errors }}</small>
60
177
  </div>
61
178
  </template>
62
-
63
- <style scoped>
64
- :deep(.p-dropdown-label) {
65
- text-align: left;
66
- }
67
- </style>
@@ -1,12 +1,51 @@
1
+ <script lang="ts">
2
+ export default {
3
+ name: "JfEnumArray",
4
+ props: {
5
+ uischema: {
6
+ type: Object,
7
+ required: true,
8
+ },
9
+ schema: {
10
+ type: Object,
11
+ required: true,
12
+ },
13
+ path: {
14
+ type: String,
15
+ required: true,
16
+ },
17
+ enabled: {
18
+ type: Boolean,
19
+ default: undefined,
20
+ },
21
+ renderers: {
22
+ type: Array,
23
+ required: false,
24
+ },
25
+ cells: {
26
+ type: Array,
27
+ required: false,
28
+ },
29
+ config: {
30
+ type: Object,
31
+ required: false,
32
+ },
33
+ },
34
+ };
35
+ </script>
36
+
1
37
  <script setup lang="ts">
2
- import type { ControlElement, JsonSchema } from "@jsonforms/core";
3
- import { rendererProps, useJsonFormsControl } from "@jsonforms/vue";
4
- import { computed } from "vue";
38
+ import type { JsonSchema } from "@jsonforms/core";
39
+ import type { ControlProps } from "@jsonforms/vue";
40
+ import { useJsonFormsControl } from "@jsonforms/vue";
41
+ import { computed, inject, getCurrentInstance } from "vue";
42
+ import { useProvider } from "../composables/useProvider";
43
+ import { useDerive } from "../composables/useDerive";
5
44
  import MultiSelect from "primevue/multiselect";
6
45
 
7
- defineOptions({ name: "JfEnumArray" });
8
-
9
- const props = defineProps(rendererProps<ControlElement>());
46
+ // Access props from the component instance
47
+ const instance = getCurrentInstance()!;
48
+ const props = instance.props as unknown as ControlProps;
10
49
  const { control, handleChange } = useJsonFormsControl(props);
11
50
 
12
51
  type Opt = { label: string; value: unknown };
@@ -26,14 +65,74 @@ const toOptions = (schema?: JsonSchema): Opt[] => {
26
65
  return [];
27
66
  };
28
67
 
29
- const options = computed(() =>
30
- toOptions((control.value.schema as { items?: JsonSchema })?.items),
31
- );
32
- const placeholder = computed<string | undefined>(
68
+ // Provider support
69
+ const binding = computed(() => {
70
+ const provider = control.value.uischema?.options?.provider;
71
+ // Ensure load property is set to 'mount' by default
72
+ if (provider && typeof provider === "object" && !provider.load) {
73
+ return { ...provider, load: "mount" };
74
+ }
75
+ return provider;
76
+ });
77
+
78
+ const deps = computed(
33
79
  () =>
34
- (control.value.uischema as { options?: { placeholder?: string } })?.options
35
- ?.placeholder ?? control.value.description,
80
+ ((
81
+ (control.value.schema as Record<string, unknown>)?.[
82
+ "x-provider"
83
+ ] as Record<string, unknown>
84
+ )?.dependsOn as string[]) ?? [],
36
85
  );
86
+ const depValues = computed(() => {
87
+ return deps.value.map((dep) => {
88
+ // Resolve dependency value from form data using JSON pointer-like path
89
+ const path = dep.startsWith("#/") ? dep.slice(2) : dep;
90
+ const keys = path.replace(/\//g, ".").split(".");
91
+ let value: unknown = rootData.value;
92
+ for (const key of keys) {
93
+ if (value && typeof value === "object" && key in value) {
94
+ value = (value as Record<string, unknown>)[key];
95
+ } else {
96
+ return null;
97
+ }
98
+ }
99
+ return value;
100
+ });
101
+ });
102
+
103
+ // Get the root form data from JSONForms context for template URL resolution
104
+ const injectedFormData = inject<{ value: unknown }>("formData", { value: {} });
105
+ const rootData = computed(() => injectedFormData.value || {});
106
+
107
+ // Use provider if available, otherwise fall back to schema enum/oneOf
108
+ const {
109
+ items: providerItems,
110
+ loading,
111
+ error,
112
+ } = useProvider(binding, {
113
+ data: rootData,
114
+ path: control.value.path,
115
+ dependsOnValues: depValues.value,
116
+ });
117
+
118
+ const options = computed(() => {
119
+ // Use provider items if available, otherwise fall back to schema enum/oneOf
120
+ if (binding.value && providerItems.value.length > 0) {
121
+ return providerItems.value;
122
+ }
123
+ return toOptions((control.value.schema as { items?: JsonSchema })?.items);
124
+ });
125
+
126
+ // Add derive functionality
127
+ useDerive({ control, handleChange });
128
+
129
+ const placeholder = computed<string | undefined>(() => {
130
+ if (loading.value) return "Loading…";
131
+ return (
132
+ (control.value.uischema as { options?: { placeholder?: string } })?.options
133
+ ?.placeholder ?? control.value.description
134
+ );
135
+ });
37
136
 
38
137
  // order-insensitive shallow equality for primitive enums
39
138
  const sameSet = (a: unknown[], b: unknown[]) => {
@@ -75,11 +174,16 @@ const model = computed<unknown[]>({
75
174
  option-value="value"
76
175
  data-key="value"
77
176
  display="chip"
78
- :disabled="!control.enabled"
177
+ :disabled="!control.enabled || loading"
79
178
  :aria-invalid="!!control.errors || undefined"
80
179
  :placeholder="placeholder"
81
180
  />
82
181
 
83
- <small v-if="control.errors" class="p-error">{{ control.errors }}</small>
182
+ <small v-if="error" class="p-error" role="alert"
183
+ >Failed to load: {{ error }}</small
184
+ >
185
+ <small v-else-if="control.errors" class="p-error">{{
186
+ control.errors
187
+ }}</small>
84
188
  </div>
85
189
  </template>
@@ -1,22 +1,108 @@
1
+ <script lang="ts">
2
+ export default {
3
+ name: "JfNumber",
4
+ props: {
5
+ uischema: {
6
+ type: Object,
7
+ required: true,
8
+ },
9
+ schema: {
10
+ type: Object,
11
+ required: true,
12
+ },
13
+ path: {
14
+ type: String,
15
+ required: true,
16
+ },
17
+ enabled: {
18
+ type: Boolean,
19
+ default: undefined,
20
+ },
21
+ renderers: {
22
+ type: Array,
23
+ required: false,
24
+ },
25
+ cells: {
26
+ type: Array,
27
+ required: false,
28
+ },
29
+ config: {
30
+ type: Object,
31
+ required: false,
32
+ },
33
+ },
34
+ };
35
+ </script>
36
+
1
37
  <script setup lang="ts">
2
- import type { ControlElement } from "@jsonforms/core";
3
- import { rendererProps, useJsonFormsControl } from "@jsonforms/vue";
4
- import { computed } from "vue";
38
+ import type { ControlProps } from "@jsonforms/vue";
39
+ import { useJsonFormsControl } from "@jsonforms/vue";
40
+ import { computed, ref, getCurrentInstance } from "vue";
41
+ import { useDerive } from "../composables/useDerive";
5
42
  import InputNumber from "primevue/inputnumber";
6
43
 
7
- defineOptions({ name: "JfNumber" });
8
-
9
- const props = defineProps(rendererProps<ControlElement>());
44
+ // Access props from the component instance
45
+ const instance = getCurrentInstance()!;
46
+ const props = instance.props as unknown as ControlProps;
10
47
  const { control, handleChange } = useJsonFormsControl(props);
11
48
 
12
- const placeholder = computed<string | undefined>(
49
+ const options = computed(
13
50
  () =>
14
- (control.value.uischema as { options?: { placeholder?: string } })?.options
15
- ?.placeholder ?? control.value.description,
51
+ (control.value.uischema as { options?: Record<string, unknown> })
52
+ ?.options ?? {},
16
53
  );
17
54
 
18
- const onNumber = (val: number | null) =>
55
+ const placeholder = computed<string | undefined>(
56
+ () => (options.value.placeholder as string) ?? control.value.description,
57
+ );
58
+
59
+ // Add derive functionality
60
+ useDerive({ control, handleChange });
61
+
62
+ // Currency and decimal configuration
63
+ const mode = computed(() => {
64
+ if (options.value.currency) return "currency";
65
+ if (options.value.decimal || typeof options.value.precision === "number")
66
+ return "decimal";
67
+ return undefined;
68
+ });
69
+
70
+ const currency = computed(() =>
71
+ typeof options.value.currency === "string" ? options.value.currency : "USD",
72
+ );
73
+
74
+ const minFractionDigits = computed(() => {
75
+ if (mode.value === "currency") return 2;
76
+ if (typeof options.value.precision === "number")
77
+ return options.value.precision;
78
+ return undefined;
79
+ });
80
+
81
+ const maxFractionDigits = computed(() => {
82
+ if (mode.value === "currency") return 2;
83
+ if (typeof options.value.precision === "number")
84
+ return options.value.precision;
85
+ return undefined;
86
+ });
87
+
88
+ const useGrouping = computed(() => {
89
+ // Enable grouping for currency by default, or if explicitly set
90
+ if (mode.value === "currency") return true;
91
+ return options.value.useGrouping === true;
92
+ });
93
+
94
+ // Track user interaction
95
+ const hasInteracted = ref(false);
96
+
97
+ const showErrors = computed(() => hasInteracted.value && control.value.errors);
98
+
99
+ const onNumber = (val: number | null) => {
19
100
  handleChange(control.value.path, val ?? undefined);
101
+ };
102
+
103
+ const onBlur = () => {
104
+ hasInteracted.value = true;
105
+ };
20
106
  </script>
21
107
 
22
108
  <template>
@@ -30,13 +116,18 @@ const onNumber = (val: number | null) =>
30
116
  <InputNumber
31
117
  class="w-full"
32
118
  input-class="w-full"
33
- :use-grouping="false"
119
+ :use-grouping="useGrouping"
120
+ :mode="mode"
121
+ :currency="currency"
122
+ :min-fraction-digits="minFractionDigits"
123
+ :max-fraction-digits="maxFractionDigits"
34
124
  :model-value="typeof control.data === 'number' ? control.data : null"
35
125
  :placeholder="placeholder"
36
126
  :disabled="!control.enabled"
37
- :aria-invalid="!!control.errors || undefined"
127
+ :aria-invalid="!!showErrors || undefined"
38
128
  @update:model-value="onNumber"
129
+ @blur="onBlur"
39
130
  />
40
- <small v-if="control.errors" class="p-error">{{ control.errors }}</small>
131
+ <small v-if="showErrors" class="p-error">{{ control.errors }}</small>
41
132
  </div>
42
133
  </template>
@@ -1,26 +1,150 @@
1
+ <script lang="ts">
2
+ export default {
3
+ name: "JfText",
4
+ props: {
5
+ uischema: {
6
+ type: Object,
7
+ required: true,
8
+ },
9
+ schema: {
10
+ type: Object,
11
+ required: true,
12
+ },
13
+ path: {
14
+ type: String,
15
+ required: true,
16
+ },
17
+ enabled: {
18
+ type: Boolean,
19
+ default: undefined,
20
+ },
21
+ renderers: {
22
+ type: Array,
23
+ required: false,
24
+ },
25
+ cells: {
26
+ type: Array,
27
+ required: false,
28
+ },
29
+ config: {
30
+ type: Object,
31
+ required: false,
32
+ },
33
+ },
34
+ };
35
+ </script>
36
+
1
37
  <script setup lang="ts">
2
- import type { ControlElement } from "@jsonforms/core";
3
- import { rendererProps, useJsonFormsControl } from "@jsonforms/vue";
4
- import { computed } from "vue";
38
+ import type { ControlProps } from "@jsonforms/vue";
39
+ import { useJsonFormsControl } from "@jsonforms/vue";
40
+ import { computed, ref, inject, watch, getCurrentInstance } from "vue";
41
+ import { useProvider } from "../composables/useProvider";
42
+ import { useDerive } from "../composables/useDerive";
5
43
  import InputText from "primevue/inputtext";
44
+ import AutoComplete from "primevue/autocomplete";
6
45
 
7
- defineOptions({ name: "JfText" });
8
-
9
- const props = defineProps(rendererProps<ControlElement>());
46
+ // Access props from the component instance
47
+ const instance = getCurrentInstance()!;
48
+ const props = instance.props as unknown as ControlProps;
10
49
  const { control, handleChange } = useJsonFormsControl(props);
11
50
 
12
- const placeholder = computed<string | undefined>(
51
+ // Provider support for autocomplete functionality
52
+ const binding = computed(() => {
53
+ const provider = control.value.uischema?.options?.provider;
54
+ // Ensure load property is set to 'query' by default for autocomplete
55
+ if (provider && typeof provider === "object" && !provider.load) {
56
+ return { ...provider, load: "query" };
57
+ }
58
+ return provider;
59
+ });
60
+
61
+ const deps = computed(
13
62
  () =>
14
- (control.value.uischema as { options?: { placeholder?: string } })?.options
15
- ?.placeholder ?? control.value.description,
63
+ ((
64
+ (control.value.schema as Record<string, unknown>)?.[
65
+ "x-provider"
66
+ ] as Record<string, unknown>
67
+ )?.dependsOn as string[]) ?? [],
16
68
  );
69
+ const depValues = computed(() => {
70
+ return deps.value.map((dep) => {
71
+ // Resolve dependency value from form data using JSON pointer-like path
72
+ const path = dep.startsWith("#/") ? dep.slice(2) : dep;
73
+ const keys = path.replace(/\//g, ".").split(".");
74
+ let value: unknown = rootData.value;
75
+ for (const key of keys) {
76
+ if (value && typeof value === "object" && key in value) {
77
+ value = (value as Record<string, unknown>)[key];
78
+ } else {
79
+ return null;
80
+ }
81
+ }
82
+ return value;
83
+ });
84
+ });
85
+
86
+ // Get the root form data from JSONForms context for template URL resolution
87
+ const injectedFormData = inject<{ value: unknown }>("formData", { value: {} });
88
+ const rootData = computed(() => injectedFormData.value || {});
89
+
90
+ const query = ref("");
91
+ const { items, loading, error, reload } = useProvider(binding, {
92
+ data: rootData,
93
+ path: control.value.path,
94
+ uiQuery: query.value,
95
+ dependsOnValues: depValues.value,
96
+ });
97
+
98
+ watch(query, () => {
99
+ if (binding.value?.load === "query") reload();
100
+ });
101
+
102
+ const placeholder = computed<string | undefined>(() => {
103
+ if (loading.value) return "Loading…";
104
+ return (
105
+ (control.value.uischema as { options?: { placeholder?: string } })?.options
106
+ ?.placeholder ?? control.value.description
107
+ );
108
+ });
109
+
110
+ const isAutocomplete = computed(() => !!binding.value);
111
+
112
+ // Add derive functionality
113
+ useDerive({ control, handleChange });
114
+
115
+ // Track user interaction
116
+ const hasInteracted = ref(false);
117
+ const hasFocused = ref(false);
118
+
119
+ const showErrors = computed(() => hasInteracted.value && control.value.errors);
17
120
 
18
121
  function onInput(val: string | undefined) {
19
- const newValue = val ?? "";
20
- if ((control.value.data ?? "") !== newValue) {
122
+ // Convert empty strings to undefined for proper required field validation
123
+ const newValue = val && val.trim() !== "" ? val : undefined;
124
+ if (control.value.data !== newValue) {
21
125
  handleChange(control.value.path, newValue);
22
126
  }
23
127
  }
128
+
129
+ function onBlur() {
130
+ if (hasFocused.value) {
131
+ hasInteracted.value = true;
132
+ }
133
+ }
134
+
135
+ function onFocus() {
136
+ hasFocused.value = true;
137
+ }
138
+
139
+ // Autocomplete specific handlers
140
+ const onComplete = (event: { query: string }) => {
141
+ query.value = event.query;
142
+ };
143
+
144
+ const onSelect = (event: { value?: { value?: unknown } | unknown }) => {
145
+ const newValue = (event.value as { value?: unknown })?.value ?? event.value;
146
+ handleChange(control.value.path, newValue);
147
+ };
24
148
  </script>
25
149
 
26
150
  <template>
@@ -31,17 +155,36 @@ function onInput(val: string | undefined) {
31
155
  <div v-if="control.description" class="text-color-secondary text-left">
32
156
  {{ control.description }}
33
157
  </div>
158
+ <AutoComplete
159
+ v-if="isAutocomplete"
160
+ class="w-full"
161
+ :model-value="control.data ?? ''"
162
+ :suggestions="items"
163
+ option-label="label"
164
+ :placeholder="placeholder"
165
+ :disabled="!control.enabled"
166
+ :aria-invalid="!!showErrors || undefined"
167
+ @complete="onComplete"
168
+ @item-select="onSelect"
169
+ @update:model-value="onInput"
170
+ @blur="onBlur"
171
+ @focus="onFocus"
172
+ />
34
173
  <InputText
174
+ v-else
35
175
  class="w-full"
36
176
  :model-value="control.data ?? ''"
37
177
  :disabled="!control.enabled"
38
- :aria-invalid="!!control.errors || undefined"
178
+ :aria-invalid="!!showErrors || undefined"
39
179
  :placeholder="placeholder"
40
180
  autocapitalize="off"
41
181
  autocomplete="off"
42
182
  spellcheck="false"
43
183
  @update:model-value="onInput"
184
+ @blur="onBlur"
185
+ @focus="onFocus"
44
186
  />
45
- <small v-if="control.errors" class="p-error">{{ control.errors }}</small>
187
+ <small v-if="error" class="p-error" role="alert">Failed: {{ error }}</small>
188
+ <small v-else-if="showErrors" class="p-error">{{ control.errors }}</small>
46
189
  </div>
47
190
  </template>