@narrative.io/jsonforms-provider-protocols 2.11.0-beta.0 → 2.12.0

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