@narrative.io/jsonforms-provider-protocols 1.1.0-beta.2 → 1.1.0-beta.4

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 (35) hide show
  1. package/dist/jsonforms-provider-protocols.css +1 -1
  2. package/dist/vue/composables/useDerive.d.ts +13 -0
  3. package/dist/vue/composables/useDerive.d.ts.map +1 -0
  4. package/dist/vue/composables/useDerive.js +59 -0
  5. package/dist/vue/composables/useDerive.js.map +1 -0
  6. package/dist/vue/primevue/JfBoolean.vue.d.ts +1 -1
  7. package/dist/vue/primevue/JfEnum.vue.d.ts +1 -1
  8. package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
  9. package/dist/vue/primevue/JfEnum.vue.js +1 -1
  10. package/dist/vue/primevue/JfEnum.vue2.js +67 -9
  11. package/dist/vue/primevue/JfEnum.vue2.js.map +1 -1
  12. package/dist/vue/primevue/JfEnumArray.vue.d.ts +1 -1
  13. package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
  14. package/dist/vue/primevue/JfEnumArray.vue.js +56 -8
  15. package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
  16. package/dist/vue/primevue/JfNumber.vue.d.ts +1 -1
  17. package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
  18. package/dist/vue/primevue/JfNumber.vue.js +15 -5
  19. package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
  20. package/dist/vue/primevue/JfText.vue.d.ts +1 -1
  21. package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
  22. package/dist/vue/primevue/JfText.vue.js +94 -11
  23. package/dist/vue/primevue/JfText.vue.js.map +1 -1
  24. package/dist/vue/primevue/JfTextArea.vue.d.ts +1 -1
  25. package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
  26. package/dist/vue/primevue/JfTextArea.vue.js +12 -6
  27. package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
  28. package/dist/vue/primevue/index.d.ts +1 -1
  29. package/package.json +1 -1
  30. package/src/vue/composables/useDerive.ts +85 -0
  31. package/src/vue/primevue/JfEnum.vue +88 -9
  32. package/src/vue/primevue/JfEnumArray.vue +76 -9
  33. package/src/vue/primevue/JfNumber.vue +19 -4
  34. package/src/vue/primevue/JfText.vue +115 -8
  35. package/src/vue/primevue/JfTextArea.vue +16 -5
@@ -1,6 +1,9 @@
1
- import { defineComponent, computed, createElementBlock, openBlock, createCommentVNode, createVNode, unref, toDisplayString } from "vue";
1
+ import { defineComponent, computed, inject, ref, watch, createElementBlock, openBlock, createCommentVNode, createBlock, unref, toDisplayString } from "vue";
2
2
  import { rendererProps, useJsonFormsControl } from "@jsonforms/vue";
3
+ import { useProvider } from "../composables/useProvider.js";
4
+ import { useDerive } from "../composables/useDerive.js";
3
5
  import InputText from "primevue/inputtext";
6
+ import AutoComplete from "primevue/autocomplete";
4
7
  const _hoisted_1 = { class: "flex flex-column gap-2" };
