@narrative.io/jsonforms-provider-protocols 2.11.0 → 3.0.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 (110) hide show
  1. package/README.md +101 -29
  2. package/dist/core/initFormData.d.ts +10 -0
  3. package/dist/core/initFormData.d.ts.map +1 -0
  4. package/dist/core/initFormData.js +99 -0
  5. package/dist/core/initFormData.js.map +1 -0
  6. package/dist/core/projection.d.ts +32 -0
  7. package/dist/core/projection.d.ts.map +1 -0
  8. package/dist/core/projection.js +74 -0
  9. package/dist/core/projection.js.map +1 -0
  10. package/dist/core/resolveScope.d.ts +11 -0
  11. package/dist/core/resolveScope.d.ts.map +1 -0
  12. package/dist/core/resolveScope.js +22 -0
  13. package/dist/core/resolveScope.js.map +1 -0
  14. package/dist/core/transforms.d.ts +8 -10
  15. package/dist/core/transforms.d.ts.map +1 -1
  16. package/dist/core/transforms.js +58 -13
  17. package/dist/core/transforms.js.map +1 -1
  18. package/dist/core/types.d.ts +8 -0
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/index.d.ts +5 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +14 -3
  23. package/dist/index.js.map +1 -1
  24. package/dist/jsonforms-provider-protocols.css +2 -2
  25. package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
  26. package/dist/vue/components/ProviderAutocomplete.vue.js +10 -5
  27. package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -1
  28. package/dist/vue/components/ProviderMultiSelect.vue.d.ts.map +1 -1
  29. package/dist/vue/components/ProviderMultiSelect.vue.js +1 -1
  30. package/dist/vue/components/ProviderMultiSelect.vue2.js +12 -7
  31. package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
  32. package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -1
  33. package/dist/vue/components/ProviderSelect.vue.js +1 -1
  34. package/dist/vue/components/ProviderSelect.vue2.js +13 -6
  35. package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
  36. package/dist/vue/composables/useDataLayer.d.ts +10 -0
  37. package/dist/vue/composables/useDataLayer.d.ts.map +1 -0
  38. package/dist/vue/composables/useDataLayer.js +26 -0
  39. package/dist/vue/composables/useDataLayer.js.map +1 -0
  40. package/dist/vue/composables/useDerive.d.ts +5 -2
  41. package/dist/vue/composables/useDerive.d.ts.map +1 -1
  42. package/dist/vue/composables/useDerive.js +29 -12
  43. package/dist/vue/composables/useDerive.js.map +1 -1
  44. package/dist/vue/composables/useDeriveInitialValue.d.ts +36 -0
  45. package/dist/vue/composables/useDeriveInitialValue.d.ts.map +1 -0
  46. package/dist/vue/composables/useDeriveInitialValue.js +125 -0
  47. package/dist/vue/composables/useDeriveInitialValue.js.map +1 -0
  48. package/dist/vue/composables/useDirtyValidation.d.ts +9 -0
  49. package/dist/vue/composables/useDirtyValidation.d.ts.map +1 -0
  50. package/dist/vue/composables/useDirtyValidation.js +15 -0
  51. package/dist/vue/composables/useDirtyValidation.js.map +1 -0
  52. package/dist/vue/composables/useProjection.d.ts +41 -0
  53. package/dist/vue/composables/useProjection.d.ts.map +1 -0
  54. package/dist/vue/composables/useProjection.js +84 -0
  55. package/dist/vue/composables/useProjection.js.map +1 -0
  56. package/dist/vue/index.d.ts +7 -0
  57. package/dist/vue/index.d.ts.map +1 -1
  58. package/dist/vue/index.js +35 -27
  59. package/dist/vue/index.js.map +1 -1
  60. package/dist/vue/primevue/JfBoolean.vue.d.ts +9 -0
  61. package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
  62. package/dist/vue/primevue/JfBoolean.vue.js +35 -13
  63. package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
  64. package/dist/vue/primevue/JfEnum.vue.d.ts +9 -0
  65. package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
  66. package/dist/vue/primevue/JfEnum.vue.js +31 -22
  67. package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
  68. package/dist/vue/primevue/JfEnumArray.vue.d.ts +9 -0
  69. package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
  70. package/dist/vue/primevue/JfEnumArray.vue.js +33 -18
  71. package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
  72. package/dist/vue/primevue/JfNumber.vue.d.ts +9 -0
  73. package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
  74. package/dist/vue/primevue/JfNumber.vue.js +31 -22
  75. package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
  76. package/dist/vue/primevue/JfText.vue.d.ts +9 -0
  77. package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
  78. package/dist/vue/primevue/JfText.vue.js +40 -32
  79. package/dist/vue/primevue/JfText.vue.js.map +1 -1
  80. package/dist/vue/primevue/JfTextArea.vue.d.ts +9 -0
  81. package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
  82. package/dist/vue/primevue/JfTextArea.vue.js +32 -18
  83. package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
  84. package/dist/vue/primevue/index.d.ts.map +1 -1
  85. package/dist/vue/primevue/index.js +100 -8
  86. package/dist/vue/primevue/index.js.map +1 -1
  87. package/package.json +3 -1
  88. package/src/core/initFormData.ts +189 -0
  89. package/src/core/projection.ts +136 -0
  90. package/src/core/resolveScope.ts +39 -0
  91. package/src/core/transforms.ts +118 -26
  92. package/src/core/types.ts +9 -0
  93. package/src/index.ts +7 -1
  94. package/src/vue/components/ProviderAutocomplete.vue +10 -5
  95. package/src/vue/components/ProviderMultiSelect.vue +14 -7
  96. package/src/vue/components/ProviderSelect.vue +15 -6
  97. package/src/vue/composables/useDataLayer.ts +43 -0
  98. package/src/vue/composables/useDerive.ts +62 -16
  99. package/src/vue/composables/useDeriveInitialValue.ts +195 -0
  100. package/src/vue/composables/useDirtyValidation.ts +20 -0
  101. package/src/vue/composables/useProjection.ts +181 -0
  102. package/src/vue/index.ts +35 -41
  103. package/src/vue/primevue/JfBoolean.vue +25 -7
  104. package/src/vue/primevue/JfEnum.vue +29 -22
  105. package/src/vue/primevue/JfEnumArray.vue +31 -16
  106. package/src/vue/primevue/JfNumber.vue +29 -22
  107. package/src/vue/primevue/JfText.vue +34 -27
  108. package/src/vue/primevue/JfTextArea.vue +29 -17
  109. package/src/vue/primevue/index.ts +114 -8
  110. package/src/vue/styles.css +26 -1
