From 5e06ed8d9e3b3528fc1c2eda981f3e675bcc368f Mon Sep 17 00:00:00 2001 From: brendanjbond Date: Thu, 14 Nov 2024 09:07:41 -0600 Subject: [PATCH 1/2] validateWhenHidden respects all kinds of visibility --- package.json | 2 +- .../_classes/component/Component.js | 15 +- test/unit/validateWhenHidden.unit.js | 531 ++++++++++++++++++ test/util.js | 3 + yarn.lock | 15 +- 5 files changed, 553 insertions(+), 13 deletions(-) create mode 100644 test/unit/validateWhenHidden.unit.js create mode 100644 test/util.js diff --git a/package.json b/package.json index dd333b3f62..a6656d7916 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "dependencies": { "@formio/bootstrap": "3.0.0-dev.98.17ba6ea", "@formio/choices.js": "^10.2.1", - "@formio/core": "v2.1.0-dev.174.9a3c6ec", + "@formio/core": "2.1.0-dev.191.8c609ab", "@formio/text-mask-addons": "^3.8.0-formio.3", "@formio/vanilla-text-mask": "^5.1.1-formio.1", "abortcontroller-polyfill": "^1.7.5", diff --git a/src/components/_classes/component/Component.js b/src/components/_classes/component/Component.js index 62d9305828..c25b280a2c 100644 --- a/src/components/_classes/component/Component.js +++ b/src/components/_classes/component/Component.js @@ -3706,12 +3706,6 @@ export default class Component extends Element { } shouldSkipValidation(data, row, flags = {}) { - const { validateWhenHidden = false } = this.component || {}; - const forceValidOnHidden = (!this.visible || !this.checkCondition(row, data)) && !validateWhenHidden; - if (forceValidOnHidden) { - // If this component is forced valid when it is hidden, then we also need to reset the errors for this component. - this._errors = []; - } const rules = [ // Do not validate if the flags say not too. () => flags.noValidate, @@ -3722,7 +3716,14 @@ export default class Component extends Element { // Check to see if we are editing and if so, check component persistence. () => this.isValueHidden(), // Force valid if component is hidden. - () => forceValidOnHidden + () => { + if (!this.component.validateWhenHidden && (!this.visible || !this.checkCondition(row, data))) { + // If this component is forced valid when it is hidden, then we also need to reset the errors for this component. + this._errors = []; + return true; + } + return false; + } ]; return rules.some(pred => pred()); diff --git a/test/unit/validateWhenHidden.unit.js b/test/unit/validateWhenHidden.unit.js new file mode 100644 index 0000000000..a576f5b79f --- /dev/null +++ b/test/unit/validateWhenHidden.unit.js @@ -0,0 +1,531 @@ +import Harness from "../harness"; +import assert from "power-assert"; +import { Formio } from "../../src/Formio"; +import { wait } from "../util"; + +describe("Validate When Hidden behavior", function () { + describe("Simple components", function () { + it("Should not validate intentionally hidden components that do not include the `validateWhenHidden` parameter", async () => { + const formWithIntentionallyHiddenField = { + components: [ + { + type: "textfield", + key: "foo", + label: "Foo", + hidden: true, + validate: { + required: true, + } + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithIntentionallyHiddenField, + ); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it("Should not validate conditionally hidden components that do not include the `validateWhenHidden` parameter", async () => { + const formWithConditionallyHiddenField = { + components: [ + { + type: "checkbox", + key: "checkbox", + label: "Checkbox", + input: true, + }, + { + type: "textfield", + key: "foo", + label: "Foo", + conditional: { + json: { + var: "data.checkbox", + }, + }, + validate: { + required: true + } + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithConditionallyHiddenField, + ); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it("Should validate intentionally hidden components that include the `validateWhenHidden` parameter", async () => { + const formWithIntentionallyHiddenField = { + components: [ + { + type: "textfield", + key: "foo", + label: "Foo", + hidden: true, + validateWhenHidden: true, + validate: { + required: true, + }, + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithIntentionallyHiddenField, + ); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + + it("Should validate conditionally hidden components that include the `validateWhenHidden` parameter", async () => { + const formWithConditionallyHiddenField = { + components: [ + { + type: "checkbox", + key: "checkbox", + label: "Checkbox", + input: true, + }, + { + type: "textfield", + key: "foo", + label: "Foo", + conditional: { + json: { + var: "data.checkbox", + }, + }, + validateWhenHidden: true, + validate: { + required: true, + }, + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithConditionallyHiddenField, + ); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + }); + + describe("Layout components", function () { + it("Should not validate intentionally hidden components that are inside of a panel component", async function () { + const formWithIntentionallyHiddenField = { + components: [ + { + type: "panel", + key: "panel", + components: [ + { + type: "textfield", + key: "foo", + label: "Foo", + hidden: true, + validate: { + required: true, + } + }, + ], + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithIntentionallyHiddenField, + ); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it("Should validate intentionally hidden components that include the `validateWhenHidden` parameter that are inside of a panel component", async function () { + const formWithIntentionallyHiddenField = { + components: [ + { + type: "panel", + key: "panel", + components: [ + { + type: "textfield", + key: "foo", + label: "Foo", + hidden: true, + validateWhenHidden: true, + validate: { + required: true + } + }, + ], + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithIntentionallyHiddenField, + ); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + + it("Should not validate conditionally hidden components that are inside of a panel component", async function () { + const formWithConditionallyHiddenField = { + components: [ + { + type: "checkbox", + key: "checkbox", + label: "Checkbox", + input: true, + }, + { + type: "panel", + key: "panel", + components: [ + { + type: "textfield", + key: "foo", + label: "Foo", + conditional: { + json: { + var: "data.checkbox", + }, + }, + validate: { + required: true + } + }, + ], + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithConditionallyHiddenField, + ); + const textField = form.getComponent('foo'); + assert.equal(textField.visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it("Should validate conditionally hidden components that include the `validateWhenHidden` parameter that are inside of a panel component", async function () { + const formWithConditionallyHiddenField = { + components: [ + { + type: "checkbox", + key: "checkbox", + label: "Checkbox", + input: true, + }, + { + type: "panel", + key: "panel", + components: [ + { + type: "textfield", + key: "foo", + label: "Foo", + conditional: { + json: { + var: "data.checkbox", + }, + }, + validateWhenHidden: true, + validate: { + required: true + } + }, + ], + }, + ], + }; + const form = await Formio.createForm( + document.createElement("div"), + formWithConditionallyHiddenField, + ); + const textField = form.getComponent('foo'); + assert.equal(textField.visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + + it('Should not validate components that are children of an intentionally hidden panel component', async function () { + const formWithIntentionallyHiddenPanel = { + components: [ + { + type: 'panel', + key: 'panel', + hidden: true, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm( + document.createElement('div'), + formWithIntentionallyHiddenPanel + ); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it('Should validate components that are children of an intentionally hidden panel component if those components have the `validateWhenHidden` property', async function () { + const formWithIntentionallyHiddenPanel = { + components: [ + { + type: 'panel', + key: 'panel', + hidden: true, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validateWhenHidden: true, + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm( + document.createElement('div'), + formWithIntentionallyHiddenPanel + ); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + + it('Should not validate components that are children of a conditionally hidden panel component', async function () { + const formWithConditionallyHiddenPanel = { + components: [ + { + type: 'checkbox', + key: 'checkbox', + label: 'Checkbox', + input: true + }, + { + type: 'panel', + key: 'panel', + conditional: { + json: { + var: 'data.checkbox' + } + }, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm( + document.createElement('div'), + formWithConditionallyHiddenPanel + ); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it('Should validate components that are children of a conditionally hidden panel component if those components include the `validateWhenHidden` parameter', async function () { + const formWithConditionallyHiddenPanel = { + components: [ + { + type: 'checkbox', + key: 'checkbox', + label: 'Checkbox', + input: true + }, + { + type: 'panel', + key: 'panel', + conditional: { + json: { + var: 'data.checkbox' + } + }, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validateWhenHidden: true, + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm( + document.createElement('div'), + formWithConditionallyHiddenPanel + ); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + }); + + describe('Container components', function () { + it('Should not validate components that are children of an intentionally hidden container component', async function () { + const formWithIntentionallyHiddenContainer = { + components: [ + { + type: 'container', + key: 'container', + hidden: true, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm( + document.createElement('div'), + formWithIntentionallyHiddenContainer + ); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it('Should validate components that are children of an intentionally hidden container component if those components have the `validateWhenHidden` property', async function () { + const formWithIntentionallyHiddenContainer = { + components: [ + { + type: 'container', + key: 'container', + hidden: true, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validateWhenHidden: true, + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm( + document.createElement('div'), + formWithIntentionallyHiddenContainer + ); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + + it('Should not validate components that are children of a conditionally hidden container component', async function () { + const formWithConditionallyHiddenContainer = { + components: [ + { + type: 'checkbox', + key: 'checkbox', + label: 'Checkbox', + input: true + }, + { + type: 'container', + key: 'container', + conditional: { + json: { + var: 'data.checkbox' + } + }, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm(document.createElement('div'), formWithConditionallyHiddenContainer); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 0); + }); + + it('Should validate components that are children of a conditionally hidden container component if those components include the `validateWhenHidden` parameter (NOTE THAT CLEAR ON HIDE MUST BE FALSE)', async function () { + const formWithConditionallyHiddenContainer = { + components: [ + { + type: 'checkbox', + key: 'checkbox', + label: 'Checkbox', + input: true + }, + { + type: 'container', + key: 'container', + clearOnHide: false, + conditional: { + json: { + var: 'data.checkbox' + } + }, + components: [ + { + type: 'textfield', + key: 'foo', + label: 'Foo', + validateWhenHidden: true, + validate: { + required: true + } + } + ] + } + ] + }; + const form = await Formio.createForm(document.createElement('div'), formWithConditionallyHiddenContainer); + assert.equal(form.getComponent('foo').visible, false, 'The textfield should be hidden'); + const errors = form.validate(); + assert.equal(errors.length, 1); + }); + }) +}); \ No newline at end of file diff --git a/test/util.js b/test/util.js new file mode 100644 index 0000000000..e10b7c5305 --- /dev/null +++ b/test/util.js @@ -0,0 +1,3 @@ +export function wait(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2ab2a0c002..a600f82a4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -381,15 +381,15 @@ fuse.js "^6.6.2" redux "^4.2.0" -"@formio/core@v2.1.0-dev.174.9a3c6ec": - version "2.1.0-dev.174.9a3c6ec" - resolved "https://registry.yarnpkg.com/@formio/core/-/core-2.1.0-dev.174.9a3c6ec.tgz#f223b5ce4f374a9f4e922dada0af7c029320e035" - integrity sha512-QQK04dP0xBFa3vuhiOi+TUP8Zwqlg38qxzHgDmBwSlRO5XqQIObPJpSSnv2VA8H7fBWWiV2g7AErHBxugJW7Rw== +"@formio/core@2.1.0-dev.191.8c609ab": + version "2.1.0-dev.191.8c609ab" + resolved "https://registry.yarnpkg.com/@formio/core/-/core-2.1.0-dev.191.8c609ab.tgz#2e442888c60786268ca16edc7cd26c38cbd4b773" + integrity sha512-lVj8hqddIwUJiWI6/yWF0NFl/f8QPS3ek4l6h0U1SFMPmeEdWQtcBTMLKi02gHx09kDgXhYocJIbBVVpYyqFnw== dependencies: browser-cookies "^1.2.0" core-js "^3.38.0" dayjs "^1.11.12" - dompurify "^3.1.6" + dompurify "^3.1.7" eventemitter3 "^5.0.0" fast-json-patch "^3.1.1" fetch-ponyfill "^7.1.0" @@ -2498,6 +2498,11 @@ dompurify@^3.1.6: resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.6.tgz#43c714a94c6a7b8801850f82e756685300a027e2" integrity sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ== +dompurify@^3.1.7: + version "3.2.0" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.0.tgz#53c414317c51503183696fcdef6dd3f916c607ed" + integrity sha512-AMdOzK44oFWqHEi0wpOqix/fUNY707OmoeFDnbi3Q5I8uOpy21ufUA5cDJPr0bosxrflOVD/H2DMSvuGKJGfmQ== + downloadjs@^1.4.7: version "1.4.7" resolved "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz#f69f96f940e0d0553dac291139865a3cd0101e3c" From c8b05b176f3b12184ae25d4085752ebf7f803233 Mon Sep 17 00:00:00 2001 From: brendanjbond Date: Thu, 14 Nov 2024 09:52:18 -0600 Subject: [PATCH 2/2] add clearOnHide: false to fix test --- test/unit/validateWhenHidden.unit.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/validateWhenHidden.unit.js b/test/unit/validateWhenHidden.unit.js index a576f5b79f..4634b28d6b 100644 --- a/test/unit/validateWhenHidden.unit.js +++ b/test/unit/validateWhenHidden.unit.js @@ -431,6 +431,7 @@ describe("Validate When Hidden behavior", function () { type: 'container', key: 'container', hidden: true, + clearOnHide: false, components: [ { type: 'textfield', @@ -528,4 +529,4 @@ describe("Validate When Hidden behavior", function () { assert.equal(errors.length, 1); }); }) -}); \ No newline at end of file +});