diff --git a/packages/example/package.json b/packages/example/package.json
index ace5b63..76ac86f 100644
--- a/packages/example/package.json
+++ b/packages/example/package.json
@@ -34,6 +34,7 @@
"json-refs": "^3.0.15",
"lodash": "^4.17.15",
"monaco-editor": "^0.49.0",
+ "ractive": "^1.4.4",
"splitpanes": "^3.1.5",
"vue": "^3.5.0",
"vuetify": "^3.11.0"
diff --git a/packages/example/src/core/jsonschema/specification/uischema.json b/packages/example/src/core/jsonschema/specification/uischema.json
index 5093b6d..3527e93 100644
--- a/packages/example/src/core/jsonschema/specification/uischema.json
+++ b/packages/example/src/core/jsonschema/specification/uischema.json
@@ -364,6 +364,9 @@
"template": {
"type": "string"
},
+ "lang": {
+ "type": "string"
+ },
"elements": {
"$ref": "#/definitions/elements"
},
diff --git a/packages/example/src/examples/button/uischema.json b/packages/example/src/examples/button/uischema.json
index 95c3dcc..d51e670 100644
--- a/packages/example/src/examples/button/uischema.json
+++ b/packages/example/src/examples/button/uischema.json
@@ -44,6 +44,7 @@
{
"type": "TemplateLayout",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "Button",
diff --git a/packages/example/src/examples/index.ts b/packages/example/src/examples/index.ts
index 022637b..7a876d7 100644
--- a/packages/example/src/examples/index.ts
+++ b/packages/example/src/examples/index.ts
@@ -7,6 +7,7 @@ import { input as anyOfSimple } from './anyOf-simple';
import { input as anyOfWithProps } from './anyOf-with-props';
import { input as array } from './array';
import { input as arrayRestrict } from './array-restrict';
+import { input as arrayType } from './array-type';
import { input as arrayWithReorder } from './array-with-reorder';
import { input as basic } from './basic';
import { input as basicTypes } from './basic-types';
@@ -64,6 +65,7 @@ import { input as templateSlot } from './template-slot';
import { input as time } from './time';
import { input as verticalLayout } from './vertical-layout';
import { input as verticalSplitLayout } from './vertical-split-layout';
+import { input as vueTemplateLayout } from './vue-template-layout';
export * from './register';
@@ -77,6 +79,7 @@ export {
anyOfWithProps,
array,
arrayRestrict,
+ arrayType,
arrayWithReorder,
basic,
basicTypes,
@@ -134,4 +137,5 @@ export {
time,
verticalLayout,
verticalSplitLayout,
+ vueTemplateLayout,
};
diff --git a/packages/example/src/examples/job/uischema.json b/packages/example/src/examples/job/uischema.json
index 8bca81a..20d69af 100644
--- a/packages/example/src/examples/job/uischema.json
+++ b/packages/example/src/examples/job/uischema.json
@@ -1,19 +1,23 @@
{
"type": "TemplateLayout",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "TemplateLayout",
"template": "{{translate('Job Application', 'Job Application')}}
",
+ "lang": "vue",
"name": "toolbar",
"elements": [
{
"type": "TemplateLayout",
- "template": " context.fireActionEvent(\"changeLang\", {\"lang\": value})' :items='[{\"label\": context.translate(\"static-text.English\", \"English\"), \"value\": \"en\"}, {\"label\": context.translate(\"static-text.Bulgarian\", \"Bulgarian\"), \"value\": \"bg\"}]' item-title='label' item-value='value'>"
+ "template": " context.fireActionEvent(\"changeLang\", {\"lang\": value})' :items='[{\"label\": context.translate(\"static-text.English\", \"English\"), \"value\": \"en\"}, {\"label\": context.translate(\"static-text.Bulgarian\", \"Bulgarian\"), \"value\": \"bg\"}]' item-title='label' item-value='value'>",
+ "lang": "vue"
},
{
"type": "TemplateLayout",
- "template": " context.fireActionEvent(\"toggleDarkMode\")'>{{ context.vuetify.dark ? 'mdi-weather-sunny' : 'mdi-weather-night' }}"
+ "template": " context.fireActionEvent(\"toggleDarkMode\")'>{{ context.vuetify.dark ? 'mdi-weather-sunny' : 'mdi-weather-night' }}",
+ "lang": "vue"
}
]
},
@@ -27,6 +31,7 @@
{
"type": "TemplateLayout",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "Template",
@@ -43,6 +48,7 @@
{
"type": "TemplateLayout",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "Template",
@@ -59,6 +65,7 @@
{
"type": "TemplateLayout",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "Control",
@@ -75,6 +82,7 @@
{
"type": "TemplateLayout",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "ListWithDetail",
diff --git a/packages/example/src/examples/job/uischemas.json b/packages/example/src/examples/job/uischemas.json
index 5365ec2..ea4d1ee 100644
--- a/packages/example/src/examples/job/uischemas.json
+++ b/packages/example/src/examples/job/uischemas.json
@@ -3,6 +3,7 @@
"tester": "(jsonSchema, schemaPath, path) => { return NOT_APPLICABLE; }",
"uischema": {
"type": "TemplateLayout",
+ "lang": "vue",
"name": "applicant",
"template": "",
"elements": [
@@ -12,6 +13,7 @@
{
"type": "TemplateLayout",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "Control",
@@ -154,6 +156,7 @@
"uischema": {
"type": "TemplateLayout",
"name": "jobDetails",
+ "lang": "vue",
"template": "",
"elements": [
{
diff --git a/packages/example/src/examples/template-layout/uischema.json b/packages/example/src/examples/template-layout/uischema.json
index 2feee25..70b2bb3 100644
--- a/packages/example/src/examples/template-layout/uischema.json
+++ b/packages/example/src/examples/template-layout/uischema.json
@@ -1,49 +1,31 @@
{
"type": "TemplateLayout",
- "template": "
",
+ "template": "
{{> header}}{{> title}}
{{#each elements:i}}{{#if name !== 'header' && name !== 'title'}}{{> name}}{{/if}}{{/each}}
",
"elements": [
{
"type": "TemplateLayout",
- "template": "{{translate('Custom TemplateLayout with JSONForms and Vuetify', 'Custom TemplateLayout with JSONForms and Vuetify')}}",
+ "template": "{{translate('Custom TemplateLayout with JSONForms and Vuetify','Custom TemplateLayout with JSONForms and Vuetify')}}{{#each elements:i}}{{> name}}{{/each}}
",
"name": "title",
"elements": [
{
"type": "Button",
"label": "EN",
"action": "changeLang",
- "params": {
- "lang": "en"
- },
- "options": {
- "vuetify": {
- "v-btn": {
- "small": true,
- "fab": true
- }
- }
- }
+ "params": { "lang": "en" },
+ "options": { "vuetify": { "v-btn": { "small": true, "fab": true } } }
},
{
"type": "Button",
"label": "BG",
"action": "changeLang",
- "params": {
- "lang": "bg"
- },
- "options": {
- "vuetify": {
- "v-btn": {
- "small": true,
- "fab": true
- }
- }
- }
+ "params": { "lang": "bg" },
+ "options": { "vuetify": { "v-btn": { "small": true, "fab": true } } }
}
]
},
{
"type": "TemplateLayout",
- "template": "",
+ "template": "",
"name": "header"
},
{
@@ -52,23 +34,17 @@
{
"type": "HorizontalLayout",
"elements": [
- {
- "type": "Control",
- "scope": "#/properties/name"
- },
+ { "type": "Control", "scope": "#/properties/name" },
{
"type": "Control",
"scope": "#/properties/personalData/properties/age"
},
- {
- "type": "Control",
- "scope": "#/properties/birthDate"
- }
+ { "type": "Control", "scope": "#/properties/birthDate" }
]
},
{
"type": "TemplateLayout",
- "template": "{{translate('Additional Information For', 'Additional Information For')}} {{ data.name }}
"
+ "template": "{{translate('Additional Information For','Additional Information For')}} {{data.name}}
"
},
{
"type": "HorizontalLayout",
@@ -77,10 +53,7 @@
"type": "Control",
"scope": "#/properties/personalData/properties/height"
},
- {
- "type": "Control",
- "scope": "#/properties/nationality"
- },
+ { "type": "Control", "scope": "#/properties/nationality" },
{
"type": "Control",
"scope": "#/properties/occupation",
@@ -103,11 +76,25 @@
},
{
"type": "TemplateLayout",
- "template": "JSON Data
",
+ "template": "JSON Data{{#if errors}}{{/if}}
{{> jsoneditor}}
",
"elements": [
{
- "type": "TemplateLayout",
- "template": ""
+ "type": "Control",
+ "scope": "#",
+ "options": {
+ "format": "code",
+ "language": "json",
+ "convertJson": true,
+ "vuetify": {
+ "v-monaco-editor": {
+ "initActions": ["editor.action.formatDocument"],
+ "readonly": false,
+ "autoGrow": true,
+ "maxRows": 15
+ }
+ }
+ },
+ "name": "jsoneditor"
}
]
}
diff --git a/packages/example/src/examples/vue-template-layout/actions.ts b/packages/example/src/examples/vue-template-layout/actions.ts
new file mode 100644
index 0000000..a0c88c7
--- /dev/null
+++ b/packages/example/src/examples/vue-template-layout/actions.ts
@@ -0,0 +1,20 @@
+import type { ActionEvent } from '@chobantonov/jsonforms-vuetify-renderers';
+
+const changeLang = (event: ActionEvent) => {
+ if (event.context.appStore) {
+ // demo app
+ event.context.appStore.jsonforms.locale = event.params.lang;
+ } else if (event.$el.getRootNode() instanceof ShadowRoot) {
+ // web component
+ const form = (event.$el.getRootNode() as ShadowRoot).host;
+ if (form) {
+ form.setAttribute('locale', event.params.lang);
+ }
+ }
+};
+
+export const onHandleAction = (event: ActionEvent) => {
+ if (event.action === 'changeLang') {
+ event.callback = changeLang;
+ }
+};
diff --git a/packages/example/src/examples/vue-template-layout/config.json b/packages/example/src/examples/vue-template-layout/config.json
new file mode 100644
index 0000000..d264bd8
--- /dev/null
+++ b/packages/example/src/examples/vue-template-layout/config.json
@@ -0,0 +1,3 @@
+{
+ "defaultTemplateLang": "vue"
+}
diff --git a/packages/example/src/examples/vue-template-layout/data.json b/packages/example/src/examples/vue-template-layout/data.json
new file mode 100644
index 0000000..44a8183
--- /dev/null
+++ b/packages/example/src/examples/vue-template-layout/data.json
@@ -0,0 +1,10 @@
+{
+ "name": "John Doe",
+ "vegetarian": false,
+ "birthDate": "1985-06-02",
+ "personalData": {
+ "age": 34,
+ "drivingSkill": 7
+ },
+ "postalCode": "12345"
+}
diff --git a/packages/example/src/examples/vue-template-layout/i18n.json b/packages/example/src/examples/vue-template-layout/i18n.json
new file mode 100644
index 0000000..e67a3ac
--- /dev/null
+++ b/packages/example/src/examples/vue-template-layout/i18n.json
@@ -0,0 +1,86 @@
+{
+ "en": {
+ "name": {
+ "label": "Name",
+ "description": "The name of the person"
+ },
+ "vegetarian": {
+ "label": "Vegetarian",
+ "description": "Whether the person is a vegetarian"
+ },
+ "birth": {
+ "label": "Birth Date",
+ "description": ""
+ },
+ "nationality": {
+ "label": "Nationality",
+ "description": ""
+ },
+ "personal-data": {
+ "age": {
+ "label": "Age"
+ },
+ "driving": {
+ "label": "Driving Skill",
+ "description": "Indicating experience level"
+ }
+ },
+ "height": {
+ "label": "Height"
+ },
+ "occupation": {
+ "label": "Occupation",
+ "description": ""
+ },
+ "postal-code": {
+ "label": "Postal Code"
+ },
+ "error": {
+ "required": "field is required"
+ }
+ },
+ "bg": {
+ "name": {
+ "label": "Име",
+ "description": "Името на лицето"
+ },
+ "vegetarian": {
+ "label": "Вегетарианец",
+ "description": "Дали човекът е вегетарианец"
+ },
+ "birth": {
+ "label": "Рождена дата",
+ "description": ""
+ },
+ "nationality": {
+ "label": "Националност",
+ "description": ""
+ },
+ "personal-data": {
+ "age": {
+ "label": "Възраст",
+ "description": "Моля, въведете вашата възраст."
+ },
+ "driving": {
+ "label": "Шофьорски умения",
+ "description": "Показва ниво на опит"
+ }
+ },
+ "height": {
+ "label": "Височина"
+ },
+ "occupation": {
+ "label": "Професия",
+ "description": ""
+ },
+ "postal-code": {
+ "label": "Пощенски код"
+ },
+ "error": {
+ "required": "полето е задължително"
+ },
+ "Additional Information": "Допълнителна информация",
+ "Custom TemplateLayout with JSONForms and Vuetify": "Персонализиран TemplateLayout с JSONForms и Vuetify",
+ "Additional Information For": "Допълнителна информация за"
+ }
+}
diff --git a/packages/example/src/examples/vue-template-layout/index.ts b/packages/example/src/examples/vue-template-layout/index.ts
new file mode 100644
index 0000000..5f846ea
--- /dev/null
+++ b/packages/example/src/examples/vue-template-layout/index.ts
@@ -0,0 +1,25 @@
+import type { ExampleInputDescription } from '@/core/types';
+import { registerExamples } from '../register';
+import { onHandleAction } from './actions';
+import data from './data.json';
+import i18n from './i18n.json';
+import schema from './schema.json';
+import uischema from './uischema.json';
+import config from './config.json';
+
+export const input: ExampleInputDescription = {
+ schema,
+ uischema,
+ data,
+ i18n,
+ onHandleAction,
+ config,
+};
+
+registerExamples([
+ {
+ name: 'vue-template-layout',
+ label: 'Vue Template Layout',
+ input,
+ },
+]);
diff --git a/packages/example/src/examples/vue-template-layout/schema.json b/packages/example/src/examples/vue-template-layout/schema.json
new file mode 100644
index 0000000..65f55ed
--- /dev/null
+++ b/packages/example/src/examples/vue-template-layout/schema.json
@@ -0,0 +1,58 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 3,
+ "description": "Please enter your name",
+ "i18n": "name"
+ },
+ "vegetarian": {
+ "type": "boolean",
+ "i18n": "vegetarian"
+ },
+ "birthDate": {
+ "type": "string",
+ "format": "date",
+ "i18n": "birth"
+ },
+ "nationality": {
+ "type": "string",
+ "enum": ["US", "BG", "DE", "JP", "RU", "Other"],
+ "i18n": "nationality"
+ },
+ "personalData": {
+ "type": "object",
+ "properties": {
+ "age": {
+ "type": "integer",
+ "description": "Please enter your age.",
+ "i18n": "personal-data.age"
+ },
+ "height": {
+ "type": "number",
+ "i18n": "height"
+ },
+ "drivingSkill": {
+ "type": "number",
+ "maximum": 10,
+ "minimum": 1,
+ "default": 7,
+ "i18n": "personal-data.driving"
+ }
+ },
+ "required": ["age", "height"]
+ },
+ "occupation": {
+ "type": "string",
+ "i18n": "occupation"
+ },
+ "postalCode": {
+ "type": "string",
+ "maxLength": 5,
+ "i18n": "postal-code"
+ }
+ },
+ "required": ["occupation", "nationality"]
+}
diff --git a/packages/example/src/examples/vue-template-layout/uischema.json b/packages/example/src/examples/vue-template-layout/uischema.json
new file mode 100644
index 0000000..959a86a
--- /dev/null
+++ b/packages/example/src/examples/vue-template-layout/uischema.json
@@ -0,0 +1,121 @@
+{
+ "type": "TemplateLayout",
+ "template": "
",
+ "lang": "vue",
+ "elements": [
+ {
+ "type": "TemplateLayout",
+ "template": "{{translate('Custom TemplateLayout with JSONForms and Vuetify', 'Custom TemplateLayout with JSONForms and Vuetify')}}",
+ "lang": "vue",
+ "name": "title",
+ "elements": [
+ {
+ "type": "Button",
+ "label": "EN",
+ "action": "changeLang",
+ "params": {
+ "lang": "en"
+ },
+ "options": {
+ "vuetify": {
+ "v-btn": {
+ "small": true,
+ "fab": true
+ }
+ }
+ }
+ },
+ {
+ "type": "Button",
+ "label": "BG",
+ "action": "changeLang",
+ "params": {
+ "lang": "bg"
+ },
+ "options": {
+ "vuetify": {
+ "v-btn": {
+ "small": true,
+ "fab": true
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "TemplateLayout",
+ "template": "",
+ "lang": "vue",
+ "name": "header"
+ },
+ {
+ "type": "VerticalLayout",
+ "elements": [
+ {
+ "type": "HorizontalLayout",
+ "elements": [
+ {
+ "type": "Control",
+ "scope": "#/properties/name"
+ },
+ {
+ "type": "Control",
+ "scope": "#/properties/personalData/properties/age"
+ },
+ {
+ "type": "Control",
+ "scope": "#/properties/birthDate"
+ }
+ ]
+ },
+ {
+ "type": "TemplateLayout",
+ "template": "{{translate('Additional Information For', 'Additional Information For')}} {{ data.name }}
",
+ "lang": "ractive"
+ },
+ {
+ "type": "HorizontalLayout",
+ "elements": [
+ {
+ "type": "Control",
+ "scope": "#/properties/personalData/properties/height"
+ },
+ {
+ "type": "Control",
+ "scope": "#/properties/nationality"
+ },
+ {
+ "type": "Control",
+ "scope": "#/properties/occupation",
+ "options": {
+ "suggestion": [
+ "Accountant",
+ "Engineer",
+ "Freelancer",
+ "Journalism",
+ "Physician",
+ "Student",
+ "Teacher",
+ "Other"
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "TemplateLayout",
+ "template": "JSON Data
",
+ "lang": "vue",
+ "elements": [
+ {
+ "type": "TemplateLayout",
+ "template": "",
+ "lang": "vue"
+ }
+ ]
+ }
+ ]
+}
diff --git a/packages/jsonforms-vuetify-renderers/package.json b/packages/jsonforms-vuetify-renderers/package.json
index 245bc08..31dfbce 100644
--- a/packages/jsonforms-vuetify-renderers/package.json
+++ b/packages/jsonforms-vuetify-renderers/package.json
@@ -62,6 +62,7 @@
"lodash": "^4.17.21",
"maska": "^2.1.11",
"monaco-editor": "^0.49.0",
+ "ractive": "^1.4.4",
"splitpanes": "^3.2.0",
"vue": "^3.5.0",
"vuetify": "^3.11.0"
@@ -87,6 +88,7 @@
"lodash": "^4.17.21",
"maska": "^2.1.11",
"monaco-editor": "^0.49.0",
+ "ractive": "^1.4.4",
"resize-observer-polyfill": "^1.5.1",
"splitpanes": "^3.2.0",
"vue": "^3.5.0",
diff --git a/packages/jsonforms-vuetify-renderers/src/components/ResolvedJsonForms.vue b/packages/jsonforms-vuetify-renderers/src/components/ResolvedJsonForms.vue
index 900d19d..bf1883c 100644
--- a/packages/jsonforms-vuetify-renderers/src/components/ResolvedJsonForms.vue
+++ b/packages/jsonforms-vuetify-renderers/src/components/ResolvedJsonForms.vue
@@ -279,10 +279,13 @@ const createMiddlewareWrapper = (wrappedFunction: Middleware): Middleware => {
};
};
-const errorMessage = computed(() => {
+const errorTitle = computed(() => {
const message = 'Error resolving schema';
if (props.state.i18n?.translate) {
- return props.state.i18n.translate(message, message);
+ return props.state.i18n.translate(
+ 'ResolvedJsonForms.resolve_schema_error_title',
+ message,
+ );
}
return message;
});
@@ -479,8 +482,11 @@ if (!handleActionEmitter) {
justify="center"
>
-
- {{ errorMessage }}: {{ resolvedSchema.error }}
+
diff --git a/packages/jsonforms-vuetify-renderers/src/renderers/MonacoRenderer.entry.ts b/packages/jsonforms-vuetify-renderers/src/renderers/MonacoRenderer.entry.ts
index 1b769ba..e7da8d3 100644
--- a/packages/jsonforms-vuetify-renderers/src/renderers/MonacoRenderer.entry.ts
+++ b/packages/jsonforms-vuetify-renderers/src/renderers/MonacoRenderer.entry.ts
@@ -4,6 +4,7 @@ import {
optionIs,
or,
rankWith,
+ uiTypeIs,
type JsonFormsRendererRegistryEntry,
type Tester,
type UISchemaElement,
@@ -30,11 +31,24 @@ const hasStringValueOption =
export const entry: JsonFormsRendererRegistryEntry = {
renderer: monacoRenderer,
tester: rankWith(
- 2,
+ 3,
and(
- isStringControl,
optionIs('format', 'code'),
- or(hasStringValueOption('language'), hasStringValueOption(':language')),
+ or(
+ and(
+ isStringControl,
+ or(
+ hasStringValueOption('language'),
+ hasStringValueOption(':language'),
+ ),
+ ),
+ // if the language is set to 'json' we can use the monaco editor for any type
+ and(
+ and(uiTypeIs('Control')),
+ optionIs('language', 'json'),
+ optionIs('convertJson', true),
+ ),
+ ),
),
),
};
diff --git a/packages/jsonforms-vuetify-renderers/src/renderers/MonacoRenderer.vue b/packages/jsonforms-vuetify-renderers/src/renderers/MonacoRenderer.vue
index 888aed0..654efff 100644
--- a/packages/jsonforms-vuetify-renderers/src/renderers/MonacoRenderer.vue
+++ b/packages/jsonforms-vuetify-renderers/src/renderers/MonacoRenderer.vue
@@ -20,7 +20,7 @@
:persistent-hint="persistentHint()"
:required="control.required"
:error-messages="control.errors"
- :model-value="control.data"
+ :model-value="adaptModelToEditorValue(control.data)"
v-bind="vuetifyProps('v-monaco-editor')"
:language="language"
@update:model-value="onChange"
@@ -55,7 +55,7 @@
:placeholder="appliedOptions.placeholder"
:required="control.required"
:error-messages="control.errors"
- :model-value="control.data"
+ :model-value="adaptModelToEditorValue(control.data)"
v-bind="vuetifyProps('v-monaco-editor')"
:language="language"
@update:model-value="onChange"
@@ -87,6 +87,7 @@ import {
useVuetifyControl,
} from '@jsonforms/vue-vuetify';
import { defineComponent, ref, nextTick, watch } from 'vue';
+import { computed } from 'vue';
import { defineAsyncComponent } from 'vue';
import {
VBtn,
@@ -123,14 +124,50 @@ const controlRenderer = defineComponent({
const editorRef = ref(null);
const clearValue = determineClearValue('');
- const adaptValue = (value: any) => value || clearValue;
- const vuetifyControl = useVuetifyControl(
- useJsonFormsControl(props),
- adaptValue,
- 300,
+ const control = useJsonFormsControl(props);
+ const language = computed(() => {
+ const langOption = control.control.value.uischema.options?.[':language'];
+ if (langOption) {
+ const rootData = jsonforms.core?.data;
+ return Resolve.data(rootData, langOption);
+ }
+ return control.control.value.uischema.options?.language;
+ });
+
+ const convertJson = computed(
+ () => control.control.value.uischema.options?.convertJson ?? false,
);
+ const adaptModelToEditorValue = (value: any): string => {
+ if (language.value === 'json' && convertJson.value) {
+ return value != undefined ? JSON.stringify(value, null, 2) : '';
+ }
+
+ if (value === undefined || value === null) {
+ return '';
+ }
+
+ return typeof value === 'string' ? value : String(value);
+ };
+
+ const vuetifyControl = useVuetifyControl(control, (v) => v, 300);
+
+ const onChange = (value: string) => {
+ let model = value || clearValue;
+
+ if (language.value === 'json' && convertJson.value) {
+ try {
+ model = JSON.parse(value);
+ } catch {
+ // do not update the model if JSON is invalid
+ return;
+ }
+ }
+
+ vuetifyControl.onChange(model);
+ };
+
const toggleMaximize = () => {
isMaximized.value = !isMaximized.value;
nextTick(() => {
@@ -148,23 +185,16 @@ const controlRenderer = defineComponent({
return {
...vuetifyControl,
+ onChange,
jsonforms,
icons,
isMaximized,
editorRef,
toggleMaximize,
+ language,
+ adaptModelToEditorValue,
};
},
- computed: {
- language(): string | undefined {
- const language = this.control.uischema.options?.[':language'];
- if (language) {
- const rootData = this.jsonforms.core?.data;
- return Resolve.data(rootData, language);
- }
- return this.control.uischema.options?.language;
- },
- },
});
export default controlRenderer;
diff --git a/packages/jsonforms-vuetify-renderers/src/renderers/TemplateLayoutRenderer.entry.ts b/packages/jsonforms-vuetify-renderers/src/renderers/TemplateLayoutRenderer.entry.ts
index 3566c29..0817646 100644
--- a/packages/jsonforms-vuetify-renderers/src/renderers/TemplateLayoutRenderer.entry.ts
+++ b/packages/jsonforms-vuetify-renderers/src/renderers/TemplateLayoutRenderer.entry.ts
@@ -1,11 +1,32 @@
import {
+ and,
rankWith,
uiTypeIs,
type JsonFormsRendererRegistryEntry,
+ type JsonSchema,
+ type Tester,
+ type TesterContext,
+ type UISchemaElement,
} from '@jsonforms/core';
import templateLayoutRenderer from './TemplateLayoutRenderer.vue';
+export const langIs =
+ (expected: string): Tester =>
+ (
+ uischema: UISchemaElement & { lang?: string },
+ schema: JsonSchema,
+ context: TesterContext,
+ ): boolean => {
+ return (
+ uischema.lang === expected ||
+ (uischema.lang === undefined &&
+ (!context.config ||
+ context.config.defaultTemplateLang === expected ||
+ context.config.defaultTemplateLang === undefined))
+ );
+ };
+
export const entry: JsonFormsRendererRegistryEntry = {
renderer: templateLayoutRenderer,
- tester: rankWith(2, uiTypeIs('TemplateLayout')),
+ tester: rankWith(2, and(uiTypeIs('TemplateLayout'), langIs('ractive'))),
};
diff --git a/packages/jsonforms-vuetify-renderers/src/renderers/TemplateLayoutRenderer.vue b/packages/jsonforms-vuetify-renderers/src/renderers/TemplateLayoutRenderer.vue
index 4925aa1..113c5ad 100644
--- a/packages/jsonforms-vuetify-renderers/src/renderers/TemplateLayoutRenderer.vue
+++ b/packages/jsonforms-vuetify-renderers/src/renderers/TemplateLayoutRenderer.vue
@@ -3,33 +3,37 @@
:disabled="!vuetifyProps('defaults')"
:defaults="vuetifyProps('defaults')"
>
-
- Template Error: {{ templateError }}
-
-
-
+
+
+
+
-
+
+
diff --git a/packages/jsonforms-vuetify-renderers/src/renderers/VueTemplateLayoutRenderer.entry.ts b/packages/jsonforms-vuetify-renderers/src/renderers/VueTemplateLayoutRenderer.entry.ts
new file mode 100644
index 0000000..ff71b7f
--- /dev/null
+++ b/packages/jsonforms-vuetify-renderers/src/renderers/VueTemplateLayoutRenderer.entry.ts
@@ -0,0 +1,31 @@
+import {
+ and,
+ rankWith,
+ uiTypeIs,
+ type JsonFormsRendererRegistryEntry,
+ type JsonSchema,
+ type Tester,
+ type TesterContext,
+ type UISchemaElement,
+} from '@jsonforms/core';
+
+import vueTemplateLayoutRenderer from './VueTemplateLayoutRenderer.vue';
+export const langIs =
+ (expected: string): Tester =>
+ (
+ uischema: UISchemaElement & { lang?: string },
+ schema: JsonSchema,
+ context: TesterContext,
+ ): boolean => {
+ return (
+ uischema.lang === expected ||
+ (uischema.lang === undefined &&
+ context.config &&
+ context.config.defaultTemplateLang === expected)
+ );
+ };
+
+export const entry: JsonFormsRendererRegistryEntry = {
+ renderer: vueTemplateLayoutRenderer,
+ tester: rankWith(2, and(uiTypeIs('TemplateLayout'), langIs('vue'))),
+};
diff --git a/packages/jsonforms-vuetify-renderers/src/renderers/VueTemplateLayoutRenderer.vue b/packages/jsonforms-vuetify-renderers/src/renderers/VueTemplateLayoutRenderer.vue
new file mode 100644
index 0000000..373e764
--- /dev/null
+++ b/packages/jsonforms-vuetify-renderers/src/renderers/VueTemplateLayoutRenderer.vue
@@ -0,0 +1,227 @@
+
+
+
+ Template Error: {{ templateError }}
+
+
+
+
+
+
+
+
diff --git a/packages/jsonforms-vuetify-renderers/src/renderers/index.ts b/packages/jsonforms-vuetify-renderers/src/renderers/index.ts
index bb0cca3..a58a360 100644
--- a/packages/jsonforms-vuetify-renderers/src/renderers/index.ts
+++ b/packages/jsonforms-vuetify-renderers/src/renderers/index.ts
@@ -7,6 +7,7 @@ import { default as MonacoRenderer } from './MonacoRenderer.vue';
import { default as NullControlRenderer } from './NullControlRenderer.vue';
import { default as SlotRenderer } from './SlotRenderer.vue';
import { default as TemplateLabelRenderer } from './TemplateLabelRenderer.vue';
+import { default as VueTemplateLayoutRenderer } from './VueTemplateLayoutRenderer.vue';
import { default as TemplateLayoutRenderer } from './TemplateLayoutRenderer.vue';
import { default as TemplateRenderer } from './TemplateRenderer.vue';
import { default as SplitLayoutRenderer } from './SplitLayoutRenderer.vue';
@@ -21,6 +22,7 @@ import { entry as monacoRendererEntry } from './MonacoRenderer.entry';
import { entry as nullControlRendererEntry } from './NullControlRenderer.entry';
import { entry as slotRendererEntry } from './SlotRenderer.entry';
import { entry as templateLabelRendererEntry } from './TemplateLabelRenderer.entry';
+import { entry as vueTemplateLayoutRendererEntry } from './VueTemplateLayoutRenderer.entry';
import { entry as templateLayoutRendererEntry } from './TemplateLayoutRenderer.entry';
import { entry as templateRendererEntry } from './TemplateRenderer.entry';
import { entry as splitLayoutRendererEntry } from './SplitLayoutRenderer.entry';
@@ -36,6 +38,7 @@ export const extraVuetifyRenderers = [
nullControlRendererEntry,
slotRendererEntry,
templateLabelRendererEntry,
+ vueTemplateLayoutRendererEntry,
templateLayoutRendererEntry,
templateRendererEntry,
splitLayoutRendererEntry,
@@ -52,6 +55,7 @@ export {
NullControlRenderer,
SlotRenderer,
TemplateLabelRenderer,
+ VueTemplateLayoutRenderer,
TemplateLayoutRenderer,
TemplateRenderer,
SplitLayoutRenderer,
diff --git a/packages/jsonforms-vuetify-renderers/src/util/ractive-template-controller.ts b/packages/jsonforms-vuetify-renderers/src/util/ractive-template-controller.ts
new file mode 100644
index 0000000..b5336f3
--- /dev/null
+++ b/packages/jsonforms-vuetify-renderers/src/util/ractive-template-controller.ts
@@ -0,0 +1,99 @@
+import Ractive, {
+ type Data,
+ type DataFn,
+ type ParsedTemplate,
+ type Partial,
+ type Registry,
+} from 'ractive';
+
+export interface SlotPlaceholder {
+ name: string;
+ el: HTMLElement;
+}
+
+export class RactiveTemplateController = Ractive> {
+ private ractive: Ractive | null = null;
+ private onMountSlots: (slots: SlotPlaceholder[]) => void;
+ private container: HTMLElement | null = null;
+ private template: ParsedTemplate | null = null;
+ private data: any = null;
+
+ constructor(onMountSlots: (slots: SlotPlaceholder[]) => void) {
+ this.onMountSlots = onMountSlots;
+ }
+
+ async destroy() {
+ if (this.ractive) {
+ this.ractive.off();
+ await this.ractive.teardown().catch(() => {});
+ this.ractive = null;
+ }
+ }
+
+ updateData(keyPath: string, value: any) {
+ if (this.ractive) {
+ this.ractive.set(keyPath, value);
+ }
+ }
+
+ async setup(
+ container: HTMLElement,
+ template: string,
+ data: (Data | DataFn) & { elements?: { name: string }[] },
+ visible: boolean,
+ ) {
+ this.container = container;
+ this.template = Ractive.parse(template);
+ this.data = data;
+
+ if (!visible) {
+ await this.destroy();
+ return;
+ }
+
+ await this.render();
+ }
+
+ async updateVisibility(visible: boolean) {
+ if (visible && !this.ractive) {
+ await this.render();
+ } else if (!visible && this.ractive) {
+ await this.destroy();
+ }
+ }
+
+ private async render() {
+ if (!this.container) return;
+
+ await this.destroy();
+ this.container.innerHTML = '';
+
+ const partials: Registry = {};
+
+ this.data.elements?.forEach((el: any) => {
+ partials[el.name] = ``;
+ });
+
+ this.ractive = new Ractive({
+ el: this.container,
+ template: this.template!,
+ partials,
+ data: this.data,
+ twoway: true,
+ lazy: false,
+ });
+
+ const runMount = () => {
+ const els = this.container!.querySelectorAll('[data-slot]');
+ const slots = Array.from(els).map((el) => ({
+ name: el.getAttribute('data-slot')!,
+ el: el as HTMLElement,
+ }));
+ this.onMountSlots(slots);
+ };
+
+ this.ractive.on('render', runMount);
+ this.ractive.on('update', runMount);
+ runMount();
+ }
+}
diff --git a/packages/jsonforms-vuetify-renderers/src/util/util.ts b/packages/jsonforms-vuetify-renderers/src/util/util.ts
index f2110ea..05b9618 100644
--- a/packages/jsonforms-vuetify-renderers/src/util/util.ts
+++ b/packages/jsonforms-vuetify-renderers/src/util/util.ts
@@ -1,5 +1,6 @@
import {
type JsonFormsUISchemaRegistryEntry,
+ type Layout,
type UISchemaElement,
type UISchemaTester,
type ValidateFunctionContext,
@@ -176,3 +177,16 @@ export const getLightDarkTheme = (
return newTheme;
};
+
+export interface TemplateLayout extends Layout {
+ type: 'TemplateLayout';
+ /**
+ * The template string.
+ */
+ template: string;
+
+ /**
+ * The template language.
+ */
+ lang: string;
+}
diff --git a/packages/jsonforms-vuetify-tooling/schemas/uischema.json b/packages/jsonforms-vuetify-tooling/schemas/uischema.json
index 533ff9e..777bf47 100644
--- a/packages/jsonforms-vuetify-tooling/schemas/uischema.json
+++ b/packages/jsonforms-vuetify-tooling/schemas/uischema.json
@@ -364,6 +364,9 @@
"template": {
"type": "string"
},
+ "lang": {
+ "type": "string"
+ },
"elements": {
"$ref": "#/definitions/elements"
},
diff --git a/packages/jsonforms-vuetify-webcomponent/package.json b/packages/jsonforms-vuetify-webcomponent/package.json
index ba374e1..0d5893e 100644
--- a/packages/jsonforms-vuetify-webcomponent/package.json
+++ b/packages/jsonforms-vuetify-webcomponent/package.json
@@ -62,6 +62,7 @@
"json-refs": "^3.0.15",
"lodash": "^4.17.15",
"monaco-editor": "^0.49.0",
+ "ractive": "^1.4.4",
"shady-css-parser": "^0.1.0",
"vue": "^3.5.0",
"vue-plugin-load-script": "^2.1.1",
diff --git a/packages/jsonforms-vuetify-webcomponent/src/examples/basic/uischema.json b/packages/jsonforms-vuetify-webcomponent/src/examples/basic/uischema.json
index 2c8c644..5ae279e 100644
--- a/packages/jsonforms-vuetify-webcomponent/src/examples/basic/uischema.json
+++ b/packages/jsonforms-vuetify-webcomponent/src/examples/basic/uischema.json
@@ -1,24 +1,29 @@
{
"type": "TemplateLayout",
"template": "{{translate('Vuetify JSON Forms Demo', 'Vuetify JSON Forms Demo')}}",
+ "lang": "vue",
"elements": [
{
"type": "TemplateLayout",
"template": "
",
+ "lang": "vue",
"elements": [
{
"type": "TemplateLayout",
- "template": " context.fireActionEvent(\"changeLang\", {\"lang\": value})' :items='[{\"label\": \"English\", \"value\": \"en\"}, {\"label\": \"Bulgarian\", \"value\": \"bg\"}]' item-title='label' item-value='value'>"
+ "template": " context.fireActionEvent(\"changeLang\", {\"lang\": value})' :items='[{\"label\": \"English\", \"value\": \"en\"}, {\"label\": \"Bulgarian\", \"value\": \"bg\"}]' item-title='label' item-value='value'>",
+ "lang": "vue"
},
{
"type": "TemplateLayout",
- "template": " context.fireActionEvent(\"toggleDarkMode\")'>{{ context.vuetify.dark ? 'mdi-weather-sunny' : 'mdi-weather-night' }}"
+ "template": " context.fireActionEvent(\"toggleDarkMode\")'>{{ context.vuetify.dark ? 'mdi-weather-sunny' : 'mdi-weather-night' }}",
+ "lang": "vue"
}
]
},
{
"type": "TemplateLayout",
"template": "Example",
+ "lang": "vue",
"elements": [
{
"type": "VerticalLayout",
@@ -139,7 +144,8 @@
"elements": [
{
"type": "TemplateLayout",
- "template": ""
+ "template": "",
+ "lang": "vue"
}
]
},
@@ -148,7 +154,8 @@
"elements": [
{
"type": "TemplateLayout",
- "template": "{{ context.uidata.errorDialog.title }}{{ context.uidata.errorDialog.text }} OK "
+ "template": "{{ context.uidata.errorDialog.title }}{{ context.uidata.errorDialog.text }} OK ",
+ "lang": "vue"
}
]
}
diff --git a/packages/jsonforms-vuetify-webcomponent/src/examples/job/uischema.json b/packages/jsonforms-vuetify-webcomponent/src/examples/job/uischema.json
index 01cb231..903547d 100644
--- a/packages/jsonforms-vuetify-webcomponent/src/examples/job/uischema.json
+++ b/packages/jsonforms-vuetify-webcomponent/src/examples/job/uischema.json
@@ -1,19 +1,23 @@
{
"type": "TemplateLayout",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "TemplateLayout",
"template": "{{translate('Job Application', 'Job Application')}}
",
+ "lang": "vue",
"name": "toolbar",
"elements": [
{
"type": "TemplateLayout",
- "template": " context.fireActionEvent(\"changeLang\", {\"lang\": value})' :items='[{\"label\": context.translate(\"static-text.English\", \"English\"), \"value\": \"en\"}, {\"label\": context.translate(\"static-text.Bulgarian\", \"Bulgarian\"), \"value\": \"bg\"}]' item-title='label' item-value='value'>"
+ "template": " context.fireActionEvent(\"changeLang\", {\"lang\": value})' :items='[{\"label\": context.translate(\"static-text.English\", \"English\"), \"value\": \"en\"}, {\"label\": context.translate(\"static-text.Bulgarian\", \"Bulgarian\"), \"value\": \"bg\"}]' item-title='label' item-value='value'>",
+ "lang": "vue"
},
{
"type": "TemplateLayout",
- "template": " context.fireActionEvent(\"toggleDarkMode\")'>{{ context.vuetify.dark ? 'mdi-weather-sunny' : 'mdi-weather-night' }}"
+ "template": " context.fireActionEvent(\"toggleDarkMode\")'>{{ context.vuetify.dark ? 'mdi-weather-sunny' : 'mdi-weather-night' }}",
+ "lang": "vue"
}
]
},
@@ -27,6 +31,7 @@
{
"type": "TemplateLayout",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "Template",
@@ -43,6 +48,7 @@
{
"type": "TemplateLayout",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "Template",
@@ -59,6 +65,7 @@
{
"type": "TemplateLayout",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "Control",
@@ -75,6 +82,7 @@
{
"type": "TemplateLayout",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "ListWithDetail",
diff --git a/packages/jsonforms-vuetify-webcomponent/src/examples/job/uischemas.json b/packages/jsonforms-vuetify-webcomponent/src/examples/job/uischemas.json
index 5365ec2..1962b6a 100644
--- a/packages/jsonforms-vuetify-webcomponent/src/examples/job/uischemas.json
+++ b/packages/jsonforms-vuetify-webcomponent/src/examples/job/uischemas.json
@@ -5,6 +5,7 @@
"type": "TemplateLayout",
"name": "applicant",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "HorizontalLayout",
@@ -12,6 +13,7 @@
{
"type": "TemplateLayout",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "Control",
@@ -155,6 +157,7 @@
"type": "TemplateLayout",
"name": "jobDetails",
"template": "",
+ "lang": "vue",
"elements": [
{
"type": "Control",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a523f65..f5fb287 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -176,6 +176,9 @@ importers:
monaco-editor:
specifier: ^0.49.0
version: 0.49.0
+ ractive:
+ specifier: ^1.4.4
+ version: 1.4.4
splitpanes:
specifier: ^3.1.5
version: 3.2.0(vue@3.5.13(typescript@5.4.5))
@@ -271,6 +274,9 @@ importers:
monaco-editor:
specifier: ^0.49.0
version: 0.49.0
+ ractive:
+ specifier: ^1.4.4
+ version: 1.4.4
resize-observer-polyfill:
specifier: ^1.5.1
version: 1.5.1
@@ -392,6 +398,9 @@ importers:
monaco-editor:
specifier: ^0.49.0
version: 0.49.0
+ ractive:
+ specifier: ^1.4.4
+ version: 1.4.4
shady-css-parser:
specifier: ^0.1.0
version: 0.1.0
@@ -4452,6 +4461,11 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ ractive@1.4.4:
+ resolution: {integrity: sha512-FFKmPox5nJQeBNPWLl2dsVC47z44O0NNx2vtGwjWYTtICIyVDyoKex8AAV99Cv07j6njFm3O70Pi96QxVu5rzA==}
+ engines: {node: '>=4.0.0', npm: '>=2.14.2'}
+ hasBin: true
+
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
@@ -10160,6 +10174,8 @@ snapshots:
queue-microtask@1.2.3: {}
+ ractive@1.4.4: {}
+
randombytes@2.1.0:
dependencies:
safe-buffer: 5.2.1