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..912fa8e0de 100644 --- a/src/components/_classes/component/Component.js +++ b/src/components/_classes/component/Component.js @@ -367,11 +367,19 @@ export default class Component extends Element { // Needs for Nextgen Rules Engine this.resetCaches(); + /** + * Determines if this component is conditionally hidden. Should generally not be set outside of conditional logic pipeline. + * This is necessary because of clearOnHide behavior that only clears when conditionally hidden - we need to track + * conditionallyHidden separately from "regular" visibility. + */ + this._parentConditionallyHidden = this.options.hasOwnProperty('parentConditionallyHidden') ? this.options.parentConditionallyHidden : false; + this._conditionallyHidden = this.checkConditionallyHidden(null, data) || this._parentConditionallyHidden; + /** * Determines if this component is visible, or not. */ this._parentVisible = this.options.hasOwnProperty('parentVisible') ? this.options.parentVisible : true; - this._visible = this._parentVisible && this.conditionallyVisible(null, data); + this._visible = this._parentVisible && (this.hasCondition() ? !this._conditionallyHidden : !this.component.hidden); this._parentDisabled = false; /** @@ -447,7 +455,7 @@ export default class Component extends Element { if (this.allowData && this.key) { this.options.name += `[${this.key}]`; // If component is visible or not set to clear on hide, set the default value. - if (this.visible || !this.component.clearOnHide) { + if (!(this.conditionallyHidden && this.component.clearOnHide)) { if (!this.hasValue()) { if (this.shouldAddDefaultValue) { this.dataValue = this.defaultValue; @@ -535,7 +543,8 @@ export default class Component extends Element { init() { this.disabled = this.shouldDisabled; - this._visible = this.conditionallyVisible(null, null); + this._conditionallyHidden = this.checkConditionallyHidden(); + this._visible = (this.hasCondition() ? !this.conditionallyHidden : !this.component.hidden); if (this.component.addons?.length) { this.component.addons.forEach((addon) => this.createAddon(addon)); } @@ -675,7 +684,6 @@ export default class Component extends Element { return; } this._visible = value; - this.clearOnHide(); this.redraw(); } } @@ -698,6 +706,23 @@ export default class Component extends Element { return this._visible && this._parentVisible; } + get conditionallyHidden() { + return this._conditionallyHidden || this._parentConditionallyHidden; + } + + /** + * Evaluates whether the component is conditionally hidden (as opposed to intentionally hidden, e.g. via the `hidden` component schema property). + * @param {object} data - The data object to evaluate the condition against. + * @param {object} row - The row object to evaluate the condition against. + * @returns {boolean} - Whether the component is conditionally hidden. + */ + checkConditionallyHidden(data = null, row = null) { + if (!this.hasCondition()) { + return false; + } + return !this.conditionallyVisible(data, row); + } + get currentForm() { return this._currentForm; } @@ -2028,7 +2053,7 @@ export default class Component extends Element { rebuild() { this.destroy(); this.init(); - this.visible = this.conditionallyVisible(null, null); + this.visible = this.hasCondition() ? !this.conditionallyHidden : !this.component.hidden; return this.redraw(); } @@ -2105,8 +2130,8 @@ export default class Component extends Element { conditionallyVisible(data, row) { data = data || this.rootValue; row = row || this.data; - if (this.builderMode || this.previewMode || !this.hasCondition()) { - return !this.component.hidden; + if (this.builderMode || this.previewMode) { + return true; } data = data || (this.root ? this.root.data : {}); return this.checkCondition(row, data); @@ -2146,8 +2171,15 @@ export default class Component extends Element { this.redraw(); } - // Check advanced conditions - const visible = this.conditionallyVisible(data, row); + // Check advanced conditions (and cache the result) + const isConditionallyHidden = this.checkConditionallyHidden(data, row) || this._parentConditionallyHidden; + if (isConditionallyHidden !== this._conditionallyHidden) { + this._conditionallyHidden = isConditionallyHidden; + this.clearOnHide(); + } + + // Check visibility + const visible = (this.hasCondition() ? !this.conditionallyHidden : !this.component.hidden); if (this.visible !== visible) { this.visible = visible; @@ -2289,6 +2321,12 @@ export default class Component extends Element { const property = action.property.value; if (!_.isEqual(_.get(this.component, property), _.get(newComponent, property))) { + // Advanced Logic can modify the component's hidden property; because we track conditionally hidden state + // separately from the component's hidden property, and technically this Advanced Logic conditionally hides + // a component, we need to set _conditionallyHidden to the new value + if (property === 'hidden') { + this._conditionallyHidden = newComponent.hidden; + } changed = true; } @@ -2307,7 +2345,7 @@ export default class Component extends Element { } ); - if (!_.isEqual(oldValue, newValue) && !(this.component.clearOnHide && !this.visible)) { + if (!_.isEqual(oldValue, newValue) && !(this.component.clearOnHide && this.conditionallyHidden)) { this.setValue(newValue); if (this.viewOnly) { @@ -2352,7 +2390,7 @@ export default class Component extends Element { }, 'value'); - if (!_.isEqual(oldValue, newValue) && !(this.component.clearOnHide && !this.visible)) { + if (!_.isEqual(oldValue, newValue) && !(this.component.clearOnHide && this.conditionallyHidden)) { this.setValue(newValue); if (this.viewOnly) { @@ -2481,7 +2519,7 @@ export default class Component extends Element { !this.options.readOnly && !this.options.showHiddenFields ) { - if (!this.visible) { + if (this.conditionallyHidden) { this.deleteValue(); } else if (!this.hasValue() && this.shouldAddDefaultValue) { @@ -2776,7 +2814,7 @@ export default class Component extends Element { get dataValue() { if ( !this.key || - (!this.visible && this.component.clearOnHide && !this.rootPristine) + (this.conditionallyHidden && this.component.clearOnHide && !this.rootPristine) ) { return this.emptyValue; } @@ -2798,7 +2836,7 @@ export default class Component extends Element { if ( !this.allowData || !this.key || - (!this.visible && this.component.clearOnHide && !this.rootPristine) + (this.conditionallyHidden && this.component.clearOnHide && !this.rootPristine) ) { return; } @@ -3162,7 +3200,7 @@ export default class Component extends Element { // If no calculated value or // hidden and set to clearOnHide (Don't calculate a value for a hidden field set to clear when hidden) const { clearOnHide } = this.component; - const shouldBeCleared = !this.visible && clearOnHide; + const shouldBeCleared = this.conditionallyHidden && clearOnHide; const allowOverride = _.get(this.component, 'allowCalculateOverride', false); if (shouldBeCleared) { @@ -3706,12 +3744,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 +3754,13 @@ 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))) { + this._errors = []; + return true; + } + return false; + } ]; return rules.some(pred => pred()); @@ -3877,7 +3915,7 @@ export default class Component extends Element { // If component definition changed, replace it. if (!_.isEqual(this.component, newComponent)) { this.component = newComponent; - const visible = this.conditionallyVisible(null, null); + const visible = this.hasCondition() ? !this.conditionallyHidden : !this.component.hidden; const disabled = this.shouldDisabled; // Change states which won't be recalculated during redrawing diff --git a/src/components/_classes/component/editForm/Component.edit.data.js b/src/components/_classes/component/editForm/Component.edit.data.js index ed2790d01a..de4bf0c477 100644 --- a/src/components/_classes/component/editForm/Component.edit.data.js +++ b/src/components/_classes/component/editForm/Component.edit.data.js @@ -128,10 +128,10 @@ export default [ { weight: 700, type: 'checkbox', - label: 'Clear Value When Hidden', + label: 'Omit Value From Submission Data When Conditionally Hidden', key: 'clearOnHide', defaultValue: true, - tooltip: 'When a field is hidden, clear the value.', + tooltip: 'When a field is conditionally hidden, omit the value from the submission data.', input: true }, EditFormUtils.javaScriptValue('Custom Default Value', 'customDefaultValue', 'customDefaultValue', 1000, diff --git a/src/components/_classes/nested/NestedComponent.js b/src/components/_classes/nested/NestedComponent.js index e429f71ff8..27e09e7259 100644 --- a/src/components/_classes/nested/NestedComponent.js +++ b/src/components/_classes/nested/NestedComponent.js @@ -86,18 +86,27 @@ export default class NestedComponent extends Field { const visibilityChanged = this._visible !== value; this._visible = value; const isVisible = this.visible; + const isConditionallyHidden = this.checkConditionallyHidden(); const forceShow = this.shouldForceShow(); const forceHide = this.shouldForceHide(); - this.components.forEach(component => { + this.components.forEach((component) => { // Set the parent visibility first since we may have nested components within nested components // and they need to be able to determine their visibility based on the parent visibility. component.parentVisible = isVisible; + component._parentConditionallyHidden = isConditionallyHidden; + let visible; + if (component.hasCondition()) { + component._conditionallyHidden = component.checkConditionallyHidden() || component._parentConditionallyHidden; + visible = !component.conditionallyHidden; + } + else { + visible = !component.component.hidden; + } - const conditionallyVisible = component.conditionallyVisible(); - if (forceShow || conditionallyVisible) { + if (forceShow || visible) { component.visible = true; } - else if (forceHide || !isVisible || !conditionallyVisible) { + else if (forceHide || !isVisible || !visible ) { component.visible = false; } // If hiding a nested component, clear all errors below. @@ -105,8 +114,8 @@ export default class NestedComponent extends Field { component.error = ''; } }); + if (visibilityChanged) { - this.clearOnHide(); this.redraw(); } } @@ -421,6 +430,7 @@ export default class NestedComponent extends Field { data = data || this.data; options.parent = this; options.parentVisible = this.visible; + options.parentConditionallyHidden = this.conditionallyHidden; options.root = options?.root || this.root || this; options.localRoot = this.localRoot; options.skipInit = true; @@ -710,7 +720,7 @@ export default class NestedComponent extends Field { clearOnHide(show) { super.clearOnHide(show); if (this.component.clearOnHide) { - if (this.allowData && !this.hasValue() && !(this.options.server && !this.visible)) { + if (this.allowData && !this.hasValue() && !this.conditionallyHidden) { this.dataValue = this.defaultValue; } if (this.hasValue()) { @@ -743,7 +753,7 @@ export default class NestedComponent extends Field { calculateValue(data, flags, row) { // Do not iterate into children and calculateValues if this nested component is conditionally hidden. - if (!this.conditionallyVisible()) { + if (this.conditionallyHidden) { return false; } return this.getComponents().reduce( diff --git a/src/components/datamap/DataMap.js b/src/components/datamap/DataMap.js index 584710e097..758ec73f76 100644 --- a/src/components/datamap/DataMap.js +++ b/src/components/datamap/DataMap.js @@ -80,7 +80,7 @@ export default class DataMapComponent extends DataGridComponent { get dataValue() { if ( !this.key || - (!this.visible && this.component.clearOnHide) + (this.conditionallyHidden && this.component.clearOnHide) ) { return this.emptyValue; } diff --git a/src/components/editgrid/EditGrid.js b/src/components/editgrid/EditGrid.js index 819cf1872d..86f0a89039 100644 --- a/src/components/editgrid/EditGrid.js +++ b/src/components/editgrid/EditGrid.js @@ -1348,7 +1348,7 @@ export default class EditGridComponent extends NestedArrayComponent { } const changed = this.hasChanged(value, this.dataValue); - if (this.parent && !this.options.server) { + if (this.parent) { this.parent.checkComponentConditions(); } this.dataValue = value; @@ -1383,10 +1383,7 @@ export default class EditGridComponent extends NestedArrayComponent { this.openWhenEmpty(); this.updateOnChange(flags, changed); - // do not call checkData with server option, it is called when change is triggered in updateOnChange - if (!this.options.server) { - this.checkData(); - } + this.checkData(); this.changeState(changed, flags); diff --git a/src/components/form/Form.js b/src/components/form/Form.js index c9d36281cf..a735d8c059 100644 --- a/src/components/form/Form.js +++ b/src/components/form/Form.js @@ -475,11 +475,11 @@ export default class FormComponent extends Component { } hideSubmitButton(component) { - const isSubmitButton = (component.type === 'button') && - ((component.action === 'submit') || !component.action); + const isSubmitButton = component.type === 'button' && (component.action === 'submit' || !component.action); if (isSubmitButton) { component.hidden = true; + component.customConditional = 'show = false'; } } @@ -489,7 +489,7 @@ export default class FormComponent extends Component { * @returns {Promise} - The promise that resolves when the subform is loaded. */ loadSubForm(fromAttach) { - if (this.builderMode || this.isHidden() || (this.isSubFormLazyLoad() && !fromAttach)) { + if (this.builderMode || this.conditionallyHidden || (this.isSubFormLazyLoad() && !fromAttach)) { return Promise.resolve(); } @@ -586,7 +586,7 @@ export default class FormComponent extends Component { * @returns {*|boolean} - TRUE if the subform should be submitted, FALSE if it should not. */ get shouldSubmit() { - return this.subFormReady && (!this.component.hasOwnProperty('reference') || this.component.reference) && !this.isHidden(); + return this.subFormReady && (!this.component.hasOwnProperty('reference') || this.component.reference) && !this.conditionallyHidden; } /** diff --git a/src/components/html/HTML.js b/src/components/html/HTML.js index c2f7a60a51..06a6de76a8 100644 --- a/src/components/html/HTML.js +++ b/src/components/html/HTML.js @@ -62,9 +62,22 @@ export default class HTMLComponent extends Component { checkRefreshOn(changed) { super.checkRefreshOn(changed); - if (!this.builderMode && this.component.refreshOnChange && this.element && - !_.isUndefined(changed) && ((_.isBoolean(changed) && changed) || !_.isEmpty(changed)) && - this.conditionallyVisible(this.data, this.row)) { + let visible; + if (this.hasCondition()) { + this._conditionallyHidden = this.checkConditionallyHidden(); + visible = !this.conditionallyHidden; + } + else { + visible = !this.component.hidden; + } + const shouldSetContent = !this.builderMode + && this.component.refreshOnChange + && this.element + && !_.isUndefined(changed) + && ((_.isBoolean(changed) && changed) || !_.isEmpty(changed)) + && visible; + + if (shouldSetContent) { this.setContent(this.element, this.renderContent()); } } diff --git a/test/unit/EditGrid.unit.js b/test/unit/EditGrid.unit.js index dfd949a8b4..47068cff2f 100644 --- a/test/unit/EditGrid.unit.js +++ b/test/unit/EditGrid.unit.js @@ -1273,30 +1273,6 @@ describe('EditGrid Component', () => { }).catch(done); }); - it('Should keep value for conditional editGrid on setValue when server option is provided', (done) => { - const element = document.createElement('div'); - - Formio.createForm(element, formsWithEditGridAndConditions.form1, { server: true }).then(form => { - const formData = { - checkbox: true, - radio: 'yes', - editGrid: [ - { textField: 'test', number: 4 }, - { textField: 'test1', number: 5 }, - ], - }; - - form.setValue({ data: _.cloneDeep(formData) }); - - setTimeout(() => { - const editGrid = form.getComponent('editGrid'); - assert.deepEqual(editGrid.dataValue, formData.editGrid); - - done(); - }, 500); - }).catch(done); - }); - it('Should set value for conditional editGrid inside editGrid on event when form is not pristine ', (done) => { const element = document.createElement('div'); @@ -1319,85 +1295,6 @@ describe('EditGrid Component', () => { }).catch(done); }); - it('Should keep value for conditional editGrid in tabs on setValue when server option is provided', (done) => { - const element = document.createElement('div'); - - Formio.createForm(element, formsWithEditGridAndConditions.form3, { server: true }).then(form => { - const formData = { - affectedRiskTypes: { - creditRisk: false, - marketRisk: true, - operationalRisk: false, - counterpartyCreditRisk: false, - creditValuationRiskAdjustment: false, - }, - rwaImpact: 'yes', - submit: true, - mr: { - quantitativeInformation: { - cva: 'yes', - sameRiskCategories: false, - impactsPerEntity: [{ number: 123 }], - sameImpactAcrossEntities: false, - }, - }, - euParentInstitution: 'EUParent', - }; - - form.setValue({ data: _.cloneDeep(formData) }); - - setTimeout(() => { - const editGrid = form.getComponent('impactsPerEntity'); - assert.deepEqual(editGrid.dataValue, formData.mr.quantitativeInformation.impactsPerEntity); - assert.deepEqual(editGrid.editRows.length, 1); - - done(); - }, 500); - }).catch(done); - }); - - it('Should calculate editGrid value when calculateOnServer is enabled and server option is passed', (done) => { - const element = document.createElement('div'); - - Formio.createForm(element, formsWithEditGridAndConditions.form4, { server: true }).then(form => { - const editGrid = form.getComponent('editGrid'); - assert.deepEqual(editGrid.dataValue, [{ textArea: 'test' }]); - assert.deepEqual(editGrid.editRows.length, 1); - done(); - }).catch(done); - }); - - it('Should keep value for conditional editGrid deeply nested in panels and containers on setValue when server option is provided', (done) => { - const element = document.createElement('div'); - - Formio.createForm(element, formsWithEditGridAndConditions.form5, { server: true }).then(form => { - const formData = { - generalInformation: { - listSupervisedEntitiesCovered: [ - { id: 6256, longName: 'Bank_DE', leiCode: 'LEI6256', countryCode: 'DE' }, - ], - deSpecific: { - criticalPartsToBeOutsourcedSuboutsourcer: 'yes', - suboutsourcers: [ - { nameSuboutsourcer: 'test' }, - { nameSuboutsourcer: 'test 1' }, - ], - }, - }, - }; - - form.setValue({ data: _.cloneDeep(formData) }); - - setTimeout(() => { - const editGrid = form.getComponent('suboutsourcers'); - assert.deepEqual(editGrid.dataValue, formData.generalInformation.deSpecific.suboutsourcers); - assert.deepEqual(editGrid.editRows.length, 2); - - done(); - }, 500); - }).catch(done); - }); - it('Should calculate editGrid value when condition is met in advanced logic', (done) => { const element = document.createElement('div'); diff --git a/test/unit/NestedComponent.unit.js b/test/unit/NestedComponent.unit.js index 1fa954b1b7..0a81f7edd2 100644 --- a/test/unit/NestedComponent.unit.js +++ b/test/unit/NestedComponent.unit.js @@ -123,7 +123,7 @@ describe('NestedComponent class', () => { comp.setValue(data); comp.checkConditions(data); assert.equal(comp.components[1]._visible, false); - assert.equal(comp.components[1].components[0]._visible, true); + assert.equal(comp.components[1].components[0]._visible, false); assert.equal(comp.components[1].components[1]._visible, false); // overrideParent is depricated. @@ -131,8 +131,8 @@ describe('NestedComponent class', () => { comp.setValue(data); comp.checkConditions(data); assert.equal(comp.components[1]._visible, false); - assert.equal(comp.components[1].components[0]._visible, true); - assert.equal(comp.components[1].components[1]._visible, true); + assert.equal(comp.components[1].components[0]._visible, false); + assert.equal(comp.components[1].components[1]._visible, false); }); }); diff --git a/test/unit/clearOnHide.js b/test/unit/clearOnHide.js new file mode 100644 index 0000000000..dafeea1c6e --- /dev/null +++ b/test/unit/clearOnHide.js @@ -0,0 +1,1214 @@ +import Harness from '../harness'; +import assert from 'power-assert'; +import { Formio } from '../../src/Formio'; +import { wait } from '../util'; + +describe('Clear on Hide (Omit When Conditionally Hidden) Behavior', function () { + describe('Layout components', function () { + it('Should conditionally hide children of conditionally hidden layout parents', async function () { + const formWithConditionallyHiddenPanel = { + components: [ + { + type: 'checkbox', + key: 'checkbox', + label: 'Checkbox', + input: true, + }, + { + type: 'textfield', + key: 'textField', + label: 'Text Field', + input: true, + }, + { + type: 'panel', + key: 'panel', + components: [ + { + type: 'textfield', + key: 'childTextField', + label: 'Text Field', + input: true, + }, + ], + conditional: { + json: { '!': { var: 'data.checkbox' } }, + }, + }, + ], + }; + const element = document.createElement('div'); + const form = await Formio.createForm( + element, + formWithConditionallyHiddenPanel + ); + const checkbox = form.getComponent('checkbox'); + const textField = form.getComponent('textField'); + const panel = form.getComponent('panel'); + const childTextField = form.getComponent('childTextField'); + assert(checkbox, 'Checkbox component not found'); + assert(textField, 'Text Field component not found'); + assert(panel, 'Panel component not found'); + assert(childTextField, 'Child Text Field component not found'); + + // Initially, all components should be visible. + assert.equal(checkbox.visible, true); + assert.equal(textField.visible, true); + assert.equal(panel.visible, true); + assert.equal(childTextField.visible, true); + + // Initially, all components should not be conditionally hidden + assert.equal(checkbox.conditionallyHidden, false); + assert.equal(textField.conditionallyHidden, false); + assert.equal(panel.conditionallyHidden, false); + assert.equal(childTextField.conditionallyHidden, false); + + // Set the checkbox to true, which should hide the panel and its children + await form.setSubmission({ data: { checkbox: true } }); + await wait(250); + assert.equal(checkbox.visible, true); + assert.equal(textField.visible, true); + assert.equal(panel.visible, false); + assert.equal(childTextField.visible, false); + + // They should also be conditionally hidden + assert.equal(checkbox.conditionallyHidden, false); + assert.equal(textField.conditionallyHidden, false); + assert.equal(panel.conditionallyHidden, true); + assert.equal(childTextField.conditionallyHidden, true); + }); + + it('Should not conditionally hide children of layout components that are hidden using the "hidden" property', async function () { + const formWithHiddenPanel = { + components: [ + { + type: 'panel', + key: 'panel', + components: [ + { + type: 'textfield', + key: 'childTextField', + label: 'Text Field', + input: true, + }, + ], + hidden: true, + }, + ], + }; + const element = document.createElement('div'); + const form = await Formio.createForm(element, formWithHiddenPanel); + const panel = form.getComponent('panel'); + const childTextField = form.getComponent('childTextField'); + assert(panel, 'Panel component not found'); + assert(childTextField, 'Child Text Field component not found'); + + // All components should not be visible + assert.equal(panel.visible, false); + assert.equal(childTextField.visible, false); + + // All components should NOT be conditionally hidden + assert.equal( + panel.conditionallyHidden, + false, + 'Panel should not be conditionally hidden' + ); + assert.equal(childTextField.conditionallyHidden, false); + }); + + it('Should conditionally hide children of a manually hidden layout component if they have a conditional', async function () { + const formWithHiddenPanelAndConditionalChild = { + components: [ + { + type: 'checkbox', + input: true, + label: 'Checkbox', + key: 'checkbox', + }, + { + type: 'panel', + key: 'panel', + components: [ + { + type: 'textfield', + key: 'childTextField', + label: 'Text Field', + input: true, + conditional: { + json: { '!': { var: 'data.checkbox' } }, + }, + }, + ], + hidden: true, + }, + ], + }; + const element = document.createElement('div'); + const form = await Formio.createForm( + element, + formWithHiddenPanelAndConditionalChild + ); + const checkbox = form.getComponent('checkbox'); + const panel = form.getComponent('panel'); + const childTextField = form.getComponent('childTextField'); + assert(checkbox, 'Checkbox component not found'); + assert(panel, 'Panel component not found'); + assert(childTextField, 'Child Text Field component not found'); + + // The panel and its child should not be visible + assert.equal(checkbox.visible, true); + assert.equal(panel.visible, false); + assert.equal(childTextField.visible, false); + + // Initially, the panel and its child should NOT be conditionally hidden + assert.equal(checkbox.conditionallyHidden, false); + assert.equal(panel.conditionallyHidden, false); + assert.equal(childTextField.conditionallyHidden, false); + + // Set the checkbox to true, which should conditionally hide the child + await form.setSubmission({ data: { checkbox: true } }); + await wait(250); + assert.equal(checkbox.visible, true); + assert.equal(panel.visible, false); + assert.equal(childTextField.visible, false); + assert.equal(panel.conditionallyHidden, false); + assert.equal(childTextField.conditionallyHidden, true); + }); + + it('Should not clear the value of a conditionally hidden child component of a hidden layout component when hiding if the form is pristine', async function () { + const formWithHiddenPanelAndConditionalChild = { + components: [ + { + type: 'checkbox', + input: true, + label: 'Checkbox', + key: 'checkbox', + }, + { + type: 'panel', + key: 'panel', + components: [ + { + type: 'textfield', + key: 'childTextField', + label: 'Text Field', + input: true, + conditional: { + json: { '!': { var: 'data.checkbox' } }, + }, + }, + ], + hidden: true, + }, + ], + }; + const element = document.createElement('div'); + const form = await Formio.createForm( + element, + formWithHiddenPanelAndConditionalChild + ); + const checkbox = form.getComponent('checkbox'); + const panel = form.getComponent('panel'); + const childTextField = form.getComponent('childTextField'); + assert(checkbox, 'Checkbox component not found'); + assert(panel, 'Panel component not found'); + assert(childTextField, 'Child Text Field component not found'); + + assert.equal(form.pristine, true, 'Form should be pristine'); + + // Initially, all components should not be conditionally hidden + assert.equal( + checkbox.conditionallyHidden, + false, + 'Checkbox should not be conditionally hidden' + ); + assert.equal( + panel.conditionallyHidden, + false, + 'Panel should not be conditionally hidden' + ); + assert.equal( + childTextField.conditionallyHidden, + false, + 'Child Text Field should not be conditionally hidden' + ); + + assert.deepEqual( + form.data, + { checkbox: false, childTextField: '' }, + 'Initial form data is incorrect' + ); + + // Hide the panel, which should clear the value of the child text field + await form.setSubmission({ + data: { checkbox: true, childTextField: 'test' }, + }); + await wait(250); + assert.equal( + childTextField.conditionallyHidden, + true, + 'Child Text Field should be conditionally hidden' + ); + assert.deepEqual( + form.data, + { checkbox: true, childTextField: 'test' }, + 'Form data is incorrect' + ); + }); + + it('Should clear the value of a conditionally hidden child component of a hidden layout component when hiding if the form is not pristine', async function () { + const formWithHiddenPanelAndConditionalChild = { + components: [ + { + type: 'checkbox', + input: true, + label: 'Checkbox', + key: 'checkbox', + }, + { + type: 'panel', + key: 'panel', + components: [ + { + type: 'textfield', + key: 'childTextField', + label: 'Text Field', + input: true, + conditional: { + json: { '!': { var: 'data.checkbox' } }, + }, + }, + ], + hidden: true, + }, + ], + }; + const element = document.createElement('div'); + const form = await Formio.createForm( + element, + formWithHiddenPanelAndConditionalChild + ); + const checkbox = form.getComponent('checkbox'); + const panel = form.getComponent('panel'); + const childTextField = form.getComponent('childTextField'); + assert(checkbox, 'Checkbox component not found'); + assert(panel, 'Panel component not found'); + assert(childTextField, 'Child Text Field component not found'); + + assert.equal(form.pristine, true, 'Form should be pristine'); + + // Initially, all components should not be conditionally hidden + assert.equal( + checkbox.conditionallyHidden, + false, + 'Checkbox should not be conditionally hidden' + ); + assert.equal( + panel.conditionallyHidden, + false, + 'Panel should not be conditionally hidden' + ); + assert.equal( + childTextField.conditionallyHidden, + false, + 'Child Text Field should not be conditionally hidden' + ); + + assert.deepEqual( + form.data, + { checkbox: false, childTextField: '' }, + 'Initial form data is incorrect' + ); + + // Hide the panel, which should clear the value of the child text field + form.pristine = false; + await form.setSubmission({ data: { checkbox: true } }); + await wait(250); + assert.equal( + childTextField.conditionallyHidden, + true, + 'Child Text Field should be conditionally hidden' + ); + assert.deepEqual(form.data, { checkbox: true }, 'Form data is incorrect'); + }); + }); + + describe('Container components', function () { + it('Should conditionally hide children of conditionally hidden container parents', async function () { + const formWithConditionallyHiddenContainer = { + components: [ + { + type: 'checkbox', + key: 'checkbox', + label: 'Checkbox', + input: true, + }, + { + type: 'textfield', + key: 'textField', + label: 'Text Field', + input: true, + }, + { + type: 'container', + key: 'container', + components: [ + { + type: 'textfield', + key: 'childTextField', + label: 'Text Field', + input: true, + }, + ], + conditional: { + json: { '!': { var: 'data.checkbox' } }, + }, + }, + ], + }; + + const element = document.createElement('div'); + const form = await Formio.createForm( + element, + formWithConditionallyHiddenContainer + ); + const checkbox = form.getComponent('checkbox'); + const textField = form.getComponent('textField'); + const container = form.getComponent('container'); + const childTextField = form.getComponent('childTextField'); + assert(checkbox, 'Checkbox component not found'); + assert(textField, 'Text Field component not found'); + assert(container, 'Container component not found'); + assert(childTextField, 'Child Text Field component not found'); + + // Initially, all components should be visible. + assert.equal(checkbox.visible, true); + assert.equal(textField.visible, true); + assert.equal(container.visible, true); + assert.equal(childTextField.visible, true); + + // Initially, all components should not be conditionally hidden + assert.equal(checkbox.conditionallyHidden, false); + assert.equal(textField.conditionallyHidden, false); + assert.equal(container.conditionallyHidden, false); + assert.equal(childTextField.conditionallyHidden, false); + + // Set the checkbox to true, which should hide the container and its children + await form.setSubmission({ data: { checkbox: true } }); + await wait(250); + assert.equal(checkbox.visible, true, 'Checkbox should be visible'); + assert.equal(textField.visible, true, 'Text Field should be visible'); + assert.equal(container.visible, false, 'Container should be hidden'); + assert.equal( + childTextField.visible, + false, + 'Child Text Field should be hidden' + ); + + // They should also be conditionally hidden + assert.equal( + checkbox.conditionallyHidden, + false, + 'Checkbox should not be conditionally hidden' + ); + assert.equal( + textField.conditionallyHidden, + false, + 'Text Field should not be conditionally hidden' + ); + assert.equal( + container.conditionallyHidden, + true, + 'Container should be conditionally hidden' + ); + assert.equal( + childTextField.conditionallyHidden, + true, + 'Child Text Field should be conditionally hidden' + ); + }); + + it('Should not conditionally hide children of container components that are hidden using the "hidden" property', async function () { + const formWithHiddenContainer = { + components: [ + { + type: 'container', + key: 'container', + components: [ + { + type: 'textfield', + key: 'childTextField', + label: 'Text Field', + input: true, + }, + ], + hidden: true, + }, + ], + }; + const element = document.createElement('div'); + const form = await Formio.createForm(element, formWithHiddenContainer); + const container = form.getComponent('container'); + const childTextField = form.getComponent('childTextField'); + assert(container, 'Container component not found'); + assert(childTextField, 'Child Text Field component not found'); + + // All components should not be visible + assert.equal(container.visible, false, 'Container should not be visible'); + assert.equal(childTextField.visible, false), + 'Child Text Field should not be visible'; + + // All components should NOT be conditionally hidden + assert.equal( + container.conditionallyHidden, + false, + 'Container should not be conditionally hidden' + ); + assert.equal(childTextField.conditionallyHidden, false); + }); + + it('Should conditionally hide children of a manually hidden container component if they have a conditional', async function () { + const formWithHiddenContainerAndConditionalChild = { + components: [ + { + type: 'checkbox', + input: true, + label: 'Checkbox', + key: 'checkbox', + }, + { + type: 'container', + key: 'container', + components: [ + { + type: 'textfield', + key: 'childTextField', + label: 'Text Field', + input: true, + conditional: { + json: { '!': { var: 'data.checkbox' } }, + }, + }, + ], + hidden: true, + }, + ], + }; + const element = document.createElement('div'); + const form = await Formio.createForm( + element, + formWithHiddenContainerAndConditionalChild + ); + const checkbox = form.getComponent('checkbox'); + const container = form.getComponent('container'); + const childTextField = form.getComponent('childTextField'); + assert(checkbox, 'Checkbox component not found'); + assert(container, 'Container component not found'); + assert(childTextField, 'Child Text Field component not found'); + + // The panel and its child should not be visible + assert.equal(checkbox.visible, true); + assert.equal(container.visible, false); + assert.equal(childTextField.visible, false); + + // Initially, the panel and its child should NOT be conditionally hidden + assert.equal(checkbox.conditionallyHidden, false); + assert.equal(container.conditionallyHidden, false); + assert.equal(childTextField.conditionallyHidden, false); + + // Set the checkbox to true, which should conditionally hide the child + await form.setSubmission({ data: { checkbox: true } }); + await wait(250); + assert.equal(checkbox.visible, true); + assert.equal(container.visible, false); + assert.equal(childTextField.visible, false); + assert.equal(container.conditionallyHidden, false); + assert.equal(childTextField.conditionallyHidden, true); + }); + + it('Should not clear the value of a conditionally hidden child component of a hidden container component when hiding if the form is pristine', async function () { + const formWithHiddenContainerAndConditionalChild = { + components: [ + { + type: 'checkbox', + input: true, + label: 'Checkbox', + key: 'checkbox', + }, + { + type: 'container', + key: 'container', + components: [ + { + type: 'textfield', + key: 'childTextField', + label: 'Text Field', + input: true, + conditional: { + json: { '!': { var: 'data.checkbox' } }, + }, + }, + ], + hidden: true, + }, + ], + }; + const element = document.createElement('div'); + const form = await Formio.createForm( + element, + formWithHiddenContainerAndConditionalChild + ); + const checkbox = form.getComponent('checkbox'); + const container = form.getComponent('container'); + const childTextField = form.getComponent('childTextField'); + assert(checkbox, 'Checkbox component not found'); + assert(container, 'Container component not found'); + assert(childTextField, 'Child Text Field component not found'); + + assert.equal(form.pristine, true, 'Form should be pristine'); + + // Initially, all components should not be conditionally hidden + assert.equal( + checkbox.conditionallyHidden, + false, + 'Checkbox should not be conditionally hidden' + ); + assert.equal( + container.conditionallyHidden, + false, + 'Container should not be conditionally hidden' + ); + assert.equal( + childTextField.conditionallyHidden, + false, + 'Child Text Field should not be conditionally hidden' + ); + + assert.deepEqual( + form.data, + { checkbox: false, container: { childTextField: '' } }, + 'Initial form data is incorrect' + ); + + // Hide the panel, which should NOT clear the value of the child text field because the form is pristine + await form.setSubmission({ + data: { checkbox: true, container: { childTextField: 'test' } }, + }); + await wait(250); + assert.equal( + childTextField.conditionallyHidden, + true, + 'Child Text Field should be conditionally hidden' + ); + assert.deepEqual( + form.data, + { checkbox: true, container: { childTextField: 'test' } }, + 'Form data is incorrect' + ); + }); + + it('Should clear the value of a conditionally hidden child component of a hidden container component when hiding if the form is not pristine', async function () { + const formWithHiddenContainerAndConditionalChild = { + components: [ + { + type: 'checkbox', + input: true, + label: 'Checkbox', + key: 'checkbox', + }, + { + type: 'container', + key: 'container', + components: [ + { + type: 'textfield', + key: 'childTextField', + label: 'Text Field', + input: true, + conditional: { + json: { '!': { var: 'data.checkbox' } }, + }, + }, + ], + hidden: true, + }, + ], + }; + const element = document.createElement('div'); + const form = await Formio.createForm( + element, + formWithHiddenContainerAndConditionalChild + ); + const checkbox = form.getComponent('checkbox'); + const container = form.getComponent('container'); + const childTextField = form.getComponent('childTextField'); + assert(checkbox, 'Checkbox component not found'); + assert(container, 'Container component not found'); + assert(childTextField, 'Child Text Field component not found'); + + assert.equal(form.pristine, true, 'Form should be pristine'); + + // Initially, all components should not be conditionally hidden + assert.equal( + checkbox.conditionallyHidden, + false, + 'Checkbox should not be conditionally hidden' + ); + assert.equal( + container.conditionallyHidden, + false, + 'Container should not be conditionally hidden' + ); + assert.equal( + childTextField.conditionallyHidden, + false, + 'Child Text Field should not be conditionally hidden' + ); + + assert.deepEqual( + form.data, + { checkbox: false, container: { childTextField: '' } }, + 'Initial form data is incorrect' + ); + + // Hide the panel, which should clear the value of the child container + form.pristine = false; + await form.setSubmission({ data: { checkbox: true } }); + await wait(250); + assert.equal( + childTextField.conditionallyHidden, + true, + 'Child Text Field should be conditionally hidden' + ); + assert.deepEqual( + form.data, + { checkbox: true, container: {} }, + 'Form data is incorrect' + ); + }); + }); + + describe('Nested form components', function () { + let oldMakeRequest; + before(function () { + oldMakeRequest = Formio.makeRequest; + Formio.makeRequest = (formio, type, url, method, data) => { + if (type === 'form' && method === 'get') { + return Promise.resolve({ + type: 'form', + components: [ + { + label: 'Nested First Name', + tableView: true, + key: 'nestedFirstName', + type: 'textfield', + input: true, + }, + { + label: 'Nested Last Name', + tableView: true, + key: 'nestedLastName', + type: 'textfield', + input: true, + }, + { + type: 'container', + key: 'nestedContainer', + components: [ + { + label: 'Nested Container Field', + tableView: true, + key: 'nestedContainerField', + type: 'textfield', + input: true, + }, + ], + }, + ], + }); + } + if ( + type === 'submission' && + method === 'get' && + url.includes('nestedFormSubmissionId') + ) { + return Promise.resolve({ + _id: 'nestedFormSubmissionId', + form: 'nestedFormId', + owner: 'nestedFormOwnerId', + data: { + nestedFirstName: 'Nested First Name', + nestedLastName: 'Nested Last Name', + nestedContainer: { + nestedContainerField: 'Nested Container Field', + }, + }, + project: 'nestedFormProjectId', + }); + } + throw new Error('Invalid request'); + }; + }); + + it('Should not conditionally hide intentionally hidden Nested Form components', async function () { + const parentFormWithIntentionallyHiddenChild = { + components: [ + { + label: 'Parent Form Checkbox', + tableView: true, + key: 'checkbox', + type: 'checkbox', + input: true, + }, + { + label: 'Form', + tableView: true, + // Should resolve to the nested form in the before block + src: 'http://localhost:3000/myproject/child', + key: 'form', + type: 'form', + input: true, + hidden: true, + }, + ], + }; + const form = await Formio.createForm( + document.createElement('div'), + parentFormWithIntentionallyHiddenChild + ); + const checkbox = form.getComponent('checkbox'); + const nestedForm = form.getComponent('form'); + assert(checkbox, 'Parent component not found'); + assert(nestedForm, 'Nested Form component not found'); + + assert.equal(checkbox.visible, true, 'Checkbox should be visible'); + assert.equal(nestedForm.visible, false, 'Nested Form should be hidden'); + assert.equal( + checkbox.conditionallyHidden, + false, + 'Checkbox should not be conditionally hidden' + ); + assert.equal( + nestedForm.conditionallyHidden, + false, + 'Nested Form should not be conditionally hidden' + ); + }); + + it('Should conditionally hide conditionally hidden Nested Form components', async function () { + const parentFormWithConditionallyHiddenChild = { + components: [ + { + label: 'Parent Form Checkbox', + tableView: true, + key: 'checkbox', + type: 'checkbox', + input: true, + }, + { + label: 'Form', + tableView: true, + // Should resolve to the nested form in the before block + src: 'http://localhost:3000/myproject/child', + key: 'form', + type: 'form', + input: true, + conditional: { + json: { var: 'data.checkbox' }, + }, + }, + ], + }; + const form = await Formio.createForm( + document.createElement('div'), + parentFormWithConditionallyHiddenChild + ); + const checkbox = form.getComponent('checkbox'); + const nestedForm = form.getComponent('form'); + assert(checkbox, 'Parent component not found'); + assert(nestedForm, 'Nested Form component not found'); + + assert.equal(checkbox.visible, true, 'Checkbox should be visible'); + assert.equal(nestedForm.visible, false, 'Nested Form should be hidden'); + assert.equal( + checkbox.conditionallyHidden, + false, + 'Checkbox should not be conditionally hidden' + ); + assert.equal( + nestedForm.conditionallyHidden, + true, + 'Nested Form should be conditionally hidden' + ); + }); + + it('Should not clear the data of an intentionally hidden Nested Form component', async function () { + const parentFormWithIntentionallyHiddenChild = { + components: [ + { + label: 'Parent Form Checkbox', + tableView: true, + key: 'checkbox', + type: 'checkbox', + input: true, + }, + { + label: 'Form', + tableView: true, + // Should resolve to the nested form in the before block + src: 'http://localhost:3000/myproject/child', + key: 'form', + type: 'form', + input: true, + hidden: true, + }, + ], + }; + const form = await Formio.createForm( + document.createElement('div'), + parentFormWithIntentionallyHiddenChild + ); + const checkbox = form.getComponent('checkbox'); + const nestedForm = form.getComponent('form'); + assert(checkbox, 'Parent component not found'); + assert(nestedForm, 'Nested Form component not found'); + + assert.deepEqual( + form.data, + { + checkbox: false, + form: { + data: { + nestedFirstName: '', + nestedLastName: '', + nestedContainer: { nestedContainerField: '' }, + }, + metadata: {} + }, + }, + 'Initial form data is incorrect' + ); + + await form.setSubmission({ + data: { + checkbox: true, + form: { _id: 'nestedFormSubmissionId' }, + }, + }); + await wait(250); + assert.deepEqual( + form.data, + { + checkbox: true, + form: { + _id: 'nestedFormSubmissionId', + data: { + nestedFirstName: 'Nested First Name', + nestedLastName: 'Nested Last Name', + nestedContainer: { nestedContainerField: 'Nested Container Field' }, + }, + form: 'nestedFormId', + owner: 'nestedFormOwnerId', + project: 'nestedFormProjectId', + }, + }, + 'Form data is incorrect' + ); + }); + + it('Should populate the data of a conditionally shown Nested Form component', async function () { + const parentFormWithConditionallyHiddenChild = { + components: [ + { + label: 'Parent Form Checkbox', + tableView: true, + key: 'checkbox', + type: 'checkbox', + input: true, + }, + { + label: 'Form', + tableView: true, + // Should resolve to the nested form in the before block + src: 'http://localhost:3000/myproject/child', + key: 'form', + type: 'form', + input: true, + conditional: { + json: { var: 'data.checkbox' }, + } + }, + ], + }; + const form = await Formio.createForm( + document.createElement('div'), + parentFormWithConditionallyHiddenChild + ); + const checkbox = form.getComponent('checkbox'); + const nestedForm = form.getComponent('form'); + assert(checkbox, 'Parent component not found'); + assert(nestedForm, 'Nested Form component not found'); + + assert.deepEqual( + form.data, + { + checkbox: false, + }, + 'Initial form data is incorrect' + ); + + await form.setSubmission({ + data: { + checkbox: true, + form: { _id: 'nestedFormSubmissionId' }, + }, + }); + await wait(400); + assert.deepEqual( + form.data, + { + checkbox: true, + form: { + _id: 'nestedFormSubmissionId', + data: { + nestedFirstName: 'Nested First Name', + nestedLastName: 'Nested Last Name', + nestedContainer: { nestedContainerField: 'Nested Container Field' }, + }, + form: 'nestedFormId', + owner: 'nestedFormOwnerId', + project: 'nestedFormProjectId', + metadata: {} + }, + }, + 'Form data is incorrect' + ); + }); + + it('Should not clear the data of a conditionally hidden Nested Form component if the form is pristine', async function () { + const parentFormWithConditionallyHiddenChild = { + components: [ + { + label: 'Parent Form Checkbox', + tableView: true, + key: 'checkbox', + type: 'checkbox', + input: true, + }, + { + label: 'Form', + tableView: true, + // Should resolve to the nested form in the before block + src: 'http://localhost:3000/myproject/child', + key: 'form', + type: 'form', + input: true, + conditional: { + json: { '!': { var: 'data.checkbox' } }, + } + }, + ], + }; + const form = await Formio.createForm( + document.createElement('div'), + parentFormWithConditionallyHiddenChild + ); + await wait(200); + const checkbox = form.getComponent('checkbox'); + const nestedForm = form.getComponent('form'); + assert(checkbox, 'Parent component not found'); + assert(nestedForm, 'Nested Form component not found'); + + assert.equal(checkbox.visible, true, 'Checkbox should be visible'); + assert.equal(nestedForm.visible, true, 'Nested Form should be visible'); + assert.equal(checkbox.conditionallyHidden, false, 'Checkbox should not be conditionally hidden'); + assert.equal(nestedForm.conditionallyHidden, false, 'Nested Form should not be conditionally hidden'); + + assert.deepEqual( + form.data, + { + checkbox: false, + form: { + data: { + nestedFirstName: '', + nestedLastName: '', + nestedContainer: { nestedContainerField: '' }, + }, + metadata: {} + }, + }, + 'Initial form data is incorrect' + ); + + await form.setSubmission({ + data: { + checkbox: true, + }, + }); + await wait(400); + assert.deepEqual( + form.data, + { + checkbox: true, + form: { + data: { + nestedFirstName: '', + nestedLastName: '', + nestedContainer: { nestedContainerField: '' }, + }, + metadata: {} + }, + }, + 'Form data is incorrect' + ); + }); + + it('Should clear the data of a conditionally hidden Nested Form component if the form is not pristine', async function () { + const parentFormWithConditionallyHiddenChild = { + components: [ + { + label: 'Parent Form Checkbox', + tableView: true, + key: 'checkbox', + type: 'checkbox', + input: true, + }, + { + label: 'Form', + tableView: true, + // Should resolve to the nested form in the before block + src: 'http://localhost:3000/myproject/child', + key: 'form', + type: 'form', + input: true, + conditional: { + json: { '!': { var: 'data.checkbox' } }, + } + }, + ], + }; + const form = await Formio.createForm( + document.createElement('div'), + parentFormWithConditionallyHiddenChild + ); + await wait(200); + const checkbox = form.getComponent('checkbox'); + const nestedForm = form.getComponent('form'); + assert(checkbox, 'Parent component not found'); + assert(nestedForm, 'Nested Form component not found'); + + assert.equal(checkbox.visible, true, 'Checkbox should be visible'); + assert.equal(nestedForm.visible, true, 'Nested Form should be visible'); + assert.equal(checkbox.conditionallyHidden, false, 'Checkbox should not be conditionally hidden'); + assert.equal(nestedForm.conditionallyHidden, false, 'Nested Form should not be conditionally hidden'); + + assert.deepEqual( + form.data, + { + checkbox: false, + form: { + data: { + nestedFirstName: '', + nestedLastName: '', + nestedContainer: { nestedContainerField: '' }, + }, + metadata: {} + }, + }, + 'Initial form data is incorrect' + ); + + form.pristine = false; + await form.setSubmission({ + data: { + checkbox: true, + }, + }); + await wait(400); + assert.deepEqual( + form.data, + { + checkbox: true, + }, + 'Form data is incorrect' + ); + }); + + it('Should clear the submission data of a conditionally hidden Nested Form component when hiding and the form is not pristine', async function () { + const parentFormWithConditionallyHiddenChild = { + components: [ + { + label: 'Parent Form Checkbox', + tableView: true, + key: 'checkbox', + type: 'checkbox', + input: true, + }, + { + label: 'Form', + tableView: true, + // Should resolve to the nested form in the before block + src: 'http://localhost:3000/myproject/child', + key: 'form', + type: 'form', + input: true, + conditional: { + json: { '!': { var: 'data.checkbox' } }, + } + }, + ], + }; + const form = await Formio.createForm( + document.createElement('div'), + parentFormWithConditionallyHiddenChild + ); + await wait(200); + const checkbox = form.getComponent('checkbox'); + const nestedForm = form.getComponent('form'); + assert(checkbox, 'Parent component not found'); + assert(nestedForm, 'Nested Form component not found'); + + assert.equal(checkbox.visible, true, 'Checkbox should be visible'); + assert.equal(nestedForm.visible, true, 'Nested Form should be visible'); + assert.equal(checkbox.conditionallyHidden, false, 'Checkbox should not be conditionally hidden'); + assert.equal(nestedForm.conditionallyHidden, false, 'Nested Form should not be conditionally hidden'); + + assert.deepEqual( + form.data, + { + checkbox: false, + form: { + data: { + nestedFirstName: '', + nestedLastName: '', + nestedContainer: { nestedContainerField: '' }, + }, + metadata: {} + } + }, + 'Initial form data is incorrect' + ); + + form.pristine = false; + // Hide the nested form AND set its submission id + await form.setSubmission({ + data: { + checkbox: true, + form: { _id: 'nestedFormSubmissionId' }, + }, + }); + await wait(300); + assert.deepEqual( + form.data, + { + checkbox: true, + }, + 'Form data is incorrect' + ); + }); + + after(function () { + Formio.makeRequest = oldMakeRequest; + }); + }); +}); diff --git a/test/unit/validateWhenHidden.unit.js b/test/unit/validateWhenHidden.unit.js new file mode 100644 index 0000000000..c63ae6eaa0 --- /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); + }); + }) +}); diff --git a/test/util.js b/test/util.js new file mode 100644 index 0000000000..53fd595f93 --- /dev/null +++ b/test/util.js @@ -0,0 +1,3 @@ +export function wait(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} 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"