@narrative.io/jsonforms-provider-protocols 3.0.0-beta.1 → 3.0.0-beta.3

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