@narrative.io/jsonforms-provider-protocols 2.10.0 → 3.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +166 -30
  2. package/dist/core/projection.d.ts +32 -0
  3. package/dist/core/projection.d.ts.map +1 -0
  4. package/dist/core/projection.js +74 -0
  5. package/dist/core/projection.js.map +1 -0
  6. package/dist/core/resolveScope.d.ts +11 -0
  7. package/dist/core/resolveScope.d.ts.map +1 -0
  8. package/dist/core/resolveScope.js +22 -0
  9. package/dist/core/resolveScope.js.map +1 -0
  10. package/dist/core/transforms.d.ts +8 -10
  11. package/dist/core/transforms.d.ts.map +1 -1
  12. package/dist/core/transforms.js +56 -13
  13. package/dist/core/transforms.js.map +1 -1
  14. package/dist/core/types.d.ts +7 -0
  15. package/dist/core/types.d.ts.map +1 -1
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +12 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/jsonforms-provider-protocols.css +2 -2
  21. package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
  22. package/dist/vue/components/ProviderAutocomplete.vue.js +4 -2
  23. package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -1
  24. package/dist/vue/components/ProviderMultiSelect.vue.d.ts.map +1 -1
  25. package/dist/vue/components/ProviderMultiSelect.vue.js +1 -1
  26. package/dist/vue/components/ProviderMultiSelect.vue2.js +6 -4
  27. package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
  28. package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -1
  29. package/dist/vue/components/ProviderSelect.vue.js +1 -1
  30. package/dist/vue/components/ProviderSelect.vue2.js +5 -3
  31. package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
  32. package/dist/vue/composables/useDataLayer.d.ts +9 -0
  33. package/dist/vue/composables/useDataLayer.d.ts.map +1 -0
  34. package/dist/vue/composables/useDataLayer.js +25 -0
  35. package/dist/vue/composables/useDataLayer.js.map +1 -0
  36. package/dist/vue/composables/useDerive.d.ts +5 -2
  37. package/dist/vue/composables/useDerive.d.ts.map +1 -1
  38. package/dist/vue/composables/useDerive.js +12 -12
  39. package/dist/vue/composables/useDerive.js.map +1 -1
  40. package/dist/vue/composables/useDirtyValidation.d.ts +9 -0
  41. package/dist/vue/composables/useDirtyValidation.d.ts.map +1 -0
  42. package/dist/vue/composables/useDirtyValidation.js +15 -0
  43. package/dist/vue/composables/useDirtyValidation.js.map +1 -0
  44. package/dist/vue/composables/useProjection.d.ts +35 -0
  45. package/dist/vue/composables/useProjection.d.ts.map +1 -0
  46. package/dist/vue/composables/useProjection.js +33 -0
  47. package/dist/vue/composables/useProjection.js.map +1 -0
  48. package/dist/vue/index.d.ts +5 -0
  49. package/dist/vue/index.d.ts.map +1 -1
  50. package/dist/vue/index.js +17 -29
  51. package/dist/vue/index.js.map +1 -1
  52. package/dist/vue/primevue/JfBoolean.vue.d.ts +9 -0
  53. package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
  54. package/dist/vue/primevue/JfBoolean.vue.js +21 -10
  55. package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
  56. package/dist/vue/primevue/JfEnum.vue.d.ts +9 -0
  57. package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
  58. package/dist/vue/primevue/JfEnum.vue.js +20 -18
  59. package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
  60. package/dist/vue/primevue/JfEnumArray.vue.d.ts +9 -0
  61. package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
  62. package/dist/vue/primevue/JfEnumArray.vue.js +24 -14
  63. package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
  64. package/dist/vue/primevue/JfNumber.vue.d.ts +9 -0
  65. package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
  66. package/dist/vue/primevue/JfNumber.vue.js +20 -18
  67. package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
  68. package/dist/vue/primevue/JfText.vue.d.ts +9 -0
  69. package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
  70. package/dist/vue/primevue/JfText.vue.js +29 -28
  71. package/dist/vue/primevue/JfText.vue.js.map +1 -1
  72. package/dist/vue/primevue/JfTextArea.vue.d.ts +9 -0
  73. package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
  74. package/dist/vue/primevue/JfTextArea.vue.js +23 -14
  75. package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
  76. package/dist/vue/primevue/index.d.ts.map +1 -1
  77. package/dist/vue/primevue/index.js +22 -7
  78. package/dist/vue/primevue/index.js.map +1 -1
  79. package/package.json +7 -3
  80. package/src/core/projection.ts +136 -0
  81. package/src/core/resolveScope.ts +39 -0
  82. package/src/core/transforms.ts +91 -26
  83. package/src/core/types.ts +8 -0
  84. package/src/index.ts +7 -0
  85. package/src/vue/components/ProviderAutocomplete.vue +4 -2
  86. package/src/vue/components/ProviderMultiSelect.vue +6 -4
  87. package/src/vue/components/ProviderSelect.vue +5 -3
  88. package/src/vue/composables/useDataLayer.ts +43 -0
  89. package/src/vue/composables/useDerive.ts +19 -16
  90. package/src/vue/composables/useDirtyValidation.ts +15 -0
  91. package/src/vue/composables/useProjection.ts +74 -0
  92. package/src/vue/index.ts +21 -46
  93. package/src/vue/primevue/JfBoolean.vue +18 -5
  94. package/src/vue/primevue/JfEnum.vue +16 -16
  95. package/src/vue/primevue/JfEnumArray.vue +21 -9
  96. package/src/vue/primevue/JfNumber.vue +16 -16
  97. package/src/vue/primevue/JfText.vue +22 -22
  98. package/src/vue/primevue/JfTextArea.vue +20 -11
  99. package/src/vue/primevue/index.ts +32 -7