@@ -22,14 +22,17 @@ export default {
22
22
  renderers: {
23
23
  type: Array,
24
24
  required: false,
25
+ default: undefined,
25
26
  },
26
27
  cells: {
27
28
  type: Array,
28
29
  required: false,
30
+ default: undefined,
29
31
  },
30
32
  config: {
31
33
  type: Object,
32
34
  required: false,
35
+ default: undefined,
33
36
  },
34
37
  },
35
38
  };
@@ -39,16 +42,25 @@ export default {
39
42
  import type { JsonSchema } from "@jsonforms/core";
40
43
  import type { ControlProps } from "@jsonforms/vue";
41
44
  import { useJsonFormsControl } from "@jsonforms/vue";
42
- import { computed, ref, inject, getCurrentInstance, watch } from "vue";
45
+ import { computed, inject, getCurrentInstance, watch } from "vue";
43
46
  import { useProvider } from "../composables/useProvider";
44
47
  import { useDerive } from "../composables/useDerive";
48
+ import { useDeriveInitialValue } from "../composables/useDeriveInitialValue";
49
+ import { useProjection } from "../composables/useProjection";
50
+ import { useDirtyValidation } from "../composables/useDirtyValidation";
45
51
  import { shouldAutoSelect } from "../utils/autoSelect";
46
52
  import Dropdown from "primevue/dropdown";
47
53
 
48
54
  // Access props from the component instance
49
55
  const instance = getCurrentInstance()!;
50
56
  const props = instance.props as unknown as ControlProps;
51
- const { control, handleChange } = useJsonFormsControl(props);
57
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
58
+ const {
59
+ projectedData,
60
+ handleProjectedChange: handleChange,
61
+ projectedErrors,
62
+ projectedLabel,
63
+ } = useProjection(control, rawHandleChange);
52
64
 
53
65
  type Opt = { label: string; value: unknown };
54
66
  const toOptions = (schema?: JsonSchema): Opt[] => {
@@ -134,7 +146,10 @@ const options = computed(() => {
134
146
  });
135
147
 
136
148
  // Add derive functionality
137
- useDerive({ control, handleChange });
149
+ useDerive({ control, handleChange, data: projectedData });
150
+
151
+ // Add deriveInitialValue — async API-based initial value seeding
152
+ useDeriveInitialValue({ control, handleChange });
138
153
 
139
154
  // Auto-select when provider returns only one item (enabled by default)
140
155
  watch(
@@ -145,7 +160,7 @@ watch(
145
160
  control.value.uischema?.options?.autoSelectSingle !== false,
146
161
  isLoading,
147
162
  items,
148
- currentValue: control.value.data,
163
+ currentValue: projectedData.value,
149
164
  });
150
165
 
151
166
  if (valueToSelect !== null) {
@@ -155,44 +170,36 @@ watch(
155
170
  { immediate: true },
156
171
  );
157
172
 
158
- // Track user interaction
159
- const hasInteracted = ref(false);
160
-
161
- const showErrors = computed(() => hasInteracted.value && control.value.errors);
173
+ // Track user interaction — errors only show after blur
174
+ const { showErrors, markDirty } = useDirtyValidation(control, projectedErrors);
162
175
 
163
176
  const onSelect = (val: unknown) => {
164
177
  handleChange(control.value.path, val);
165
178
  };
166
-
167
- const onBlur = () => {
168
- hasInteracted.value = true;
169
- };
170
179
  </script>
171
180
 
172
181
  <template>
173
- <div class="flex flex-col gap-2">
174
- <label v-if="control.label" class="text-color text-left">{{
175
- control.label
176
- }}</label>
177
- <div v-if="control.description" class="text-color-secondary text-left">
182
+ <div class="jf-control">
183
+ <label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
184
+ <div v-if="control.description" class="jf-description">
178
185
  {{ control.description }}
179
186
  </div>
180
187
  <Dropdown
181
- class="w-full!"
188
+ :class="['w-full!', { 'p-invalid': showErrors }]"
182
189
  :options="options"
183
190
  option-label="label"
184
191
  option-value="value"
185
- :model-value="control.data ?? null"
192
+ :model-value="projectedData ?? null"
186
193
  :placeholder="placeholder"
187
194
  :disabled="!control.enabled || loading"
188
- :aria-invalid="!!showErrors || undefined"
195
+ :aria-invalid="showErrors || undefined"
189
196
  :show-clear="true"
190
197
  @update:model-value="onSelect"
191
- @blur="onBlur"
198
+ @blur="markDirty"
192
199
  />
193
200
  <small v-if="error" class="p-error" role="alert"
194
201
  >Failed to load: {{ error }}</small
195
202
  >
196
- <small v-else-if="showErrors" class="p-error">{{ control.errors }}</small>
203
+ <small v-else-if="showErrors" class="p-error">{{ projectedErrors }}</small>
197
204
  </div>
198
205
  </template>
@@ -21,14 +21,17 @@ export default {
21
21
  renderers: {
22
22
  type: Array,
23
23
  required: false,
24
+ default: undefined,
24
25
  },
25
26
  cells: {
26
27
  type: Array,
27
28
  required: false,
29
+ default: undefined,
28
30
  },
29
31
  config: {
30
32
  type: Object,
31
33
  required: false,
34
+ default: undefined,
32
35
  },
33
36
  },
34
37
  };
@@ -41,13 +44,21 @@ import { useJsonFormsControl } from "@jsonforms/vue";
41
44
  import { computed, inject, getCurrentInstance, watch } from "vue";
42
45
  import { useProvider } from "../composables/useProvider";
43
46
  import { useDerive } from "../composables/useDerive";
47
+ import { useProjection } from "../composables/useProjection";
48
+ import { useDirtyValidation } from "../composables/useDirtyValidation";
44
49
  import { shouldAutoSelectMulti } from "../utils/autoSelect";
45
50
  import MultiSelect from "primevue/multiselect";
46
51
 
47
52
  // Access props from the component instance
48
53
  const instance = getCurrentInstance()!;
49
54
  const props = instance.props as unknown as ControlProps;
50
- const { control, handleChange } = useJsonFormsControl(props);
55
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
56
+ const {
57
+ projectedData,
58
+ handleProjectedChange: handleChange,
59
+ projectedErrors,
60
+ projectedLabel,
61
+ } = useProjection(control, rawHandleChange);
51
62
 
52
63
  type Opt = { label: string; value: unknown };
53
64
  const toOptions = (schema?: JsonSchema): Opt[] => {
@@ -125,7 +136,10 @@ const options = computed(() => {
125
136
  });
126
137
 
127
138
  // Add derive functionality
128
- useDerive({ control, handleChange });
139
+ useDerive({ control, handleChange, data: projectedData });
140
+
141
+ // Track user interaction — errors only show after first change
142
+ const { showErrors, markDirty } = useDirtyValidation(control, projectedErrors);
129
143
 
130
144
  // Auto-select when provider returns only one item (opt-in for multiselect)
131
145
  watch(
@@ -136,7 +150,9 @@ watch(
136
150
  control.value.uischema?.options?.autoSelectSingle === true,
137
151
  isLoading,
138
152
  items,
139
- currentValue: Array.isArray(control.value.data) ? control.value.data : [],
153
+ currentValue: Array.isArray(projectedData.value)
154
+ ? projectedData.value
155
+ : [],
140
156
  });
141
157
 
142
158
  if (valueToSelect !== null) {
@@ -165,45 +181,44 @@ const sameSet = (a: unknown[], b: unknown[]) => {
165
181
  // v-model with guard to avoid recursive updates
166
182
  const model = computed<unknown[]>({
167
183
  get() {
168
- const curr = Array.isArray(control.value.data) ? control.value.data : [];
184
+ const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
169
185
  // return a fresh copy so PrimeMultiSelect can't mutate JSONForms' array in place
170
186
  return [...curr];
171
187
  },
172
188
  set(val) {
173
189
  const next = Array.isArray(val) ? [...val] : [];
174
- const curr = Array.isArray(control.value.data) ? control.value.data : [];
175
- if (!sameSet(curr, next)) handleChange(control.value.path, next);
190
+ const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
191
+ if (!sameSet(curr, next)) {
192
+ markDirty();
193
+ handleChange(control.value.path, next);
194
+ }
176
195
  },
177
196
  });
178
197
  </script>
179
198
 
180
199
  <template>
181
- <div class="flex flex-col gap-2">
182
- <label v-if="control.label" class="text-color text-left">{{
183
- control.label
184
- }}</label>
185
- <div v-if="control.description" class="text-color-secondary text-left">
200
+ <div class="jf-control">
201
+ <label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
202
+ <div v-if="control.description" class="jf-description">
186
203
  {{ control.description }}
187
204
  </div>
188
205
 
189
206
  <MultiSelect
190
207
  v-model="model"
191
- class="w-full!"
208
+ :class="['w-full!', { 'p-invalid': showErrors }]"
192
209
  :options="options"
193
210
  option-label="label"
194
211
  option-value="value"
195
212
  data-key="value"
196
213
  display="chip"
197
214
  :disabled="!control.enabled || loading"
198
- :aria-invalid="!!control.errors || undefined"
215
+ :aria-invalid="showErrors || undefined"
199
216
  :placeholder="placeholder"
200
217
  />
201
218
 
202
219
  <small v-if="error" class="p-error" role="alert"
203
220
  >Failed to load: {{ error }}</small
204
221
  >
205
- <small v-else-if="control.errors" class="p-error">{{
206
- control.errors
207
- }}</small>
222
+ <small v-else-if="showErrors" class="p-error">{{ projectedErrors }}</small>
208
223
  </div>
209
224
  </template>
@@ -21,14 +21,17 @@ export default {
21
21
  renderers: {
22
22
  type: Array,
23
23
  required: false,
24
+ default: undefined,
24
25
  },
25
26
  cells: {
26
27
  type: Array,
27
28
  required: false,
29
+ default: undefined,
28
30
  },
29
31
  config: {
30
32
  type: Object,
31
33
  required: false,
34
+ default: undefined,
32
35
  },
33
36
  },
34
37
  };
@@ -37,14 +40,23 @@ export default {
37
40
  <script setup lang="ts">
38
41
  import type { ControlProps } from "@jsonforms/vue";
39
42
  import { useJsonFormsControl } from "@jsonforms/vue";
40
- import { computed, ref, getCurrentInstance } from "vue";
43
+ import { computed, getCurrentInstance } from "vue";
41
44
  import { useDerive } from "../composables/useDerive";
45
+ import { useDeriveInitialValue } from "../composables/useDeriveInitialValue";
46
+ import { useProjection } from "../composables/useProjection";
47
+ import { useDirtyValidation } from "../composables/useDirtyValidation";
42
48
  import InputNumber from "primevue/inputnumber";
43
49
 
44
50
  // Access props from the component instance
45
51
  const instance = getCurrentInstance()!;
46
52
  const props = instance.props as unknown as ControlProps;
47
- const { control, handleChange } = useJsonFormsControl(props);
53
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
54
+ const {
55
+ projectedData,
56
+ handleProjectedChange: handleChange,
57
+ projectedErrors,
58
+ projectedLabel,
59
+ } = useProjection(control, rawHandleChange);
48
60
 
49
61
  const options = computed(
50
62
  () =>
@@ -57,7 +69,10 @@ const placeholder = computed<string | undefined>(
57
69
  );
58
70
 
59
71
  // Add derive functionality
60
- useDerive({ control, handleChange });
72
+ useDerive({ control, handleChange, data: projectedData });
73
+
74
+ // Add deriveInitialValue — async API-based initial value seeding
75
+ useDeriveInitialValue({ control, handleChange });
61
76
 
62
77
  // Currency and decimal configuration
63
78
  const mode = computed(() => {
@@ -95,43 +110,35 @@ const useGrouping = computed(() => {
95
110
  return options.value.useGrouping === true;
96
111
  });
97
112
 
98
- // Track user interaction
99
- const hasInteracted = ref(false);
100
-
101
- const showErrors = computed(() => hasInteracted.value && control.value.errors);
113
+ // Track user interaction — errors only show after blur
114
+ const { showErrors, markDirty } = useDirtyValidation(control, projectedErrors);
102
115
 
103
116
  const onNumber = (val: number | null) => {
104
117
  handleChange(control.value.path, val ?? undefined);
105
118
  };
106
-
107
- const onBlur = () => {
108
- hasInteracted.value = true;
109
- };
110
119
  </script>
111
120
 
112
121
  <template>
113
- <div class="flex flex-col gap-2">
114
- <label v-if="control.label" class="text-color text-left">{{
115
- control.label
116
- }}</label>
117
- <div v-if="control.description" class="text-color-secondary text-left">
122
+ <div class="jf-control">
123
+ <label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
124
+ <div v-if="control.description" class="jf-description">
118
125
  {{ control.description }}
119
126
  </div>
120
127
  <InputNumber
121
- class="w-full!"
122
- input-class="w-full!"
128
+ :class="['w-full!', { 'p-invalid': showErrors }]"
129
+ :input-class="['w-full!', { 'p-invalid': showErrors }]"
123
130
  :use-grouping="useGrouping"
124
131
  :mode="mode"
125
132
  :currency="currency"
126
133
  :min-fraction-digits="minFractionDigits"
127
134
  :max-fraction-digits="maxFractionDigits"
128
- :model-value="typeof control.data === 'number' ? control.data : null"
135
+ :model-value="typeof projectedData === 'number' ? projectedData : null"
129
136
  :placeholder="placeholder"
130
137
  :disabled="!control.enabled"
131
- :aria-invalid="!!showErrors || undefined"
138
+ :aria-invalid="showErrors || undefined"
132
139
  @update:model-value="onNumber"
133
- @blur="onBlur"
140
+ @blur="markDirty"
134
141
  />
135
- <small v-if="showErrors" class="p-error">{{ control.errors }}</small>
142
+ <small v-if="showErrors" class="p-error">{{ projectedErrors }}</small>
136
143
  </div>
137
144
  </template>
@@ -21,14 +21,17 @@ export default {
21
21
  renderers: {
22
22
  type: Array,
23
23
  required: false,
24
+ default: undefined,
24
25
  },
25
26
  cells: {
26
27
  type: Array,
27
28
  required: false,
29
+ default: undefined,
28
30
  },
29
31
  config: {
30
32
  type: Object,
31
33
  required: false,
34
+ default: undefined,
32
35
  },
33
36
  },
34
37
  };
@@ -40,13 +43,22 @@ import { useJsonFormsControl } from "@jsonforms/vue";
40
43
  import { computed, ref, inject, watch, getCurrentInstance } from "vue";
41
44
  import { useProvider } from "../composables/useProvider";
42
45
  import { useDerive } from "../composables/useDerive";
46
+ import { useDeriveInitialValue } from "../composables/useDeriveInitialValue";
47
+ import { useProjection } from "../composables/useProjection";
48
+ import { useDirtyValidation } from "../composables/useDirtyValidation";
43
49
  import InputText from "primevue/inputtext";
44
50
  import AutoComplete from "primevue/autocomplete";
45
51
 
46
52
  // Access props from the component instance
47
53
  const instance = getCurrentInstance()!;
48
54
  const props = instance.props as unknown as ControlProps;
49
- const { control, handleChange } = useJsonFormsControl(props);
55
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
56
+ const {
57
+ projectedData,
58
+ handleProjectedChange: handleChange,
59
+ projectedErrors,
60
+ projectedLabel,
61
+ } = useProjection(control, rawHandleChange);
50
62
 
51
63
  // Provider support for autocomplete functionality
52
64
  const binding = computed(() => {
@@ -110,32 +122,31 @@ const placeholder = computed<string | undefined>(() => {
110
122
  const isAutocomplete = computed(() => !!binding.value);
111
123
 
112
124
  // Add derive functionality
113
- useDerive({ control, handleChange });
125
+ useDerive({ control, handleChange, data: projectedData });
114
126
 
115
- // Track user interaction
116
- const hasInteracted = ref(false);
117
- const hasFocused = ref(false);
127
+ // Add deriveInitialValue — async API-based initial value seeding
128
+ useDeriveInitialValue({ control, handleChange });
118
129
 
119
- const showErrors = computed(() => hasInteracted.value && control.value.errors);
130
+ // Track user interaction errors only show after blur
131
+ const { showErrors, markDirty } = useDirtyValidation(control, projectedErrors);
120
132
 
121
133
  function onInput(val: string | undefined) {
122
134
  // Convert empty strings to undefined for proper required field validation
123
135
  const newValue = val && val.trim() !== "" ? val : undefined;
124
- if (control.value.data !== newValue) {
136
+ if (projectedData.value !== newValue) {
125
137
  handleChange(control.value.path, newValue);
126
138
  }
127
139
  }
128
140
 
129
141
  function onBlur() {
130
- if (hasFocused.value) {
131
- hasInteracted.value = true;
142
+ markDirty();
143
+ // Normalize empty strings to undefined so required validation fires
144
+ const val = projectedData.value;
145
+ if (typeof val === "string" && val.trim() === "") {
146
+ handleChange(control.value.path, undefined);
132
147
  }
133
148
  }
134
149
 
135
- function onFocus() {
136
- hasFocused.value = true;
137
- }
138
-
139
150
  // Autocomplete specific handlers
140
151
  const onComplete = (event: { query: string }) => {
141
152
  query.value = event.query;
@@ -148,43 +159,39 @@ const onSelect = (event: { value?: { value?: unknown } | unknown }) => {
148
159
  </script>
149
160
 
150
161
  <template>
151
- <div class="flex flex-col gap-2">
152
- <label v-if="control.label" class="text-color text-left">{{
153
- control.label
154
- }}</label>
155
- <div v-if="control.description" class="text-color-secondary text-left">
162
+ <div class="jf-control">
163
+ <label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
164
+ <div v-if="control.description" class="jf-description">
156
165
  {{ control.description }}
157
166
  </div>
158
167
  <AutoComplete
159
168
  v-if="isAutocomplete"
160
- class="w-full!"
161
- :model-value="control.data ?? ''"
169
+ :class="['w-full!', { 'p-invalid': showErrors }]"
170
+ :model-value="projectedData ?? ''"
162
171
  :suggestions="items"
163
172
  option-label="label"
164
173
  :placeholder="placeholder"
165
174
  :disabled="!control.enabled"
166
- :aria-invalid="!!showErrors || undefined"
175
+ :aria-invalid="showErrors || undefined"
167
176
  @complete="onComplete"
168
177
  @item-select="onSelect"
169
178
  @update:model-value="onInput"
170
179
  @blur="onBlur"
171
- @focus="onFocus"
172
180
  />
173
181
  <InputText
174
182
  v-else
175
- class="w-full!"
176
- :model-value="control.data ?? ''"
183
+ :class="['w-full!', { 'p-invalid': showErrors }]"
184
+ :model-value="(projectedData as string) ?? ''"
177
185
  :disabled="!control.enabled"
178
- :aria-invalid="!!showErrors || undefined"
186
+ :aria-invalid="showErrors || undefined"
179
187
  :placeholder="placeholder"
180
188
  autocapitalize="off"
181
189
  autocomplete="off"
182
190
  spellcheck="false"
183
191
  @update:model-value="onInput"
184
192
  @blur="onBlur"
185
- @focus="onFocus"
186
193
  />
187
194
  <small v-if="error" class="p-error" role="alert">Failed: {{ error }}</small>
188
- <small v-else-if="showErrors" class="p-error">{{ control.errors }}</small>
195
+ <small v-else-if="showErrors" class="p-error">{{ projectedErrors }}</small>
189
196
  </div>
190
197
  </template>
@@ -21,14 +21,17 @@ export default {
21
21
  renderers: {
22
22
  type: Array,
23
23
  required: false,
24
+ default: undefined,
24
25
  },
25
26
  cells: {
26
27
  type: Array,
27
28
  required: false,
29
+ default: undefined,
28
30
  },
29
31
  config: {
30
32
  type: Object,
31
33
  required: false,
34
+ default: undefined,
32
35
  },
33
36
  },
34
37
  };
@@ -37,13 +40,21 @@ export default {
37
40
  <script setup lang="ts">
38
41
  import type { ControlProps } from "@jsonforms/vue";
39
42
  import { useJsonFormsControl } from "@jsonforms/vue";
40
- import { computed, ref, getCurrentInstance } from "vue";
43
+ import { computed, getCurrentInstance } from "vue";
44
+ import { useProjection } from "../composables/useProjection";
45
+ import { useDirtyValidation } from "../composables/useDirtyValidation";
41
46
  import Textarea from "primevue/textarea";
42
47
 
43
48
  // Access props from the component instance
44
49
  const instance = getCurrentInstance()!;
45
50
  const props = instance.props as unknown as ControlProps;
46
- const { control, handleChange } = useJsonFormsControl(props);
51
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
52
+ const {
53
+ projectedData,
54
+ handleProjectedChange: handleChange,
55
+ projectedErrors,
56
+ projectedLabel,
57
+ } = useProjection(control, rawHandleChange);
47
58
 
48
59
  const placeholder = computed<string | undefined>(
49
60
  () =>
@@ -51,43 +62,44 @@ const placeholder = computed<string | undefined>(
51
62
  ?.placeholder ?? control.value.description,
52
63
  );
53
64
 
54
- // Track user interaction
55
- const hasInteracted = ref(false);
56
-
57
- const showErrors = computed(() => hasInteracted.value && control.value.errors);
65
+ // Track user interaction — errors only show after blur
66
+ const { showErrors, markDirty } = useDirtyValidation(control, projectedErrors);
58
67
 
59
68
  function onInput(val: string | undefined) {
60
69
  // Convert empty strings to undefined for proper required field validation
61
70
  const newValue = val && val.trim() !== "" ? val : undefined;
62
- if (control.value.data !== newValue) {
71
+ if (projectedData.value !== newValue) {
63
72
  handleChange(control.value.path, newValue);
64
73
  }
65
74
  }
66
75
 
67
76
  function onBlur() {
68
- hasInteracted.value = true;
77
+ markDirty();
78
+ // Normalize empty strings to undefined so required validation fires
79
+ const val = projectedData.value;
80
+ if (typeof val === "string" && val.trim() === "") {
81
+ handleChange(control.value.path, undefined);
82
+ }
69
83
  }
70
84
  </script>
71
85
 
72
86
  <template>
73
- <div class="flex flex-col gap-2">
74
- <label v-if="control.label" class="text-color text-left">{{
75
- control.label
76
- }}</label>
77
- <div v-if="control.description" class="text-color-secondary text-left">
87
+ <div class="jf-control">
88
+ <label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
89
+ <div v-if="control.description" class="jf-description">
78
90
  {{ control.description }}
79
91
  </div>
80
92
  <Textarea
81
- class="w-full!"
82
- :model-value="control.data ?? ''"
93
+ :class="['w-full!', { 'p-invalid': showErrors }]"
94
+ :model-value="(projectedData as string) ?? ''"
83
95
  :disabled="!control.enabled"
84
- :aria-invalid="!!showErrors || undefined"
96
+ :aria-invalid="showErrors || undefined"
85
97
  :placeholder="placeholder"
86
98
  :rows="4"
87
99
  :auto-resize="true"
88
100
  @update:model-value="onInput"
89
101
  @blur="onBlur"
90
102
  />
91
- <small v-if="showErrors" class="p-error">{{ control.errors }}</small>
103
+ <small v-if="showErrors" class="p-error">{{ projectedErrors }}</small>
92
104
  </div>
93
105
  </template>