From 3c03a216d05ee7fbf164ec334df707474887188b Mon Sep 17 00:00:00 2001 From: Valentin Serra Date: Tue, 12 Dec 2023 05:10:31 +0100 Subject: [PATCH] feat: implement proper variable hiding within repeated field Related to #808 --- packages/form-js-viewer/src/Form.js | 2 +- .../form-js-viewer/src/core/PathRegistry.js | 4 +- .../expressionLanguage/ConditionChecker.js | 95 +++++++++++++++---- 3 files changed, 82 insertions(+), 19 deletions(-) diff --git a/packages/form-js-viewer/src/Form.js b/packages/form-js-viewer/src/Form.js index 816867bd3..d21ea38cc 100644 --- a/packages/form-js-viewer/src/Form.js +++ b/packages/form-js-viewer/src/Form.js @@ -256,7 +256,7 @@ export default class Form { const workingErrors = {}; validateFieldRecursively(workingErrors, formFieldRegistry.getForm()); - const filteredErrors = this._applyConditions(workingErrors, data, { getFilterPath: getErrorPath }); + const filteredErrors = this._applyConditions(workingErrors, data, { getValuePath: getErrorPath }); this._setState({ errors: filteredErrors }); return filteredErrors; diff --git a/packages/form-js-viewer/src/core/PathRegistry.js b/packages/form-js-viewer/src/core/PathRegistry.js index 3bf506720..90e2d416a 100644 --- a/packages/form-js-viewer/src/core/PathRegistry.js +++ b/packages/form-js-viewer/src/core/PathRegistry.js @@ -183,8 +183,8 @@ export default class PathRegistry { result = result && callResult; } - // stop executing if false is specifically returned - if (result === false) { + // stop executing if false is specifically returned or if preventing recursion + if (result === false || context.preventRecursion) { return result; } diff --git a/packages/form-js-viewer/src/features/expressionLanguage/ConditionChecker.js b/packages/form-js-viewer/src/features/expressionLanguage/ConditionChecker.js index 1a0f9386d..cd6e615ad 100644 --- a/packages/form-js-viewer/src/features/expressionLanguage/ConditionChecker.js +++ b/packages/form-js-viewer/src/features/expressionLanguage/ConditionChecker.js @@ -1,6 +1,6 @@ import { unaryTest } from 'feelin'; import { get, isString, set, values, isObject } from 'min-dash'; -import { clone } from '../../util'; +import { buildExpressionContext, clone } from '../../util'; /** * @typedef {object} Condition @@ -17,37 +17,100 @@ export default class ConditionChecker { /** * For given data, remove properties based on condition. * - * @param {Object} properties * @param {Object} data + * @param {Object} conditionData * @param {Object} [options] - * @param {Function} [options.getFilterPath] + * @param {Function} [options.getValuePath] */ - applyConditions(properties, data = {}, options = {}) { + applyConditions(data, conditionData = {}, options = {}) { - const newProperties = clone(properties); + const workingData = clone(data); const { - getFilterPath = (field) => this._pathRegistry.getValuePath(field) + getValuePath = (field, indexes) => this._pathRegistry.getValuePath(field, { indexes }) } = options; + const _applyConditionsWithinScope = (rootField, scopeContext) => { + + const { + indexes = {}, + expressionIndexes = [], + scopeData = conditionData, + parentScopeData = null + } = scopeContext; + + this._pathRegistry.executeRecursivelyOnFields(rootField, ({ field, isClosed, isRepeatable, context }) => { + + const { + conditional, + components, + id + } = field; + + // build the expression context in the right format + const localExpressionContext = buildExpressionContext({ + data, + i: expressionIndexes, + this: scopeData, + parent: parentScopeData + }); + + context.isHidden = context.isHidden || (conditional && this._checkHideCondition(conditional, localExpressionContext)); + + // if a field is repeatable and visible, we need to implement custom recursion on its children + if (isRepeatable && !context.isHidden) { + + // prevent the regular recursion behavior of executeRecursivelyOnFields + context.preventRecursion = true; + + const repeaterValuePath = getValuePath(field, indexes); + const repeaterValue = get(workingData, repeaterValuePath); + + // quit early if there are no children or data associated with the repeater + if (!Array.isArray(repeaterValue) || !repeaterValue.length || !Array.isArray(components) || !components.length) { + return; + } + + for (let i = 0; i < repeaterValue.length; i++) { + + // create a new scope context for each index + const newScopeContext = { + indexes: { ...indexes, [id]: i }, + expressionIndexes: [ ...expressionIndexes, i + 1 ], + scopeData: repeaterValue[i], + parentScopeData: scopeData + }; + + // for each child component, apply conditions within the new repetition scope + components.forEach(component => { + _applyConditionsWithinScope(component, newScopeContext); + }); + + } + + } + + // if a field is a leaf node (or repeatable, as they behave similarly), and hidden, we need to clear the value from the data from each index + if (context.isHidden && (isClosed || isRepeatable)) { + this._clearObjectValueRecursively(getValuePath(field, indexes), workingData); + } + + }); + + }; + + // apply conditions starting with the root of the form const form = this._formFieldRegistry.getForm(); if (!form) { throw new Error('form field registry has no form'); } - this._pathRegistry.executeRecursivelyOnFields(form, ({ field, isClosed, isRepeatable, context }) => { - const { conditional: condition } = field; - - context.isHidden = context.isHidden || (condition && this._checkHideCondition(condition, data)); - - // if a field is a leaf node (or repeatable, as they behave similarly), and hidden, we need to clear the value from the data from each index - if (context.isHidden && (isClosed || isRepeatable)) { - this._clearObjectValueRecursively(getFilterPath(field), newProperties); - } + _applyConditionsWithinScope(form, { + scopeData: conditionData }); - return newProperties; + return workingData; } /**