5
8
  const _hoisted_2 = {
6
9
  key: 0,
@@ -11,7 +14,12 @@ const _hoisted_3 = {
11
14
  class: "text-color-secondary text-left"
12
15
  };
13
16
  const _hoisted_4 = {
14
- key: 2,
17
+ key: 4,
18
+ class: "p-error",
19
+ role: "alert"
20
+ };
21
+ const _hoisted_5 = {
22
+ key: 5,
15
23
  class: "p-error"
16
24
  };
17
25
  const _sfc_main = /* @__PURE__ */ defineComponent({
@@ -21,31 +29,106 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
21
29
  setup(__props) {
22
30
  const props = __props;
23
31
  const { control, handleChange } = useJsonFormsControl(props);
24
- const placeholder = computed(
25
- () => control.value.uischema?.options?.placeholder ?? control.value.description
32
+ const binding = computed(() => {
33
+ const provider = control.value.uischema?.options?.provider;
34
+ if (provider && typeof provider === "object" && !provider.load) {
35
+ return { ...provider, load: "query" };
36
+ }
37
+ return provider;
38
+ });
39
+ const deps = computed(
40
+ () => control.value.schema?.["x-provider"]?.dependsOn ?? []
26
41
  );
42
+ const depValues = computed(() => {
43
+ return deps.value.map((dep) => {
44
+ const path = dep.startsWith("#/") ? dep.slice(2) : dep;
45
+ const keys = path.replace(/\//g, ".").split(".");
46
+ let value = rootData.value;
47
+ for (const key of keys) {
48
+ if (value && typeof value === "object" && key in value) {
49
+ value = value[key];
50
+ } else {
51
+ return null;
52
+ }
53
+ }
54
+ return value;
55
+ });
56
+ });
57
+ const injectedFormData = inject("formData", { value: {} });
58
+ const rootData = computed(() => injectedFormData.value || {});
59
+ const query = ref("");
60
+ const { items, loading, error, reload } = useProvider(binding, {
61
+ data: rootData,
62
+ path: control.value.path,
63
+ uiQuery: query.value,
64
+ dependsOnValues: depValues.value
65
+ });
66
+ watch(query, () => {
67
+ if (binding.value?.load === "query") reload();
68
+ });
69
+ const placeholder = computed(() => {
70
+ if (loading.value) return "Loading…";
71
+ return control.value.uischema?.options?.placeholder ?? control.value.description;
72
+ });
73
+ const isAutocomplete = computed(() => !!binding.value);
74
+ useDerive({ control, handleChange });
75
+ const hasInteracted = ref(false);
76
+ const hasFocused = ref(false);
77
+ const showErrors = computed(() => hasInteracted.value && control.value.errors);
27
78
  function onInput(val) {
28
- const newValue = val ?? "";
29
- if ((control.value.data ?? "") !== newValue) {
79
+ const newValue = val && val.trim() !== "" ? val : void 0;
80
+ if (control.value.data !== newValue) {
30
81
  handleChange(control.value.path, newValue);
31
82
  }
32
83
  }
84
+ function onBlur() {
85
+ if (hasFocused.value) {
86
+ hasInteracted.value = true;
87
+ }
88
+ }
89
+ function onFocus() {
90
+ hasFocused.value = true;
91
+ }
92
+ const onComplete = (event) => {
93
+ query.value = event.query;
94
+ };
95
+ const onSelect = (event) => {
96
+ const newValue = event.value?.value ?? event.value;
97
+ handleChange(control.value.path, newValue);
98
+ };
33
99
  return (_ctx, _cache) => {
34
100
  return openBlock(), createElementBlock("div", _hoisted_1, [
35
101
  unref(control).label ? (openBlock(), createElementBlock("label", _hoisted_2, toDisplayString(unref(control).label), 1)) : createCommentVNode("", true),
36
102
  unref(control).description ? (openBlock(), createElementBlock("div", _hoisted_3, toDisplayString(unref(control).description), 1)) : createCommentVNode("", true),
37
- createVNode(unref(InputText), {
103
+ isAutocomplete.value ? (openBlock(), createBlock(unref(AutoComplete), {
104
+ key: 2,
105
+ class: "w-full",
106
+ "model-value": unref(control).data ?? "",
107
+ suggestions: unref(items),
108
+ "option-label": "label",
109
+ placeholder: placeholder.value,
110
+ disabled: !unref(control).enabled,
111
+ "aria-invalid": !!showErrors.value || void 0,
112
+ onComplete,
113
+ onItemSelect: onSelect,
114
+ "onUpdate:modelValue": onInput,
115
+ onBlur,
116
+ onFocus
117
+ }, null, 8, ["model-value", "suggestions", "placeholder", "disabled", "aria-invalid"])) : (openBlock(), createBlock(unref(InputText), {
118
+ key: 3,
38
119
  class: "w-full",
39
120
  "model-value": unref(control).data ?? "",
40
121
  disabled: !unref(control).enabled,
41
- "aria-invalid": !!unref(control).errors || void 0,
122
+ "aria-invalid": !!showErrors.value || void 0,
42
123
  placeholder: placeholder.value,
43
124
  autocapitalize: "off",
44
125
  autocomplete: "off",
45
126
  spellcheck: "false",
46
- "onUpdate:modelValue": onInput
47
- }, null, 8, ["model-value", "disabled", "aria-invalid", "placeholder"]),
48
- unref(control).errors ? (openBlock(), createElementBlock("small", _hoisted_4, toDisplayString(unref(control).errors), 1)) : createCommentVNode("", true)
127
+ "onUpdate:modelValue": onInput,
128
+ onBlur,
129
+ onFocus
130
+ }, null, 8, ["model-value", "disabled", "aria-invalid", "placeholder"])),
131
+ unref(error) ? (openBlock(), createElementBlock("small", _hoisted_4, "Failed: " + toDisplayString(unref(error)), 1)) : showErrors.value ? (openBlock(), createElementBlock("small", _hoisted_5, toDisplayString(unref(control).errors), 1)) : createCommentVNode("", true)
49
132
  ]);
50
133
  };
51
134
  }
@@ -1 +1 @@
1
- {"version":3,"file":"JfText.vue.js","sources":["../../../src/vue/primevue/JfText.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { ControlElement } from \"@jsonforms/core\";\nimport { rendererProps, useJsonFormsControl } from \"@jsonforms/vue\";\nimport { computed } from \"vue\";\nimport InputText from \"primevue/inputtext\";\n\ndefineOptions({ name: \"JfText\" });\n\nconst props = defineProps(rendererProps<ControlElement>());\nconst { control, handleChange } = useJsonFormsControl(props);\n\nconst placeholder = computed<string | undefined>(\n () =>\n (control.value.uischema as { options?: { placeholder?: string } })?.options\n ?.placeholder ?? control.value.description,\n);\n\nfunction onInput(val: string | undefined) {\n const newValue = val ?? \"\";\n if ((control.value.data ?? \"\") !== newValue) {\n handleChange(control.value.path, newValue);\n }\n}\n</script>\n\n<template>\n <div class=\"flex flex-column gap-2\">\n <label v-if=\"control.label\" class=\"text-color text-left\">{{\n control.label\n }}</label>\n <div v-if=\"control.description\" class=\"text-color-secondary text-left\">\n {{ control.description }}\n </div>\n <InputText\n class=\"w-full\"\n :model-value=\"control.data ?? ''\"\n :disabled=\"!control.enabled\"\n :aria-invalid=\"!!control.errors || undefined\"\n :placeholder=\"placeholder\"\n autocapitalize=\"off\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n @update:model-value=\"onInput\"\n />\n <small v-if=\"control.errors\" class=\"p-error\">{{ control.errors }}</small>\n </div>\n</template>\n"],"names":["_openBlock","_createElementBlock","_unref","_toDisplayString","_createVNode"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAQA,UAAM,QAAQ;AACd,UAAM,EAAE,SAAS,iBAAiB,oBAAoB,KAAK;AAE3D,UAAM,cAAc;AAAA,MAClB,MACG,QAAQ,MAAM,UAAqD,SAChE,eAAe,QAAQ,MAAM;AAAA,IAAA;AAGrC,aAAS,QAAQ,KAAyB;AACxC,YAAM,WAAW,OAAO;AACxB,WAAK,QAAQ,MAAM,QAAQ,QAAQ,UAAU;AAC3C,qBAAa,QAAQ,MAAM,MAAM,QAAQ;AAAA,MAC3C;AAAA,IACF;;AAIE,aAAAA,UAAA,GAAAC,mBAmBM,OAnBN,YAmBM;AAAA,QAlBSC,MAAA,OAAA,EAAQ,SAArBF,UAAA,GAAAC,mBAEU,SAFV,YAEUE,gBADRD,MAAA,OAAA,EAAQ,KAAK,GAAA,CAAA;QAEJA,MAAA,OAAA,EAAQ,eAAnBF,UAAA,GAAAC,mBAEM,OAFN,YAEME,gBADDD,MAAA,OAAA,EAAQ,WAAW,GAAA,CAAA;QAExBE,YAUEF,MAAA,SAAA,GAAA;AAAA,UATA,OAAM;AAAA,UACL,eAAaA,MAAA,OAAA,EAAQ,QAAI;AAAA,UACzB,UAAQ,CAAGA,MAAA,OAAA,EAAQ;AAAA,UACnB,gBAAY,CAAA,CAAIA,MAAA,OAAA,EAAQ,UAAU;AAAA,UAClC,aAAa,YAAA;AAAA,UACd,gBAAe;AAAA,UACf,cAAa;AAAA,UACb,YAAW;AAAA,UACV,uBAAoB;AAAA,QAAA;QAEVA,MAAA,OAAA,EAAQ,UAArBF,UAAA,GAAAC,mBAAyE,SAAzE,YAAyEE,gBAAzBD,MAAA,OAAA,EAAQ,MAAM,GAAA,CAAA;;;;;"}
1
+ {"version":3,"file":"JfText.vue.js","sources":["../../../src/vue/primevue/JfText.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { ControlElement } from \"@jsonforms/core\";\nimport { rendererProps, useJsonFormsControl } from \"@jsonforms/vue\";\nimport { computed, ref, inject, watch } from \"vue\";\nimport { useProvider } from \"../composables/useProvider\";\nimport { useDerive } from \"../composables/useDerive\";\nimport InputText from \"primevue/inputtext\";\nimport AutoComplete from \"primevue/autocomplete\";\n\ndefineOptions({ name: \"JfText\" });\n\nconst props = defineProps(rendererProps<ControlElement>());\nconst { control, handleChange } = useJsonFormsControl(props);\n\n// Provider support for autocomplete functionality\nconst binding = computed(() => {\n const provider = control.value.uischema?.options?.provider;\n // Ensure load property is set to 'query' by default for autocomplete\n if (provider && typeof provider === \"object\" && !provider.load) {\n return { ...provider, load: \"query\" };\n }\n return provider;\n});\n\nconst deps = computed(\n () =>\n ((\n (control.value.schema as Record<string, unknown>)?.[\n \"x-provider\"\n ] as Record<string, unknown>\n )?.dependsOn as string[]) ?? [],\n);\nconst depValues = computed(() => {\n return deps.value.map((dep) => {\n // Resolve dependency value from form data using JSON pointer-like path\n const path = dep.startsWith(\"#/\") ? dep.slice(2) : dep;\n const keys = path.replace(/\\//g, \".\").split(\".\");\n let value: unknown = rootData.value;\n for (const key of keys) {\n if (value && typeof value === \"object\" && key in value) {\n value = (value as Record<string, unknown>)[key];\n } else {\n return null;\n }\n }\n return value;\n });\n});\n\n// Get the root form data from JSONForms context for template URL resolution\nconst injectedFormData = inject<{ value: unknown }>(\"formData\", { value: {} });\nconst rootData = computed(() => injectedFormData.value || {});\n\nconst query = ref(\"\");\nconst { items, loading, error, reload } = useProvider(binding, {\n data: rootData,\n path: control.value.path,\n uiQuery: query.value,\n dependsOnValues: depValues.value,\n});\n\nwatch(query, () => {\n if (binding.value?.load === \"query\") reload();\n});\n\nconst placeholder = computed<string | undefined>(() => {\n if (loading.value) return \"Loading…\";\n return (\n (control.value.uischema as { options?: { placeholder?: string } })?.options\n ?.placeholder ?? control.value.description\n );\n});\n\nconst isAutocomplete = computed(() => !!binding.value);\n\n// Add derive functionality\nuseDerive({ control, handleChange });\n\n// Track user interaction\nconst hasInteracted = ref(false);\nconst hasFocused = ref(false);\n\nconst showErrors = computed(() => hasInteracted.value && control.value.errors);\n\nfunction onInput(val: string | undefined) {\n // Convert empty strings to undefined for proper required field validation\n const newValue = val && val.trim() !== \"\" ? val : undefined;\n if (control.value.data !== newValue) {\n handleChange(control.value.path, newValue);\n }\n}\n\nfunction onBlur() {\n if (hasFocused.value) {\n hasInteracted.value = true;\n }\n}\n\nfunction onFocus() {\n hasFocused.value = true;\n}\n\n// Autocomplete specific handlers\nconst onComplete = (event: { query: string }) => {\n query.value = event.query;\n};\n\nconst onSelect = (event: { value?: { value?: unknown } | unknown }) => {\n const newValue = (event.value as { value?: unknown })?.value ?? event.value;\n handleChange(control.value.path, newValue);\n};\n</script>\n\n<template>\n <div class=\"flex flex-column gap-2\">\n <label v-if=\"control.label\" class=\"text-color text-left\">{{\n control.label\n }}</label>\n <div v-if=\"control.description\" class=\"text-color-secondary text-left\">\n {{ control.description }}\n </div>\n <AutoComplete\n v-if=\"isAutocomplete\"\n class=\"w-full\"\n :model-value=\"control.data ?? ''\"\n :suggestions=\"items\"\n option-label=\"label\"\n :placeholder=\"placeholder\"\n :disabled=\"!control.enabled\"\n :aria-invalid=\"!!showErrors || undefined\"\n @complete=\"onComplete\"\n @item-select=\"onSelect\"\n @update:model-value=\"onInput\"\n @blur=\"onBlur\"\n @focus=\"onFocus\"\n />\n <InputText\n v-else\n class=\"w-full\"\n :model-value=\"control.data ?? ''\"\n :disabled=\"!control.enabled\"\n :aria-invalid=\"!!showErrors || undefined\"\n :placeholder=\"placeholder\"\n autocapitalize=\"off\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n @update:model-value=\"onInput\"\n @blur=\"onBlur\"\n @focus=\"onFocus\"\n />\n <small v-if=\"error\" class=\"p-error\" role=\"alert\">Failed: {{ error }}</small>\n <small v-else-if=\"showErrors\" class=\"p-error\">{{ control.errors }}</small>\n </div>\n</template>\n"],"names":["_openBlock","_createElementBlock","_unref","_toDisplayString","_createBlock"],"mappings":"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"}
@@ -66,10 +66,10 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
66
66
  default: undefined;
67
67
  };
68
68
  }>> & Readonly<{}>, {
69
+ config: Record<string, any>;
69
70
  enabled: boolean;
70
71
  renderers: import("@jsonforms/core").JsonFormsRendererRegistryEntry[];
71
72
  cells: import("@jsonforms/core").JsonFormsCellRendererRegistryEntry[];
72
- config: Record<string, any>;
73
73
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
74
74
  export default _default;
75
75
  //# sourceMappingURL=JfTextArea.vue.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"JfTextArea.vue.d.ts","sourceRoot":"","sources":["../../../src/vue/primevue/JfTextArea.vue"],"names":[],"mappings":"AAgDA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiItD,wBAEG"}
1
+ {"version":3,"file":"JfTextArea.vue.d.ts","sourceRoot":"","sources":["../../../src/vue/primevue/JfTextArea.vue"],"names":[],"mappings":"AA2DA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkJtD,wBAEG"}
@@ -1,4 +1,4 @@
1
- import { defineComponent, computed, createElementBlock, openBlock, createCommentVNode, createVNode, unref, toDisplayString } from "vue";
1
+ import { defineComponent, computed, ref, createElementBlock, openBlock, createCommentVNode, createVNode, unref, toDisplayString } from "vue";
2
2
  import { rendererProps, useJsonFormsControl } from "@jsonforms/vue";
3
3
  import Textarea from "primevue/textarea";
4
4
  const _hoisted_1 = { class: "flex flex-column gap-2" };
@@ -24,12 +24,17 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
24
24
  const placeholder = computed(
25
25
  () => control.value.uischema?.options?.placeholder ?? control.value.description
26
26
  );
27
+ const hasInteracted = ref(false);
28
+ const showErrors = computed(() => hasInteracted.value && control.value.errors);
27
29
  function onInput(val) {
28
- const newValue = val ?? "";
29
- if ((control.value.data ?? "") !== newValue) {
30
+ const newValue = val && val.trim() !== "" ? val : void 0;
31
+ if (control.value.data !== newValue) {
30
32
  handleChange(control.value.path, newValue);
31
33
  }
32
34
  }
35
+ function onBlur() {
36
+ hasInteracted.value = true;
37
+ }
33
38
  return (_ctx, _cache) => {
34
39
  return openBlock(), createElementBlock("div", _hoisted_1, [
35
40
  unref(control).label ? (openBlock(), createElementBlock("label", _hoisted_2, toDisplayString(unref(control).label), 1)) : createCommentVNode("", true),
@@ -38,13 +43,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
38
43
  class: "w-full",
39
44
  "model-value": unref(control).data ?? "",
40
45
  disabled: !unref(control).enabled,
41
- "aria-invalid": !!unref(control).errors || void 0,
46
+ "aria-invalid": !!showErrors.value || void 0,
42
47
  placeholder: placeholder.value,
43
48
  rows: 4,
44
49
  "auto-resize": true,
45
- "onUpdate:modelValue": onInput
50
+ "onUpdate:modelValue": onInput,
51
+ onBlur
46
52
  }, null, 8, ["model-value", "disabled", "aria-invalid", "placeholder"]),
47
- unref(control).errors ? (openBlock(), createElementBlock("small", _hoisted_4, toDisplayString(unref(control).errors), 1)) : createCommentVNode("", true)
53
+ showErrors.value ? (openBlock(), createElementBlock("small", _hoisted_4, toDisplayString(unref(control).errors), 1)) : createCommentVNode("", true)
48
54
  ]);
49
55
  };
50
56
  }
@@ -1 +1 @@
1
- {"version":3,"file":"JfTextArea.vue.js","sources":["../../../src/vue/primevue/JfTextArea.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { ControlElement } from \"@jsonforms/core\";\nimport { rendererProps, useJsonFormsControl } from \"@jsonforms/vue\";\nimport { computed } from \"vue\";\nimport Textarea from \"primevue/textarea\";\n\ndefineOptions({ name: \"JfTextArea\" });\n\nconst props = defineProps(rendererProps<ControlElement>());\nconst { control, handleChange } = useJsonFormsControl(props);\n\nconst placeholder = computed<string | undefined>(\n () =>\n (control.value.uischema as { options?: { placeholder?: string } })?.options\n ?.placeholder ?? control.value.description,\n);\n\nfunction onInput(val: string | undefined) {\n const newValue = val ?? \"\";\n if ((control.value.data ?? \"\") !== newValue) {\n handleChange(control.value.path, newValue);\n }\n}\n</script>\n\n<template>\n <div class=\"flex flex-column gap-2\">\n <label v-if=\"control.label\" class=\"text-color text-left\">{{\n control.label\n }}</label>\n <div v-if=\"control.description\" class=\"text-color-secondary text-left\">\n {{ control.description }}\n </div>\n <Textarea\n class=\"w-full\"\n :model-value=\"control.data ?? ''\"\n :disabled=\"!control.enabled\"\n :aria-invalid=\"!!control.errors || undefined\"\n :placeholder=\"placeholder\"\n :rows=\"4\"\n :auto-resize=\"true\"\n @update:model-value=\"onInput\"\n />\n <small v-if=\"control.errors\" class=\"p-error\">{{ control.errors }}</small>\n </div>\n</template>\n"],"names":["_openBlock","_createElementBlock","_unref","_toDisplayString","_createVNode"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAQA,UAAM,QAAQ;AACd,UAAM,EAAE,SAAS,iBAAiB,oBAAoB,KAAK;AAE3D,UAAM,cAAc;AAAA,MAClB,MACG,QAAQ,MAAM,UAAqD,SAChE,eAAe,QAAQ,MAAM;AAAA,IAAA;AAGrC,aAAS,QAAQ,KAAyB;AACxC,YAAM,WAAW,OAAO;AACxB,WAAK,QAAQ,MAAM,QAAQ,QAAQ,UAAU;AAC3C,qBAAa,QAAQ,MAAM,MAAM,QAAQ;AAAA,MAC3C;AAAA,IACF;;AAIE,aAAAA,UAAA,GAAAC,mBAkBM,OAlBN,YAkBM;AAAA,QAjBSC,MAAA,OAAA,EAAQ,SAArBF,UAAA,GAAAC,mBAEU,SAFV,YAEUE,gBADRD,MAAA,OAAA,EAAQ,KAAK,GAAA,CAAA;QAEJA,MAAA,OAAA,EAAQ,eAAnBF,UAAA,GAAAC,mBAEM,OAFN,YAEME,gBADDD,MAAA,OAAA,EAAQ,WAAW,GAAA,CAAA;QAExBE,YASEF,MAAA,QAAA,GAAA;AAAA,UARA,OAAM;AAAA,UACL,eAAaA,MAAA,OAAA,EAAQ,QAAI;AAAA,UACzB,UAAQ,CAAGA,MAAA,OAAA,EAAQ;AAAA,UACnB,gBAAY,CAAA,CAAIA,MAAA,OAAA,EAAQ,UAAU;AAAA,UAClC,aAAa,YAAA;AAAA,UACb,MAAM;AAAA,UACN,eAAa;AAAA,UACb,uBAAoB;AAAA,QAAA;QAEVA,MAAA,OAAA,EAAQ,UAArBF,UAAA,GAAAC,mBAAyE,SAAzE,YAAyEE,gBAAzBD,MAAA,OAAA,EAAQ,MAAM,GAAA,CAAA;;;;;"}
1
+ {"version":3,"file":"JfTextArea.vue.js","sources":["../../../src/vue/primevue/JfTextArea.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { ControlElement } from \"@jsonforms/core\";\nimport { rendererProps, useJsonFormsControl } from \"@jsonforms/vue\";\nimport { computed, ref } from \"vue\";\nimport Textarea from \"primevue/textarea\";\n\ndefineOptions({ name: \"JfTextArea\" });\n\nconst props = defineProps(rendererProps<ControlElement>());\nconst { control, handleChange } = useJsonFormsControl(props);\n\nconst placeholder = computed<string | undefined>(\n () =>\n (control.value.uischema as { options?: { placeholder?: string } })?.options\n ?.placeholder ?? control.value.description,\n);\n\n// Track user interaction\nconst hasInteracted = ref(false);\n\nconst showErrors = computed(() => hasInteracted.value && control.value.errors);\n\nfunction onInput(val: string | undefined) {\n // Convert empty strings to undefined for proper required field validation\n const newValue = val && val.trim() !== \"\" ? val : undefined;\n if (control.value.data !== newValue) {\n handleChange(control.value.path, newValue);\n }\n}\n\nfunction onBlur() {\n hasInteracted.value = true;\n}\n</script>\n\n<template>\n <div class=\"flex flex-column gap-2\">\n <label v-if=\"control.label\" class=\"text-color text-left\">{{\n control.label\n }}</label>\n <div v-if=\"control.description\" class=\"text-color-secondary text-left\">\n {{ control.description }}\n </div>\n <Textarea\n class=\"w-full\"\n :model-value=\"control.data ?? ''\"\n :disabled=\"!control.enabled\"\n :aria-invalid=\"!!showErrors || undefined\"\n :placeholder=\"placeholder\"\n :rows=\"4\"\n :auto-resize=\"true\"\n @update:model-value=\"onInput\"\n @blur=\"onBlur\"\n />\n <small v-if=\"showErrors\" class=\"p-error\">{{ control.errors }}</small>\n </div>\n</template>\n"],"names":["_openBlock","_createElementBlock","_unref","_toDisplayString","_createVNode"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAQA,UAAM,QAAQ;AACd,UAAM,EAAE,SAAS,iBAAiB,oBAAoB,KAAK;AAE3D,UAAM,cAAc;AAAA,MAClB,MACG,QAAQ,MAAM,UAAqD,SAChE,eAAe,QAAQ,MAAM;AAAA,IAAA;AAIrC,UAAM,gBAAgB,IAAI,KAAK;AAE/B,UAAM,aAAa,SAAS,MAAM,cAAc,SAAS,QAAQ,MAAM,MAAM;AAE7E,aAAS,QAAQ,KAAyB;AAExC,YAAM,WAAW,OAAO,IAAI,KAAA,MAAW,KAAK,MAAM;AAClD,UAAI,QAAQ,MAAM,SAAS,UAAU;AACnC,qBAAa,QAAQ,MAAM,MAAM,QAAQ;AAAA,MAC3C;AAAA,IACF;AAEA,aAAS,SAAS;AAChB,oBAAc,QAAQ;AAAA,IACxB;;AAIE,aAAAA,UAAA,GAAAC,mBAmBM,OAnBN,YAmBM;AAAA,QAlBSC,MAAA,OAAA,EAAQ,SAArBF,UAAA,GAAAC,mBAEU,SAFV,YAEUE,gBADRD,MAAA,OAAA,EAAQ,KAAK,GAAA,CAAA;QAEJA,MAAA,OAAA,EAAQ,eAAnBF,UAAA,GAAAC,mBAEM,OAFN,YAEME,gBADDD,MAAA,OAAA,EAAQ,WAAW,GAAA,CAAA;QAExBE,YAUEF,MAAA,QAAA,GAAA;AAAA,UATA,OAAM;AAAA,UACL,eAAaA,MAAA,OAAA,EAAQ,QAAI;AAAA,UACzB,UAAQ,CAAGA,MAAA,OAAA,EAAQ;AAAA,UACnB,gBAAY,CAAA,CAAI,WAAA,SAAc;AAAA,UAC9B,aAAa,YAAA;AAAA,UACb,MAAM;AAAA,UACN,eAAa;AAAA,UACb,uBAAoB;AAAA,UACpB;AAAA,QAAA;QAEU,WAAA,SAAbF,UAAA,GAAAC,mBAAqE,SAArE,YAAqEE,gBAAzBD,MAAA,OAAA,EAAQ,MAAM,GAAA,CAAA;;;;;"}
@@ -73,10 +73,10 @@ export declare const primevueRenderers: {
73
73
  default: undefined;
74
74
  };
75
75
  }>> & Readonly<{}>, {
76
+ config: Record<string, any>;
76
77
  enabled: boolean;
77
78
  renderers: import("@jsonforms/core").JsonFormsRendererRegistryEntry[];
78
79
  cells: import("@jsonforms/core").JsonFormsCellRendererRegistryEntry[];
79
- config: Record<string, any>;
80
80
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
81
81
  }[];
82
82
  export { JfText, JfTextArea, JfNumber, JfEnum, JfEnumArray, JfBoolean };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narrative.io/jsonforms-provider-protocols",
3
- "version": "1.1.0-beta.2",
3
+ "version": "1.1.0-beta.4",
4
4
  "description": "Dynamic data provider capabilities for JSONForms with Vue 3 integration",
5
5
  "type": "module",
6
6
  "author": "Narrative I/O",
@@ -0,0 +1,85 @@
1
+ import { computed, watch, inject, type Ref } from "vue";
2
+ import { type ControlElement } from "@jsonforms/core";
3
+
4
+ interface DeriveOptions {
5
+ control: Ref<{
6
+ uischema: ControlElement;
7
+ path: string;
8
+ data: unknown;
9
+ }>;
10
+ handleChange: (path: string, value: unknown) => void;
11
+ }
12
+
13
+ export function useDerive({ control, handleChange }: DeriveOptions) {
14
+ // Get the root form data from JSONForms context
15
+ const injectedFormData = inject<{ value: unknown }>("formData", {
16
+ value: {},
17
+ });
18
+ const rootData = computed(() => injectedFormData.value || {});
19
+
20
+ // Extract derive configuration from uischema options
21
+ const deriveConfig = computed(() => {
22
+ const options = control.value.uischema?.options as
23
+ | { derive?: string; mode?: string }
24
+ | undefined;
25
+ return {
26
+ expression: options?.derive,
27
+ mode: options?.mode || "follow", // 'follow' = auto-update, 'manual' = user controlled
28
+ };
29
+ });
30
+
31
+ // Watch for changes in form data and update derived field
32
+ watch(
33
+ [rootData, deriveConfig],
34
+ ([data, config]) => {
35
+ if (!config.expression || config.mode !== "follow") {
36
+ return;
37
+ }
38
+
39
+ try {
40
+ const derivedValue = resolveDeriveExpression(config.expression, data);
41
+ if (derivedValue !== control.value.data) {
42
+ handleChange(control.value.path, derivedValue);
43
+ }
44
+ } catch (error) {
45
+ console.warn(
46
+ `Failed to derive value for ${control.value.path}:`,
47
+ error,
48
+ );
49
+ }
50
+ },
51
+ { deep: true, immediate: true },
52
+ );
53
+ }
54
+
55
+ function resolveDeriveExpression(expression: string, data: unknown): unknown {
56
+ // Handle simple property paths like "country.name"
57
+ if (
58
+ !expression.includes("(") &&
59
+ !expression.includes("+") &&
60
+ !expression.includes("?")
61
+ ) {
62
+ return resolvePropertyPath(expression, data);
63
+ }
64
+
65
+ // For now, we'll only support simple property paths
66
+ // Complex expressions would require a safe expression evaluator
67
+ return resolvePropertyPath(expression, data);
68
+ }
69
+
70
+ function resolvePropertyPath(path: string, data: unknown): unknown {
71
+ if (!path || !data) return "";
72
+
73
+ const keys = path.split(".");
74
+ let value: unknown = data;
75
+
76
+ for (const key of keys) {
77
+ if (value && typeof value === "object" && key in value) {
78
+ value = (value as Record<string, unknown>)[key];
79
+ } else {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ return value;
85
+ }
@@ -1,7 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import type { ControlElement, JsonSchema } from "@jsonforms/core";
3
3
  import { rendererProps, useJsonFormsControl } from "@jsonforms/vue";
4
- import { computed } from "vue";
4
+ import { computed, ref, inject } from "vue";
5
+ import { useProvider } from "../composables/useProvider";
6
+ import { useDerive } from "../composables/useDerive";
5
7
  import Dropdown from "primevue/dropdown";
6
8
 
7
9
  defineOptions({ name: "JfEnum" });
@@ -26,14 +28,87 @@ const toOptions = (schema?: JsonSchema): Opt[] => {
26
28
  return [];
27
29
  };
28
30
 
29
- const placeholder = computed<string | undefined>(
31
+ // Provider support
32
+ const binding = computed(() => {
33
+ const provider = control.value.uischema?.options?.provider;
34
+ // Ensure load property is set to 'mount' by default
35
+ if (provider && typeof provider === "object" && !provider.load) {
36
+ return { ...provider, load: "mount" };
37
+ }
38
+ return provider;
39
+ });
40
+
41
+ const deps = computed(
30
42
  () =>
31
- (control.value.uischema as { options?: { placeholder?: string } })?.options
32
- ?.placeholder ?? control.value.description,
43
+ ((
44
+ (control.value.schema as Record<string, unknown>)?.[
45
+ "x-provider"
46
+ ] as Record<string, unknown>
47
+ )?.dependsOn as string[]) ?? [],
33
48
  );
49
+ const depValues = computed(() => {
50
+ return deps.value.map((dep) => {
51
+ // Resolve dependency value from form data using JSON pointer-like path
52
+ const path = dep.startsWith("#/") ? dep.slice(2) : dep;
53
+ const keys = path.replace(/\//g, ".").split(".");
54
+ let value: unknown = rootData.value;
55
+ for (const key of keys) {
56
+ if (value && typeof value === "object" && key in value) {
57
+ value = (value as Record<string, unknown>)[key];
58
+ } else {
59
+ return null;
60
+ }
61
+ }
62
+ return value;
63
+ });
64
+ });
65
+
66
+ // Get the root form data from JSONForms context for template URL resolution
67
+ const injectedFormData = inject<{ value: unknown }>("formData", { value: {} });
68
+ const rootData = computed(() => injectedFormData.value || {});
69
+
70
+ // Use provider if available, otherwise fall back to schema enum/oneOf
71
+ const {
72
+ items: providerItems,
73
+ loading,
74
+ error,
75
+ } = useProvider(binding, {
76
+ data: rootData,
77
+ path: control.value.path,
78
+ dependsOnValues: depValues.value,
79
+ });
80
+
81
+ const placeholder = computed<string | undefined>(() => {
82
+ if (loading.value) return "Loading…";
83
+ return (
84
+ (control.value.uischema as { options?: { placeholder?: string } })?.options
85
+ ?.placeholder ?? control.value.description
86
+ );
87
+ });
88
+
89
+ const options = computed(() => {
90
+ // Use provider items if available, otherwise fall back to schema enum/oneOf
91
+ if (binding.value && providerItems.value.length > 0) {
92
+ return providerItems.value;
93
+ }
94
+ return toOptions(control.value.schema);
95
+ });
34
96
 
35
- const options = computed(() => toOptions(control.value.schema));
36
- const onSelect = (val: unknown) => handleChange(control.value.path, val);
97
+ // Add derive functionality
98
+ useDerive({ control, handleChange });
99
+
100
+ // Track user interaction
101
+ const hasInteracted = ref(false);
102
+
103
+ const showErrors = computed(() => hasInteracted.value && control.value.errors);
104
+
105
+ const onSelect = (val: unknown) => {
106
+ handleChange(control.value.path, val);
107
+ };
108
+
109
+ const onBlur = () => {
110
+ hasInteracted.value = true;
111
+ };
37
112
  </script>
38
113
 
39
114
  <template>
@@ -51,12 +126,16 @@ const onSelect = (val: unknown) => handleChange(control.value.path, val);
51
126
  option-value="value"
52
127
  :model-value="control.data ?? null"
53
128
  :placeholder="placeholder"
54
- :disabled="!control.enabled"
55
- :aria-invalid="!!control.errors || undefined"
129
+ :disabled="!control.enabled || loading"
130
+ :aria-invalid="!!showErrors || undefined"
56
131
  :show-clear="true"
57
132
  @update:model-value="onSelect"
133
+ @blur="onBlur"
58
134
  />
59
- <small v-if="control.errors" class="p-error">{{ control.errors }}</small>
135
+ <small v-if="error" class="p-error" role="alert"
136
+ >Failed to load: {{ error }}</small
137
+ >
138
+ <small v-else-if="showErrors" class="p-error">{{ control.errors }}</small>
60
139
  </div>
61
140
  </template>
62
141
 
@@ -1,7 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import type { ControlElement, JsonSchema } from "@jsonforms/core";
3
3
  import { rendererProps, useJsonFormsControl } from "@jsonforms/vue";
4
- import { computed } from "vue";
4
+ import { computed, inject } from "vue";
5
+ import { useProvider } from "../composables/useProvider";
6
+ import { useDerive } from "../composables/useDerive";
5
7
  import MultiSelect from "primevue/multiselect";
6
8
 
7
9
  defineOptions({ name: "JfEnumArray" });
@@ -26,14 +28,74 @@ const toOptions = (schema?: JsonSchema): Opt[] => {
26
28
  return [];
27
29
  };
28
30
 
29
- const options = computed(() =>
30
- toOptions((control.value.schema as { items?: JsonSchema })?.items),
31
- );
32
- const placeholder = computed<string | undefined>(
31
+ // Provider support
32
+ const binding = computed(() => {
33
+ const provider = control.value.uischema?.options?.provider;
34
+ // Ensure load property is set to 'mount' by default
35
+ if (provider && typeof provider === "object" && !provider.load) {
36
+ return { ...provider, load: "mount" };
37
+ }
38
+ return provider;
39
+ });
40
+
41
+ const deps = computed(
33
42
  () =>
34
- (control.value.uischema as { options?: { placeholder?: string } })?.options
35
- ?.placeholder ?? control.value.description,
43
+ ((
44
+ (control.value.schema as Record<string, unknown>)?.[
45
+ "x-provider"
46
+ ] as Record<string, unknown>
47
+ )?.dependsOn as string[]) ?? [],
36
48
  );
49
+ const depValues = computed(() => {
50
+ return deps.value.map((dep) => {
51
+ // Resolve dependency value from form data using JSON pointer-like path
52
+ const path = dep.startsWith("#/") ? dep.slice(2) : dep;
53
+ const keys = path.replace(/\//g, ".").split(".");
54
+ let value: unknown = rootData.value;
55
+ for (const key of keys) {
56
+ if (value && typeof value === "object" && key in value) {
57
+ value = (value as Record<string, unknown>)[key];
58
+ } else {
59
+ return null;
60
+ }
61
+ }
62
+ return value;
63
+ });
64
+ });
65
+
66
+ // Get the root form data from JSONForms context for template URL resolution
67
+ const injectedFormData = inject<{ value: unknown }>("formData", { value: {} });
68
+ const rootData = computed(() => injectedFormData.value || {});
69
+
70
+ // Use provider if available, otherwise fall back to schema enum/oneOf
71
+ const {
72
+ items: providerItems,
73
+ loading,
74
+ error,
75
+ } = useProvider(binding, {
76
+ data: rootData,
77
+ path: control.value.path,
78
+ dependsOnValues: depValues.value,
79
+ });
80
+
81
+ const options = computed(() => {
82
+ // Use provider items if available, otherwise fall back to schema enum/oneOf
83
+ if (binding.value && providerItems.value.length > 0) {
84
+ return providerItems.value;
85
+ }
86
+ return toOptions((control.value.schema as { items?: JsonSchema })?.items);
87
+ });
88
+
89
+ // Add derive functionality
90
+ useDerive({ control, handleChange });
91
+
92
+ const placeholder = computed<string | undefined>(() => {
93
+ if (loading.value) return "Loading…";
94
+ return (
95
+ (control.value.uischema as { options?: { placeholder?: string } })?.options
96
+ ?.placeholder ?? control.value.description
97
+ );
98
+ });
37
99
 
38
100
  // order-insensitive shallow equality for primitive enums
39
101
  const sameSet = (a: unknown[], b: unknown[]) => {
@@ -75,11 +137,16 @@ const model = computed<unknown[]>({
75
137
  option-value="value"
76
138
  data-key="value"
77
139
  display="chip"
78
- :disabled="!control.enabled"
140
+ :disabled="!control.enabled || loading"
79
141
  :aria-invalid="!!control.errors || undefined"
80
142
  :placeholder="placeholder"
81
143
  />
82
144
 
83
- <small v-if="control.errors" class="p-error">{{ control.errors }}</small>
145
+ <small v-if="error" class="p-error" role="alert"
146
+ >Failed to load: {{ error }}</small
147
+ >
148
+ <small v-else-if="control.errors" class="p-error">{{
149
+ control.errors
150
+ }}</small>
84
151
  </div>
85
152
  </template>
@@ -1,7 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import type { ControlElement } from "@jsonforms/core";
3
3
  import { rendererProps, useJsonFormsControl } from "@jsonforms/vue";
4
- import { computed } from "vue";
4
+ import { computed, ref } from "vue";
5
+ import { useDerive } from "../composables/useDerive";
5
6
  import InputNumber from "primevue/inputnumber";
6
7
 
7
8
  defineOptions({ name: "JfNumber" });
@@ -19,6 +20,9 @@ const placeholder = computed<string | undefined>(
19
20
  () => (options.value.placeholder as string) ?? control.value.description,
20
21
  );
21
22
 
23
+ // Add derive functionality
24
+ useDerive({ control, handleChange });
25
+
22
26
  // Currency and decimal configuration
23
27
  const mode = computed(() => {
24
28
  if (options.value.currency) return "currency";
@@ -51,8 +55,18 @@ const useGrouping = computed(() => {
51
55
  return options.value.useGrouping === true;
52
56
  });
53
57
 
54
- const onNumber = (val: number | null) =>
58
+ // Track user interaction
59
+ const hasInteracted = ref(false);
60
+
61
+ const showErrors = computed(() => hasInteracted.value && control.value.errors);
62
+
63
+ const onNumber = (val: number | null) => {
55
64
  handleChange(control.value.path, val ?? undefined);
65
+ };
66
+
67
+ const onBlur = () => {
68
+ hasInteracted.value = true;
69
+ };
56
70
  </script>
57
71
 
58
72
  <template>
@@ -74,9 +88,10 @@ const onNumber = (val: number | null) =>
74
88
  :model-value="typeof control.data === 'number' ? control.data : null"
75
89
  :placeholder="placeholder"
76
90
  :disabled="!control.enabled"
77
- :aria-invalid="!!control.errors || undefined"
91
+ :aria-invalid="!!showErrors || undefined"
78
92
  @update:model-value="onNumber"
93
+ @blur="onBlur"
79
94
  />
80
- <small v-if="control.errors" class="p-error">{{ control.errors }}</small>
95
+ <small v-if="showErrors" class="p-error">{{ control.errors }}</small>
81
96
  </div>
82
97
  </template>