From 0905630677f738528418843645723f761cd2fc14 Mon Sep 17 00:00:00 2001 From: Lucas Koehler Date: Tue, 10 Mar 2026 12:21:19 +0100 Subject: [PATCH 1/3] vue-vanilla: Fix OneOfRenderer.vue not clearing some primitive values Replace isEmpty check with check against undefined because isEmpty always returns true for boolean values, 0, empty strings and objects without props. This lead to errors when switching from any of these to an array and trying to add elements there. Also adds unit tests for this. fixes #2506 --- .../vue-vanilla/src/complex/OneOfRenderer.vue | 3 +- .../tests/unit/complex/OneOfRenderer.spec.ts | 126 ++++++++++++++++++ packages/vue-vanilla/tests/unit/setup.ts | 14 ++ 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 packages/vue-vanilla/tests/unit/setup.ts diff --git a/packages/vue-vanilla/src/complex/OneOfRenderer.vue b/packages/vue-vanilla/src/complex/OneOfRenderer.vue index b676662bb9..4a0379f2b4 100644 --- a/packages/vue-vanilla/src/complex/OneOfRenderer.vue +++ b/packages/vue-vanilla/src/complex/OneOfRenderer.vue @@ -88,7 +88,6 @@ import { RendererProps, useJsonFormsOneOfControl, } from '@jsonforms/vue'; -import isEmpty from 'lodash/isEmpty'; import { defineComponent, inject, nextTick, ref } from 'vue'; import { useVanillaControl } from '../util'; import { ControlWrapper } from '../controls'; @@ -158,7 +157,7 @@ const controlRenderer = defineComponent({ const target = event.target as any; this.selectIndex = target.value; - if (this.control.enabled && !isEmpty(this.control.data)) { + if (this.control.enabled && this.control.data !== undefined) { this.showDialog(); nextTick(() => { this.newSelectedIndex = this.selectIndex; diff --git a/packages/vue-vanilla/tests/unit/complex/OneOfRenderer.spec.ts b/packages/vue-vanilla/tests/unit/complex/OneOfRenderer.spec.ts index d1bb27878a..045a0f5371 100644 --- a/packages/vue-vanilla/tests/unit/complex/OneOfRenderer.spec.ts +++ b/packages/vue-vanilla/tests/unit/complex/OneOfRenderer.spec.ts @@ -1,6 +1,43 @@ import { expect } from 'chai'; +import '../setup'; import { mountJsonForms } from '../util'; +const schemaWithVariousTypes = { + type: 'object', + properties: { + oneOfProp: { + title: 'Boolean or Array', + oneOf: [ + { + title: 'Boolean', + type: 'boolean', + }, + { + title: 'Array', + type: 'array', + items: { + type: 'string', + }, + }, + { + title: 'Object', + type: 'object', + properties: { + name: { + type: 'string', + }, + }, + }, + ], + }, + }, +}; + +const uischemaWithVariousTypes = { + type: 'Control', + scope: '#/properties/oneOfProp', +}; + const schema = { title: 'My Object', oneOf: [ @@ -34,6 +71,95 @@ const uischema = { }; describe('OneOfRenderer.vue', () => { + it('shows confirmation dialog when switching from boolean true to array', async () => { + const wrapper = mountJsonForms( + { oneOfProp: true }, + schemaWithVariousTypes, + uischemaWithVariousTypes + ); + const oneOfSelect = wrapper.find('select'); + const dialog = wrapper.find('dialog'); + + expect(oneOfSelect.element.value).to.equal('0'); + expect(dialog.element.open).to.be.false; + + await oneOfSelect.setValue('1'); + + expect(dialog.element.open).to.be.true; + }); + + it('shows confirmation dialog when switching from boolean false to array', async () => { + const wrapper = mountJsonForms( + { oneOfProp: false }, + schemaWithVariousTypes, + uischemaWithVariousTypes + ); + const oneOfSelect = wrapper.find('select'); + const dialog = wrapper.find('dialog'); + + expect(oneOfSelect.element.value).to.equal('0'); + expect(dialog.element.open).to.be.false; + + await oneOfSelect.setValue('1'); + + expect(dialog.element.open).to.be.true; + }); + + it('allows adding items after switching from boolean to array', async () => { + const wrapper = mountJsonForms( + { oneOfProp: true }, + schemaWithVariousTypes, + uischemaWithVariousTypes + ); + const oneOfSelect = wrapper.find('select'); + + await oneOfSelect.setValue('1'); + + const confirmButton = wrapper.find('dialog button:last-child'); + await confirmButton.trigger('click'); + + const addButton = wrapper.find('.array-list-add'); + await addButton.trigger('click'); + + expect(wrapper.vm.data).to.deep.equal({ oneOfProp: [''] }); + }); + + it('does not show confirmation dialog when switching from undefined to array', async () => { + const wrapper = mountJsonForms( + {}, + schemaWithVariousTypes, + uischemaWithVariousTypes + ); + const oneOfSelect = wrapper.find('select'); + const dialog = wrapper.find('dialog'); + + await oneOfSelect.setValue('1'); + + expect(dialog.element.open).to.be.false; + expect(wrapper.vm.data).to.deep.equal({}); + }); + + it('allows adding items after switching from object to array', async () => { + const wrapper = mountJsonForms( + { oneOfProp: { name: 'test' } }, + schemaWithVariousTypes, + uischemaWithVariousTypes + ); + const oneOfSelect = wrapper.find('select'); + + expect(oneOfSelect.element.value).to.equal('2'); + + await oneOfSelect.setValue('1'); + + const confirmButton = wrapper.find('dialog button:last-child'); + await confirmButton.trigger('click'); + + const addButton = wrapper.find('.array-list-add'); + await addButton.trigger('click'); + + expect(wrapper.vm.data).to.deep.equal({ oneOfProp: [''] }); + }); + it('render has a class', () => { const wrapper = mountJsonForms({ variant: 'b', b: 'b' }, schema, uischema); expect(wrapper.find('div.one-of').exists()).to.be.true; diff --git a/packages/vue-vanilla/tests/unit/setup.ts b/packages/vue-vanilla/tests/unit/setup.ts new file mode 100644 index 0000000000..be8bced710 --- /dev/null +++ b/packages/vue-vanilla/tests/unit/setup.ts @@ -0,0 +1,14 @@ +// JSDOM doesn't support HTMLDialogElement.showModal() and close(), so we need to polyfill/mock them +// Also see https://github.com/jsdom/jsdom/issues/3294 +if (typeof HTMLDialogElement !== 'undefined') { + HTMLDialogElement.prototype.showModal = + HTMLDialogElement.prototype.showModal || + function (this: HTMLDialogElement) { + this.setAttribute('open', ''); + }; + HTMLDialogElement.prototype.close = + HTMLDialogElement.prototype.close || + function (this: HTMLDialogElement) { + this.removeAttribute('open'); + }; +} From 961363df05b0d0d45c3b20cbbefe359325093fb8 Mon Sep 17 00:00:00 2001 From: Lucas Koehler Date: Tue, 10 Mar 2026 12:22:53 +0100 Subject: [PATCH 2/3] vue-vuetify: Fix oneOf renderers not clearing some primitive values Replace isEmpty check with check against undefined because isEmpty always returns true for boolean values, 0, empty strings and objects without props. This could lead to errors when switching from any of these to an array and trying to add elements there. --- packages/vue-vuetify/src/complex/OneOfRenderer.vue | 5 ++--- packages/vue-vuetify/src/complex/OneOfTabRenderer.vue | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/vue-vuetify/src/complex/OneOfRenderer.vue b/packages/vue-vuetify/src/complex/OneOfRenderer.vue index e687ffc30d..8ec2d19ce6 100644 --- a/packages/vue-vuetify/src/complex/OneOfRenderer.vue +++ b/packages/vue-vuetify/src/complex/OneOfRenderer.vue @@ -90,7 +90,6 @@ import { type RendererProps, useJsonFormsOneOfControl, } from '@jsonforms/vue'; -import isEmpty from 'lodash/isEmpty'; import { defineComponent, ref } from 'vue'; import { VBtn, @@ -138,7 +137,7 @@ const controlRenderer = defineComponent({ control.indexOfFittingSchema != null && control.indexOfFittingSchema != undefined // use the fitting schema if found ? control.indexOfFittingSchema - : !isEmpty(input.control.value.data) + : input.control.value.data !== undefined ? 0 // uses the first schema and report errors if not empty : null, ); @@ -176,7 +175,7 @@ const controlRenderer = defineComponent({ handleSelectChange(selectIndex: number | null): void { this.newSelectedIndex = selectIndex; - if (isEmpty(this.control.data)) { + if (this.control.data === undefined) { this.openNewTab(this.newSelectedIndex); } else { this.dialog = true; diff --git a/packages/vue-vuetify/src/complex/OneOfTabRenderer.vue b/packages/vue-vuetify/src/complex/OneOfTabRenderer.vue index cd9d884899..083a638029 100644 --- a/packages/vue-vuetify/src/complex/OneOfTabRenderer.vue +++ b/packages/vue-vuetify/src/complex/OneOfTabRenderer.vue @@ -76,7 +76,6 @@ import { type RendererProps, useJsonFormsOneOfControl, } from '@jsonforms/vue'; -import isEmpty from 'lodash/isEmpty'; import { defineComponent, ref } from 'vue'; import { VBtn, @@ -156,7 +155,7 @@ const controlRenderer = defineComponent({ // revert back to the orginal value until the dialog is done this.selectIndex = this.selectedIndex; - if (isEmpty(this.control.data)) { + if (this.control.data === undefined) { this.openNewTab(this.newSelectedIndex); } else { this.dialog = true; From df27638bce44a35b53651752d6a4d49cb63fc63f Mon Sep 17 00:00:00 2001 From: Lucas Koehler Date: Tue, 10 Mar 2026 12:21:56 +0100 Subject: [PATCH 3/3] react-material: Fix oneOf renderer not clearing some primitive values Replace isEmpty check with check against undefined because isEmpty always returns true for boolean values, 0, empty strings and objects without props. This could lead to errors when switching from any of these to an array and trying to add elements there. --- .../material-renderers/src/complex/MaterialOneOfRenderer.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/material-renderers/src/complex/MaterialOneOfRenderer.tsx b/packages/material-renderers/src/complex/MaterialOneOfRenderer.tsx index ac6aad5230..4dd8ffd750 100644 --- a/packages/material-renderers/src/complex/MaterialOneOfRenderer.tsx +++ b/packages/material-renderers/src/complex/MaterialOneOfRenderer.tsx @@ -23,7 +23,6 @@ THE SOFTWARE. */ import React, { useCallback, useMemo, useState } from 'react'; -import isEmpty from 'lodash/isEmpty'; import { TabSwitchConfirmDialog } from './TabSwitchConfirmDialog'; @@ -98,7 +97,7 @@ export const MaterialOneOfRenderer = ({ const handleTabChange = useCallback( (_event: any, newOneOfIndex: number) => { setNewSelectedIndex(newOneOfIndex); - if (isEmpty(data)) { + if (data === undefined) { openNewTab(newOneOfIndex); } else { setConfirmDialogOpen(true);