diff --git a/Changelog.md b/Changelog.md index 443f8b324e..365f4063ea 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased: 5.0.0-rc.99] +### Changed + - FIO-9329: validateWhenHidden respects both conditionally hidden and intentionally hidden + ## 5.0.0-rc.98 ### Changed - FIO-9280 fixed validation for select boxes with valid values and when value property is not set diff --git a/src/components/_classes/component/Component.js b/src/components/_classes/component/Component.js index dd93853977..50fdce4f2c 100644 --- a/src/components/_classes/component/Component.js +++ b/src/components/_classes/component/Component.js @@ -3678,12 +3678,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, @@ -3694,7 +3688,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/src/validateWhenHidden.unit.js b/src/validateWhenHidden.unit.js new file mode 100644 index 0000000000..4634b28d6b --- /dev/null +++ b/src/validateWhenHidden.unit.js @@ -0,0 +1,532 @@ +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, + clearOnHide: false, + 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); + }); + }) +}); 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 b39eb9d7f9..d76ffdcf3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -282,9 +282,9 @@ dependencies: "@types/json-logic-js" "^2.0.7" browser-cookies "^1.2.0" - core-js "^3.37.1" - dayjs "^1.11.11" - dompurify "^3.1.4" + core-js "^3.38.0" + dayjs "^1.11.12" + dompurify "^3.1.7" eventemitter3 "^5.0.0" fast-json-patch "^3.1.1" fetch-ponyfill "^7.1.0" @@ -2299,6 +2299,11 @@ dompurify@^3.1.3, dompurify@^3.1.4: 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"