diff --git a/src/components/_classes/component/Component.js b/src/components/_classes/component/Component.js index 69cbeef94c..e7f36bd4df 100644 --- a/src/components/_classes/component/Component.js +++ b/src/components/_classes/component/Component.js @@ -374,11 +374,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; /** @@ -454,7 +462,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; @@ -537,7 +545,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)); } @@ -706,7 +715,6 @@ export default class Component extends Element { return; } this._visible = value; - this.clearOnHide(); this.redraw(); } } @@ -729,6 +737,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; } @@ -2059,7 +2084,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(); } @@ -2136,8 +2161,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); @@ -2177,8 +2202,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; @@ -2320,6 +2352,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; } @@ -2338,7 +2376,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) { @@ -2383,7 +2421,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) { @@ -2512,7 +2550,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) { @@ -2807,7 +2845,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; } @@ -2829,7 +2867,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; } @@ -3193,7 +3231,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) { @@ -3917,7 +3955,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 b05c4f680f..6c2bab0442 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(); } } @@ -399,6 +408,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; @@ -688,7 +698,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()) { @@ -721,7 +731,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 68526eab38..85eb47913d 100644 --- a/src/components/datamap/DataMap.js +++ b/src/components/datamap/DataMap.js @@ -79,7 +79,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 8c3e55294c..2f119946a0 100644 --- a/src/components/editgrid/EditGrid.js +++ b/src/components/editgrid/EditGrid.js @@ -1354,7 +1354,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; @@ -1389,10 +1389,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 31490bcf9d..bc607d0e17 100644 --- a/src/components/form/Form.js +++ b/src/components/form/Form.js @@ -473,11 +473,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'; } } @@ -487,7 +487,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(); } @@ -569,7 +569,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 47773d913c..1e8e574bbf 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 6b994d093c..15f54dd3a4 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/util.js b/test/util.js index e10b7c5305..8aeba7ae38 100644 --- a/test/util.js +++ b/test/util.js @@ -1,3 +1,3 @@ export function wait(ms) { - return new Promise(resolve => setTimeout(resolve, ms)) -} \ No newline at end of file + return new Promise((resolve) => setTimeout(resolve, ms)); +}