@@ -1,5 +1,6 @@
1
- import { computed, watch, inject, type Ref } from "vue";
1
+ import { computed, watch, unref, inject, type Ref, type ComputedRef } from "vue";
2
2
  import { type ControlElement } from "@jsonforms/core";
3
+ import { useDataLayer } from "./useDataLayer";
3
4
 
4
5
  interface DeriveOptions {
5
6
  control: Ref<{
@@ -8,20 +9,21 @@ interface DeriveOptions {
8
9
  data: unknown;
9
10
  }>;
10
11
  handleChange: (path: string, value: unknown) => void;
12
+ /** When projection is active, pass projectedData so the comparison
13
+ * matches the projected (unwrapped) value rather than raw scope data. */
14
+ data?: Ref<unknown> | ComputedRef<unknown>;
11
15
  }
12
16
 
13
- export function useDerive({ control, handleChange }: DeriveOptions) {
17
+ export function useDerive({ control, handleChange, data: dataOverride }: DeriveOptions) {
14
18
  // Get the root form data from JSONForms context
15
19
  const injectedFormData = inject<{ value: unknown }>("formData", {
16
20
  value: {},
17
21
  });
18
22
  const rootData = computed(() => injectedFormData.value || {});
19
23
 
20
- // Get external data from context if available
21
- const injectedExternalData = inject<{ value: unknown }>("externalData", {
22
- value: {},
23
- });
24
- const externalData = computed(() => injectedExternalData.value || {});
24
+ // Get data from the dataLayer
25
+ const dataLayerState = useDataLayer();
26
+ const dataLayerData = computed(() => dataLayerState.value || {});
25
27
 
26
28
  // Extract derive configuration from uischema options
27
29
  const deriveConfig = computed(() => {
@@ -34,9 +36,9 @@ export function useDerive({ control, handleChange }: DeriveOptions) {
34
36
  };
35
37
  });
36
38
 
37
- // Watch for changes in form data and external data and update derived field
39
+ // Watch for changes in form data and dataLayer and update derived field
38
40
  watch(
39
- [rootData, externalData, deriveConfig],
41
+ [rootData, dataLayerData, deriveConfig],
40
42
  ([data, extData, config]) => {
41
43
  if (!config.expression || config.mode !== "follow") {
42
44
  return;
@@ -48,7 +50,8 @@ export function useDerive({ control, handleChange }: DeriveOptions) {
48
50
  data,
49
51
  extData,
50
52
  );
51
- if (derivedValue !== control.value.data) {
53
+ const compareData = dataOverride ? unref(dataOverride) : control.value.data;
54
+ if (derivedValue !== compareData) {
52
55
  handleChange(control.value.path, derivedValue);
53
56
  }
54
57
  } catch (error) {
@@ -65,12 +68,12 @@ export function useDerive({ control, handleChange }: DeriveOptions) {
65
68
  function resolveDeriveExpression(
66
69
  expression: string,
67
70
  data: unknown,
68
- externalData?: unknown,
71
+ dataLayerData?: unknown,
69
72
  ): unknown {
70
- // Handle externalData() syntax
71
- if (expression.startsWith("externalData(") && expression.endsWith(")")) {
72
- const propertyPath = expression.slice(13, -1); // Remove "externalData(" and ")"
73
- return resolvePropertyPath(propertyPath, externalData);
73
+ // Handle dataLayer() syntax
74
+ if (expression.startsWith("dataLayer(") && expression.endsWith(")")) {
75
+ const propertyPath = expression.slice(10, -1); // Remove "dataLayer(" and ")"
76
+ return resolvePropertyPath(propertyPath, dataLayerData);
74
77
  }
75
78
 
76
79
  // Handle simple property paths like "country.name"
@@ -82,7 +85,7 @@ function resolveDeriveExpression(
82
85
  return resolvePropertyPath(expression, data);
83
86
  }
84
87
 
85
- // For now, we'll only support simple property paths and externalData() calls
88
+ // For now, we'll only support simple property paths and dataLayer() calls
86
89
  // Complex expressions would require a safe expression evaluator
87
90
  return resolvePropertyPath(expression, data);
88
91
  }
@@ -0,0 +1,15 @@
1
+ import { ref, computed, type Ref } from "vue";
2
+
3
+ export function useDirtyValidation(control: Ref<{ errors: string }>) {
4
+ const hasInteracted = ref(false);
5
+
6
+ const showErrors = computed(
7
+ () => hasInteracted.value && !!control.value.errors,
8
+ );
9
+
10
+ const markDirty = () => {
11
+ hasInteracted.value = true;
12
+ };
13
+
14
+ return { hasInteracted, showErrors, markDirty };
15
+ }
@@ -0,0 +1,74 @@
1
+ import { computed, type ComputedRef, type Ref } from "vue";
2
+ import {
3
+ getProjectedValue,
4
+ setProjectedValue,
5
+ getProjectedSchema,
6
+ } from "../../core/projection";
7
+
8
+ interface ProjectionControl {
9
+ data: unknown;
10
+ path: string;
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ schema: Record<string, any>;
13
+ uischema: { options?: { projection?: string; [key: string]: unknown } };
14
+ }
15
+
16
+ export interface ProjectionResult {
17
+ /** The value at the projected path (for rendering) */
18
+ projectedData: ComputedRef<unknown>;
19
+ /** The schema at the projected path (for renderer selection) */
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ projectedSchema: ComputedRef<Record<string, any>>;
22
+ /** Wrapped handleChange that writes through the projection */
23
+ handleProjectedChange: (path: string, value: unknown) => void;
24
+ /** Whether projection is active */
25
+ hasProjection: boolean;
26
+ }
27
+
28
+ /**
29
+ * Composable that wraps a JSON Forms control with projection support.
30
+ *
31
+ * When `options.projection` is set on the uischema, this composable:
32
+ * - Reads the projected sub-value from the control data
33
+ * - Resolves the projected sub-schema for renderer type resolution
34
+ * - Wraps handleChange to write back through the projection path (preserving siblings)
35
+ *
36
+ * When no projection is set, it passes through control data/schema/handleChange unchanged.
37
+ */
38
+ export function useProjection(
39
+ control: Ref<ProjectionControl>,
40
+ handleChange: (path: string, value: unknown) => void,
41
+ ): ProjectionResult {
42
+ const projection = control.value.uischema?.options?.projection as
43
+ | string
44
+ | undefined;
45
+
46
+ if (!projection) {
47
+ return {
48
+ projectedData: computed(() => control.value.data),
49
+ projectedSchema: computed(() => control.value.schema),
50
+ handleProjectedChange: handleChange,
51
+ hasProjection: false,
52
+ };
53
+ }
54
+
55
+ const projectedData = computed(() =>
56
+ getProjectedValue(control.value.data, projection),
57
+ );
58
+
59
+ const projectedSchema = computed(() =>
60
+ getProjectedSchema(control.value.schema, projection),
61
+ );
62
+
63
+ const handleProjectedChange = (path: string, value: unknown) => {
64
+ const fullValue = setProjectedValue(control.value.data, projection, value);
65
+ handleChange(path, fullValue);
66
+ };
67
+
68
+ return {
69
+ projectedData,
70
+ projectedSchema,
71
+ handleProjectedChange,
72
+ hasProjection: true,
73
+ };
74
+ }
package/src/vue/index.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  rankWith,
8
8
  isControl,
9
9
  } from "@jsonforms/core";
10
+ import { resolveScopeSchema } from "../core/resolveScope";
10
11
  import ProviderAutocomplete from "./components/ProviderAutocomplete.vue";
11
12
  import ProviderSelect from "./components/ProviderSelect.vue";
12
13
  import ProviderMultiSelect from "./components/ProviderMultiSelect.vue";
@@ -16,29 +17,21 @@ const hasProvider = (uischema: UISchemaElement) => {
16
17
  return uischema?.options?.provider !== undefined;
17
18
  };
18
19
 
20
+ // Integer fallback tester — handles nested scopes like #/properties/parent/properties/child
21
+ const isIntegerScope = (uischema: unknown, schema: unknown) => {
22
+ const ui = uischema as { type?: string; scope?: string };
23
+ if (ui?.type !== "Control" || !ui?.scope) return false;
24
+
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ const propertySchema = resolveScopeSchema(ui.scope, schema as Record<string, any>);
27
+ return propertySchema?.type === "integer";
28
+ };
29
+
19
30
  // Create specific testers for each component type
20
31
  const providerSelectTester = rankWith(
21
32
  106, // Higher than PrimeVue base (100) to ensure providers take precedence
22
33
  and(
23
- or(
24
- isStringControl,
25
- isNumberControl,
26
- and(isControl, (uischema: unknown, schema: unknown) => {
27
- const ui = uischema as { type?: string; scope?: string };
28
- const rootSchema = schema as {
29
- properties?: Record<string, { type?: string }>;
30
- };
31
-
32
- if (ui?.type !== "Control" || !ui?.scope || !rootSchema?.properties)
33
- return false;
34
-
35
- // Extract property name from scope (e.g., "#/properties/age" -> "age")
36
- const propertyName = ui.scope.replace("#/properties/", "");
37
- const propertySchema = rootSchema.properties[propertyName];
38
-
39
- return propertySchema?.type === "integer";
40
- }),
41
- ),
34
+ or(isStringControl, isNumberControl, and(isControl, isIntegerScope)),
42
35
  hasProvider,
43
36
  (uischema) => !uischema?.options?.autocomplete,
44
37
  ),
@@ -47,44 +40,21 @@ const providerSelectTester = rankWith(
47
40
  const providerAutocompleteTester = rankWith(
48
41
  107, // Higher than PrimeVue base (100) to ensure providers take precedence
49
42
  and(
50
- or(
51
- isStringControl,
52
- isNumberControl,
53
- and(isControl, (uischema: unknown, schema: unknown) => {
54
- const ui = uischema as { type?: string; scope?: string };
55
- const rootSchema = schema as {
56
- properties?: Record<string, { type?: string }>;
57
- };
58
-
59
- if (ui?.type !== "Control" || !ui?.scope || !rootSchema?.properties)
60
- return false;
61
-
62
- // Extract property name from scope (e.g., "#/properties/age" -> "age")
63
- const propertyName = ui.scope.replace("#/properties/", "");
64
- const propertySchema = rootSchema.properties[propertyName];
65
-
66
- return propertySchema?.type === "integer";
67
- }),
68
- ),
43
+ or(isStringControl, isNumberControl, and(isControl, isIntegerScope)),
69
44
  hasProvider,
70
45
  (uischema) => uischema?.options?.autocomplete === true,
71
46
  ),
72
47
  );
73
48
 
74
- // Custom array tester - check both uischema control type and schema type
49
+ // Custom array tester - supports nested scope paths
75
50
  const isArrayControl = (uischema: UISchemaElement, schema: unknown) => {
76
51
  const controlSchema = uischema as { type: string; scope?: string };
77
52
  if (controlSchema.type !== "Control" || !controlSchema.scope) {
78
53
  return false;
79
54
  }
80
55
 
81
- // Extract the property schema from the root schema
82
- const rootSchema = schema as { properties?: Record<string, unknown> };
83
- const propertyPath = controlSchema.scope.replace("#/properties/", "");
84
- const propertySchema = rootSchema?.properties?.[propertyPath] as {
85
- type?: string;
86
- };
87
-
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ const propertySchema = resolveScopeSchema(controlSchema.scope, schema as Record<string, any>);
88
58
  return propertySchema?.type === "array";
89
59
  };
90
60
 
@@ -105,6 +75,11 @@ export { primevueRenderers, registerPrimevueRenderers } from "./primevue";
105
75
  // Export individual components
106
76
  export { ProviderAutocomplete, ProviderSelect, ProviderMultiSelect };
107
77
  export { useProvider } from "./composables/useProvider";
78
+ export { useProjection } from "./composables/useProjection";
79
+ export type { ProjectionResult } from "./composables/useProjection";
80
+ export { createDataLayer, useDataLayer } from "./composables/useDataLayer";
81
+ export type { DataLayer } from "./composables/useDataLayer";
82
+ export { useDirtyValidation } from "./composables/useDirtyValidation";
108
83
  export * from "./testers";
109
84
 
110
85
  // Export individual PrimeVue components using lazy evaluation to avoid circular deps
@@ -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
  };
@@ -38,26 +41,36 @@ export default {
38
41
  import type { ControlProps } from "@jsonforms/vue";
39
42
  import { useJsonFormsControl } from "@jsonforms/vue";
40
43
  import { getCurrentInstance } from "vue";
44
+ import { useProjection } from "../composables/useProjection";
45
+ import { useDirtyValidation } from "../composables/useDirtyValidation";
41
46
  import Checkbox from "primevue/checkbox";
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 { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
47
53
 
48
- const onToggle = (val: boolean) => handleChange(control.value.path, val);
54
+ // Track user interaction errors only show after first toggle
55
+ const { showErrors, markDirty } = useDirtyValidation(control);
56
+
57
+ const onToggle = (val: boolean) => {
58
+ markDirty();
59
+ handleChange(control.value.path, val);
60
+ };
49
61
  </script>
50
62
 
51
63
  <template>
52
64
  <div class="flex items-center gap-2">
53
65
  <Checkbox
54
66
  :binary="true"
55
- :model-value="!!control.data"
67
+ :model-value="!!projectedData"
56
68
  :disabled="!control.enabled"
57
- :aria-invalid="!!control.errors || undefined"
69
+ :class="{ 'p-invalid': showErrors }"
70
+ :aria-invalid="showErrors || undefined"
58
71
  @update:model-value="onToggle"
59
72
  />
60
73
  <label v-if="control.label">{{ control.label }}</label>
61
- <small v-if="control.errors" class="p-error">{{ control.errors }}</small>
74
+ <small v-if="showErrors" class="p-error">{{ control.errors }}</small>
62
75
  </div>
63
76
  </template>
@@ -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,19 @@ 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 { useProjection } from "../composables/useProjection";
49
+ import { useDirtyValidation } from "../composables/useDirtyValidation";
45
50
  import { shouldAutoSelect } from "../utils/autoSelect";
46
51
  import Dropdown from "primevue/dropdown";
47
52
 
48
53
  // Access props from the component instance
49
54
  const instance = getCurrentInstance()!;
50
55
  const props = instance.props as unknown as ControlProps;
51
- const { control, handleChange } = useJsonFormsControl(props);
56
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
57
+ const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
52
58
 
53
59
  type Opt = { label: string; value: unknown };
54
60
  const toOptions = (schema?: JsonSchema): Opt[] => {
@@ -134,7 +140,7 @@ const options = computed(() => {
134
140
  });
135
141
 
136
142
  // Add derive functionality
137
- useDerive({ control, handleChange });
143
+ useDerive({ control, handleChange, data: projectedData });
138
144
 
139
145
  // Auto-select when provider returns only one item (enabled by default)
140
146
  watch(
@@ -145,7 +151,7 @@ watch(
145
151
  control.value.uischema?.options?.autoSelectSingle !== false,
146
152
  isLoading,
147
153
  items,
148
- currentValue: control.value.data,
154
+ currentValue: projectedData.value,
149
155
  });
150
156
 
151
157
  if (valueToSelect !== null) {
@@ -155,18 +161,12 @@ watch(
155
161
  { immediate: true }
156
162
  );
157
163
 
158
- // Track user interaction
159
- const hasInteracted = ref(false);
160
-
161
- const showErrors = computed(() => hasInteracted.value && control.value.errors);
164
+ // Track user interaction — errors only show after blur
165
+ const { showErrors, markDirty } = useDirtyValidation(control);
162
166
 
163
167
  const onSelect = (val: unknown) => {
164
168
  handleChange(control.value.path, val);
165
169
  };
166
-
167
- const onBlur = () => {
168
- hasInteracted.value = true;
169
- };
170
170
  </script>
171
171
 
172
172
  <template>
@@ -178,17 +178,17 @@ const onBlur = () => {
178
178
  {{ control.description }}
179
179
  </div>
180
180
  <Dropdown
181
- class="w-full"
181
+ :class="['w-full', { 'p-invalid': showErrors }]"
182
182
  :options="options"
183
183
  option-label="label"
184
184
  option-value="value"
185
- :model-value="control.data ?? null"
185
+ :model-value="projectedData ?? null"
186
186
  :placeholder="placeholder"
187
187
  :disabled="!control.enabled || loading"
188
- :aria-invalid="!!showErrors || undefined"
188
+ :aria-invalid="showErrors || undefined"
189
189
  :show-clear="true"
190
190
  @update:model-value="onSelect"
191
- @blur="onBlur"
191
+ @blur="markDirty"
192
192
  />
193
193
  <small v-if="error" class="p-error" role="alert"
194
194
  >Failed to load: {{ error }}</small
@@ -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,16 @@ 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 { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
51
57
 
52
58
  type Opt = { label: string; value: unknown };
53
59
  const toOptions = (schema?: JsonSchema): Opt[] => {
@@ -125,7 +131,10 @@ const options = computed(() => {
125
131
  });
126
132
 
127
133
  // Add derive functionality
128
- useDerive({ control, handleChange });
134
+ useDerive({ control, handleChange, data: projectedData });
135
+
136
+ // Track user interaction — errors only show after first change
137
+ const { showErrors, markDirty } = useDirtyValidation(control);
129
138
 
130
139
  // Auto-select when provider returns only one item (opt-in for multiselect)
131
140
  watch(
@@ -136,7 +145,7 @@ watch(
136
145
  control.value.uischema?.options?.autoSelectSingle === true,
137
146
  isLoading,
138
147
  items,
139
- currentValue: Array.isArray(control.value.data) ? control.value.data : [],
148
+ currentValue: Array.isArray(projectedData.value) ? projectedData.value : [],
140
149
  });
141
150
 
142
151
  if (valueToSelect !== null) {
@@ -165,14 +174,17 @@ const sameSet = (a: unknown[], b: unknown[]) => {
165
174
  // v-model with guard to avoid recursive updates
166
175
  const model = computed<unknown[]>({
167
176
  get() {
168
- const curr = Array.isArray(control.value.data) ? control.value.data : [];
177
+ const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
169
178
  // return a fresh copy so PrimeMultiSelect can't mutate JSONForms' array in place
170
179
  return [...curr];
171
180
  },
172
181
  set(val) {
173
182
  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);
183
+ const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
184
+ if (!sameSet(curr, next)) {
185
+ markDirty();
186
+ handleChange(control.value.path, next);
187
+ }
176
188
  },
177
189
  });
178
190
  </script>
@@ -188,21 +200,21 @@ const model = computed<unknown[]>({
188
200
 
189
201
  <MultiSelect
190
202
  v-model="model"
191
- class="w-full"
203
+ :class="['w-full', { 'p-invalid': showErrors }]"
192
204
  :options="options"
193
205
  option-label="label"
194
206
  option-value="value"
195
207
  data-key="value"
196
208
  display="chip"
197
209
  :disabled="!control.enabled || loading"
198
- :aria-invalid="!!control.errors || undefined"
210
+ :aria-invalid="showErrors || undefined"
199
211
  :placeholder="placeholder"
200
212
  />
201
213
 
202
214
  <small v-if="error" class="p-error" role="alert"
203
215
  >Failed to load: {{ error }}</small
204
216
  >
205
- <small v-else-if="control.errors" class="p-error">{{
217
+ <small v-else-if="showErrors" class="p-error">{{
206
218
  control.errors
207
219
  }}</small>
208
220
  </div>
@@ -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,17 @@ 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 { useProjection } from "../composables/useProjection";
46
+ import { useDirtyValidation } from "../composables/useDirtyValidation";
42
47
  import InputNumber from "primevue/inputnumber";
43
48
 
44
49
  // Access props from the component instance
45
50
  const instance = getCurrentInstance()!;
46
51
  const props = instance.props as unknown as ControlProps;
47
- const { control, handleChange } = useJsonFormsControl(props);
52
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
53
+ const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
48
54
 
49
55
  const options = computed(
50
56
  () =>
@@ -57,7 +63,7 @@ const placeholder = computed<string | undefined>(
57
63
  );
58
64
 
59
65
  // Add derive functionality
60
- useDerive({ control, handleChange });
66
+ useDerive({ control, handleChange, data: projectedData });
61
67
 
62
68
  // Currency and decimal configuration
63
69
  const mode = computed(() => {
@@ -95,18 +101,12 @@ const useGrouping = computed(() => {
95
101
  return options.value.useGrouping === true;
96
102
  });
97
103
 
98
- // Track user interaction
99
- const hasInteracted = ref(false);
100
-
101
- const showErrors = computed(() => hasInteracted.value && control.value.errors);
104
+ // Track user interaction — errors only show after blur
105
+ const { showErrors, markDirty } = useDirtyValidation(control);
102
106
 
103
107
  const onNumber = (val: number | null) => {
104
108
  handleChange(control.value.path, val ?? undefined);
105
109
  };
106
-
107
- const onBlur = () => {
108
- hasInteracted.value = true;
109
- };
110
110
  </script>
111
111
 
112
112
  <template>
@@ -118,19 +118,19 @@ const onBlur = () => {
118
118
  {{ control.description }}
119
119
  </div>
120
120
  <InputNumber
121
- class="w-full"
122
- input-class="w-full"
121
+ :class="['w-full', { 'p-invalid': showErrors }]"
122
+ :input-class="['w-full', { 'p-invalid': showErrors }]"
123
123
  :use-grouping="useGrouping"
124
124
  :mode="mode"
125
125
  :currency="currency"
126
126
  :min-fraction-digits="minFractionDigits"
127
127
  :max-fraction-digits="maxFractionDigits"
128
- :model-value="typeof control.data === 'number' ? control.data : null"
128
+ :model-value="typeof projectedData === 'number' ? projectedData : null"
129
129
  :placeholder="placeholder"
130
130
  :disabled="!control.enabled"
131
- :aria-invalid="!!showErrors || undefined"
131
+ :aria-invalid="showErrors || undefined"
132
132
  @update:model-value="onNumber"
133
- @blur="onBlur"
133
+ @blur="markDirty"
134
134
  />
135
135
  <small v-if="showErrors" class="p-error">{{ control.errors }}</small>
136
136
  </div>