From 492ccdfff1ffbab60fb9b10f1edc5cecd1574301 Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Mon, 22 Jul 2024 15:23:36 -0500 Subject: [PATCH 01/10] Additional fixes for the 5x renderer. --- src/Webform.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Webform.js b/src/Webform.js index e6b5b412f6..a9916ace87 100644 --- a/src/Webform.js +++ b/src/Webform.js @@ -958,6 +958,9 @@ export default class Webform extends NestedDataComponent { } getValue() { + if (!this._submission) { + this._submission = {}; + } if (!this._submission.data) { this._submission.data = {}; } From 8f49547fcbb0e70b346b5381f74ecb60175f3130 Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Wed, 14 Aug 2024 14:38:52 -0500 Subject: [PATCH 02/10] Fixing issues where the wrong urls are used for CDN on remote environments. --- src/CDN.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/CDN.js b/src/CDN.js index aab34bc431..65b842b744 100644 --- a/src/CDN.js +++ b/src/CDN.js @@ -70,10 +70,7 @@ class CDN { url += `/${lib}`; } // Only attach the version if this is the hosted cdn. - if ( - cdnUrl.includes('form.io') && - version && version !== 'latest' - ) { + if (cdnUrl.match(/cdn\.(test-)?form.io/) && version && version !== 'latest') { url += `/${version}`; } return url; From 37e2422b6d6b8aa44e996f8f715e343567917dfa Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Fri, 6 Sep 2024 12:33:03 -0500 Subject: [PATCH 03/10] Adding jsdocs. --- src/utils/utils.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/utils/utils.js b/src/utils/utils.js index 1dcc2ee7b1..fadad0a87a 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -163,6 +163,11 @@ export function checkCalculated(component, submission, rowData) { * @returns {boolean} - TRUE if the condition is true; FALSE otherwise. */ +/** + * + * @param conditionPaths + * @param data + */ function getConditionalPathsRecursive(conditionPaths, data) { let currentGlobalIndex = 0; const conditionalPathsArray = []; @@ -208,6 +213,14 @@ function getConditionalPathsRecursive(conditionPaths, data) { return conditionalPathsArray; } + /** + * + * @param component + * @param condition + * @param row + * @param data + * @param instance + */ export function checkSimpleConditional(component, condition, row, data, instance) { if (condition.when) { const value = getComponentActualValue(condition.when, data, row); From df110d265103affd46512c7e07056c7d79e9eced Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Tue, 12 Nov 2024 09:30:53 -0600 Subject: [PATCH 04/10] Changing the paths to use the component paths. --- src/components/Components.js | 34 +------------- .../_classes/component/Component.js | 46 ++++++++++++++++--- .../_classes/nested/NestedComponent.js | 6 +-- .../nestedarray/NestedArrayComponent.js | 7 ++- src/components/datagrid/DataGrid.js | 5 +- src/components/datamap/DataMap.js | 2 - src/utils/formUtils.js | 14 +++--- src/utils/utils.js | 21 +-------- 8 files changed, 60 insertions(+), 75 deletions(-) diff --git a/src/components/Components.js b/src/components/Components.js index 47cc8b4e94..d35a362d66 100644 --- a/src/components/Components.js +++ b/src/components/Components.js @@ -1,7 +1,6 @@ import Component from './_classes/component/Component'; import EditFormUtils from './_classes/component/editForm/utils'; import BaseEditForm from './_classes/component/Component.form'; -import { getComponentKey, getModelType } from '../utils/utils'; import _ from 'lodash'; export default class Components { static _editFormUtils = EditFormUtils; @@ -57,35 +56,6 @@ export default class Components { Components.components[name] = comp; } - /** - * Return a path of component's value. - * @param {Component} component - The component instance. - * @returns {string} - The component's value path. - */ - static getComponentPath(component) { - let path = ''; - const componentKey = getComponentKey(component.component); - if (componentKey) { - let thisPath = component.options?.parent || component; - while (thisPath && !thisPath.allowData && thisPath.parent) { - thisPath = thisPath.parent; - } - // TODO: any component that is nested in e.g. a Data Grid or an Edit Grid is going to receive a row prop; the problem - // is that options.row is passed to each further nested component, which results in erroneous paths like - // `editGrid[0].container[0].textField` rather than `editGrid[0].container.textField`. This should be adapted for other - // components with a tree-like data model - const rowIndex = component.row; - const rowIndexPath = rowIndex && !['container'].includes(thisPath.component.type) ? `[${Number.parseInt(rowIndex)}]` : ''; - path = `${thisPath.path}${rowIndexPath}.`; - if (rowIndexPath && getModelType(thisPath) === 'nestedDataArray') { - path = `${path}data.`; - } - path += componentKey; - return _.trim(path, '.'); - } - return path; - } - static create(component, options, data) { let comp = null; if (component.type && Components.components.hasOwnProperty(component.type)) { @@ -110,9 +80,7 @@ export default class Components { else { comp = new Component(component, options, data); } - const path = Components.getComponentPath(comp); - if (path) { - comp.path = path; + if (comp.path) { comp.componentsMap[comp.path] = comp; } return comp; diff --git a/src/components/_classes/component/Component.js b/src/components/_classes/component/Component.js index 62d9305828..c5b332c1d3 100644 --- a/src/components/_classes/component/Component.js +++ b/src/components/_classes/component/Component.js @@ -1,5 +1,7 @@ /* globals Quill, ClassicEditor, CKEDITOR */ import { conformToMask } from '@formio/vanilla-text-mask'; +import { Utils } from '@formio/core/utils'; +const { componentPath, COMPONENT_PATH, setComponentScope, setDefaultComponentPaths, setParentReference } = Utils; import tippy from 'tippy.js'; import _ from 'lodash'; import isMobile from 'ismobilejs'; @@ -256,6 +258,11 @@ export default class Component extends Element { */ this._hasCondition = null; + /** + * The row index for this component. + */ + this._rowIndex = undefined; + /** * References to dom elements */ @@ -270,12 +277,6 @@ export default class Component extends Element { _.merge(component, this.options.components[component.type]); } - /** - * The data path to this specific component instance. - * @type {string} - */ - this.path = component?.key || ''; - /** * An array of all the children components errors. */ @@ -360,6 +361,11 @@ export default class Component extends Element { */ this.parent = this.options.parent; + // Set the component paths for this component. + setParentReference(component, this.parent?.component); + setDefaultComponentPaths(component); + setComponentScope(component, 'dataPath', componentPath(component, COMPONENT_PATH.DATA)); + setComponentScope(component, 'localDataPath', componentPath(component, COMPONENT_PATH.LOCAL_DATA)); this.options.name = this.options.name || 'data'; this._path = ''; @@ -541,6 +547,26 @@ export default class Component extends Element { } } + /** + * Get Row Index. + * @returns {number} - The row index. + */ + get rowIndex() { + return this._rowIndex; + } + + /** + * Set Row Index to row and update each component. + * @param {number} value - The row index. + * @returns {void} + */ + set rowIndex(value) { + setComponentScope(this.component, 'dataIndex', value); + setComponentScope(this.component, 'dataPath', componentPath(this.component, COMPONENT_PATH.DATA)); + setComponentScope(this.component, 'localDataPath', componentPath(this.component, COMPONENT_PATH.LOCAL_DATA)); + this._rowIndex = value; + } + afterComponentAssign() { //implement in extended classes } @@ -623,6 +649,14 @@ export default class Component extends Element { return _.get(this.component, 'key', ''); } + get path() { + return this.component.scope?.dataPath; + } + + set path(path) { + throw new Error('Should not be setting the path of a component.'); + } + set parentVisible(value) { this._parentVisible = value; } diff --git a/src/components/_classes/nested/NestedComponent.js b/src/components/_classes/nested/NestedComponent.js index e429f71ff8..ebcf7744b2 100644 --- a/src/components/_classes/nested/NestedComponent.js +++ b/src/components/_classes/nested/NestedComponent.js @@ -1,7 +1,7 @@ 'use strict'; import _ from 'lodash'; import Field from '../field/Field'; -import Components from '../../Components'; +import Components from '../../Components';'' import { getArrayFromComponentPath, getStringFromComponentPath, getRandomComponentId } from '../../../utils/utils'; import { process as processAsync, processSync } from '@formio/core/process'; @@ -774,10 +774,6 @@ export default class NestedComponent extends Field { return; } - if(!instance.component.path) { - instance.component.path = component.path; - } - instance.checkComponentValidity(data, dirty, row, flags, scope.errors); if (instance.processOwnValidation) { scope.noRecurse = true; diff --git a/src/components/_classes/nestedarray/NestedArrayComponent.js b/src/components/_classes/nestedarray/NestedArrayComponent.js index 02a6ee11b6..9751dcdce1 100644 --- a/src/components/_classes/nestedarray/NestedArrayComponent.js +++ b/src/components/_classes/nestedarray/NestedArrayComponent.js @@ -1,6 +1,8 @@ 'use strict'; import _ from 'lodash'; +import { Utils } from '@formio/core/utils'; +const { componentPath, COMPONENT_PATH, setComponentScope } = Utils; import { componentValueTypes, getStringFromComponentPath, isLayoutComponent } from '../../../utils/utils'; import Component from '../component/Component'; @@ -26,10 +28,13 @@ export default class NestedArrayComponent extends NestedDataComponent { } get rowIndex() { - return super.rowIndex; + return this._rowIndex; } set rowIndex(value) { + setComponentScope(this.component, 'dataIndex', value); + setComponentScope(this.component, 'dataPath', componentPath(this.component, COMPONENT_PATH.DATA)); + setComponentScope(this.component, 'localDataPath', componentPath(this.component, COMPONENT_PATH.LOCAL_DATA)); this._rowIndex = value; } diff --git a/src/components/datagrid/DataGrid.js b/src/components/datagrid/DataGrid.js index 0fd9523511..3fcb67f7af 100644 --- a/src/components/datagrid/DataGrid.js +++ b/src/components/datagrid/DataGrid.js @@ -1,7 +1,6 @@ import _ from 'lodash'; import NestedArrayComponent from '../_classes/nestedarray/NestedArrayComponent'; -import { fastCloneDeep, getFocusableElements } from '../../utils/utils'; -import Components from '../Components'; +import { fastCloneDeep, getFocusableElements, setComponentScope } from '../../utils/utils'; export default class DataGridComponent extends NestedArrayComponent { static schema(...extend) { @@ -504,7 +503,6 @@ export default class DataGridComponent extends NestedArrayComponent { } component.rowIndex = rowIndex; component.row = `${rowIndex}-${colIndex}`; - component.path = Components.getComponentPath(component); }); } @@ -589,6 +587,7 @@ export default class DataGridComponent extends NestedArrayComponent { columnComponent = { ...col, id: (col.id + rowIndex) }; } + setComponentScope(this.component, 'dataIndex', rowIndex); const component = this.createComponent(columnComponent, options, row); component.parentDisabled = !!this.disabled; component.rowIndex = rowIndex; diff --git a/src/components/datamap/DataMap.js b/src/components/datamap/DataMap.js index 584710e097..0178846883 100644 --- a/src/components/datamap/DataMap.js +++ b/src/components/datamap/DataMap.js @@ -3,7 +3,6 @@ import DataGridComponent from '../datagrid/DataGrid'; import _ from 'lodash'; import EventEmitter from 'eventemitter3'; import { componentValueTypes, getComponentSavedTypes, uniqueKey } from '../../utils/utils'; -import Components from '../Components'; export default class DataMapComponent extends DataGridComponent { static schema(...extend) { @@ -275,7 +274,6 @@ export default class DataMapComponent extends DataGridComponent { delete dataValue[key]; const comp = components[this.valueKey]; comp.component.key = newKey; - comp.path = Components.getComponentPath(comp); key = newKey; }); diff --git a/src/utils/formUtils.js b/src/utils/formUtils.js index 4f7ac0eef7..c8a9136876 100644 --- a/src/utils/formUtils.js +++ b/src/utils/formUtils.js @@ -3,13 +3,14 @@ const { flattenComponents, guid, uniqueName, - MODEL_TYPES, + MODEL_TYPES_OF_KNOWN_COMPONENTS, getModelType, - getComponentAbsolutePath, getComponentPath, + setComponentScope, + resetComponentScope, isComponentNestedDataType, componentPath, - componentChildPath, + setComponentPaths, eachComponentDataAsync, eachComponentData, getComponentKey, @@ -56,13 +57,14 @@ export { flattenComponents, guid, uniqueName, - MODEL_TYPES, + MODEL_TYPES_OF_KNOWN_COMPONENTS, getModelType, - getComponentAbsolutePath, getComponentPath, + setComponentScope, + resetComponentScope, isComponentNestedDataType, componentPath, - componentChildPath, + setComponentPaths, eachComponentDataAsync, eachComponentData, getComponentKey, diff --git a/src/utils/utils.js b/src/utils/utils.js index 907d21b581..3a4302c8f5 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -32,17 +32,6 @@ jsonLogic.add_operation('relativeMaxDate', (relativeMaxDate) => { }); export { jsonLogic, ConditionOperators, moment }; -/** - * Sets the path to the component and parent schema. - * @param {import('@formio/core').Component} component - The component to set the path for. - */ -function setPathToComponentAndPerentSchema(component) { - component.path = getComponentPath(component); - const dataParent = getDataParentComponent(component); - if (dataParent && typeof dataParent === 'object') { - dataParent.path = getComponentPath(dataParent); - } -} /** * Evaluate a method. @@ -377,7 +366,6 @@ function getRow(component, row, instance, conditional) { // If no component's instance passed (happens only in 6.x server), calculate its path based on the schema if (!instance) { instance = _.cloneDeep(component); - setPathToComponentAndPerentSchema(instance); } const dataParent = getDataParentComponent(instance); const parentPath = dataParent ? getComponentPath(dataParent) : null; @@ -1664,15 +1652,10 @@ export function getComponentPathWithoutIndicies(path = '') { /** * Returns a path to the component which based on its schema * @param {import('@formio/core').Component} component - Component containing link to its parent's schema in the 'parent' property - * @param {string} path - Path to the component * @returns {string} - Path to the component */ -export function getComponentPath(component, path = '') { - if (!component || !component.key || component?._form?.display === 'wizard') { // unlike the Webform, the Wizard has the key and it is a duplicate of the panel key - return path; - } - path = component.isInputComponent || component.input === true ? `${component.key}${path ? '.' : ''}${path}` : path; - return getComponentPath(component.parent, path); +export function getComponentPath(component) { + return component.component.scope?.dataPath; } /** From 53e9dafbcb3ff54b6332ed81787104e8a6c0d77d Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Fri, 15 Nov 2024 14:15:37 -0600 Subject: [PATCH 05/10] Changing the path strategy to not require stateful objects on component object. --- src/Webform.js | 39 ++++---- src/Wizard.js | 2 +- .../_classes/component/Component.js | 38 ++++---- .../_classes/nested/NestedComponent.js | 94 ++++++++----------- .../nestedarray/NestedArrayComponent.js | 55 ++--------- src/components/datagrid/DataGrid.js | 51 +--------- src/components/datamap/DataMap.js | 1 + src/components/editgrid/EditGrid.js | 12 +-- src/components/editgrid/EditGrid.unit.js | 8 +- src/components/form/Form.js | 47 +++------- src/components/selectboxes/SelectBoxes.js | 2 +- src/utils/formUtils.js | 25 +++-- src/utils/utils.js | 21 +++-- test/unit/DataGrid.unit.js | 10 +- test/unit/EditGrid.unit.js | 12 +-- test/unit/Form.unit.js | 4 +- test/unit/Formio.unit.js | 15 ++- test/unit/NestedComponent.unit.js | 15 ++- test/unit/Webform.unit.js | 12 +-- test/unit/WebformBuilder.unit.js | 2 +- 20 files changed, 171 insertions(+), 294 deletions(-) diff --git a/src/Webform.js b/src/Webform.js index 2b6112eb28..45baa8f5a0 100644 --- a/src/Webform.js +++ b/src/Webform.js @@ -660,6 +660,7 @@ export default class Webform extends NestedDataComponent { try { // Do not set the form again if it has been already set if (isFormAlreadySet && JSON.stringify(this._form) === JSON.stringify(form)) { + this.formReadyResolve(); return Promise.resolve(); } @@ -671,11 +672,13 @@ export default class Webform extends NestedDataComponent { } if (this.parent?.component?.modalEdit) { + this.formReadyResolve(); return Promise.resolve(); } } catch (err) { console.warn(err); // If provided form is not a valid JSON object, do not set it too + this.formReadyReject(err); return Promise.resolve(); } @@ -1274,32 +1277,24 @@ export default class Webform extends NestedDataComponent { } // Mark any components as invalid if in a custom message. + const componentErrors = {}; errors.forEach((err) => { - const { components = [] } = err; - if (err.component) { - components.push(err.component); + const path = err.path || err.context?.path || err.component?.key; + if (!componentErrors[path]) { + componentErrors[path] = []; } + componentErrors[path].push(err); + }); - if (err.path) { - components.push(err.path); + // Iterate through all of our component errors and apply them to the components. + for (let path in componentErrors) { + const component = this.getComponent(path); + const errors = componentErrors[path]; + if (component) { + component.serverErrors = errors.filter((err) => err.fromServer); + component.setCustomValidity(errors, true) } - - components.forEach((path) => { - const originalPath = getStringFromComponentPath(path); - const component = this.getComponent(path, _.identity, originalPath); - - if (err.fromServer) { - if (component.serverErrors) { - component.serverErrors.push(err); - } else { - component.serverErrors = [err]; - } - } - const components = _.compact(Array.isArray(component) ? component : [component]); - - components.forEach((component) => component.setCustomValidity(err.message, true)); - }); - }); + } const displayedErrors = []; if (errors.length) { diff --git a/src/Wizard.js b/src/Wizard.js index 35c800f497..7e8f6ecfa8 100644 --- a/src/Wizard.js +++ b/src/Wizard.js @@ -1080,7 +1080,7 @@ export default class Wizard extends Webform { errors = [...errors, ...subWizard.errors] } }) - }; + } return super.showErrors(errors, triggerEvent) } diff --git a/src/components/_classes/component/Component.js b/src/components/_classes/component/Component.js index c5b332c1d3..771a1f89f2 100644 --- a/src/components/_classes/component/Component.js +++ b/src/components/_classes/component/Component.js @@ -1,7 +1,5 @@ /* globals Quill, ClassicEditor, CKEDITOR */ import { conformToMask } from '@formio/vanilla-text-mask'; -import { Utils } from '@formio/core/utils'; -const { componentPath, COMPONENT_PATH, setComponentScope, setDefaultComponentPaths, setParentReference } = Utils; import tippy from 'tippy.js'; import _ from 'lodash'; import isMobile from 'ismobilejs'; @@ -10,7 +8,7 @@ import { processOne, processOneSync, validateProcessInfo } from '@formio/core/pr import { Formio } from '../../../Formio'; import * as FormioUtils from '../../../utils/utils'; import { - fastCloneDeep, boolValue, getComponentPath, isInsideScopingComponent, currentTimezone, getScriptPlugin + fastCloneDeep, boolValue, isInsideScopingComponent, currentTimezone, getScriptPlugin } from '../../../utils/utils'; import Element from '../../../Element'; import ComponentModal from '../componentModal/ComponentModal'; @@ -361,11 +359,14 @@ export default class Component extends Element { */ this.parent = this.options.parent; - // Set the component paths for this component. - setParentReference(component, this.parent?.component); - setDefaultComponentPaths(component); - setComponentScope(component, 'dataPath', componentPath(component, COMPONENT_PATH.DATA)); - setComponentScope(component, 'localDataPath', componentPath(component, COMPONENT_PATH.LOCAL_DATA)); + /** + * The component paths for this component. + * @type {import('@formio/core').ComponentPaths} - The component paths. + */ + this.paths = FormioUtils.getComponentPaths(this.component, this.parent?.component, { + ...this.parent?.paths, + dataIndex: this.options.rowIndex === undefined ? this.parent?.paths?.dataIndex : this.options.rowIndex + }); this.options.name = this.options.name || 'data'; this._path = ''; @@ -485,12 +486,7 @@ export default class Component extends Element { /* eslint-enable max-statements */ get componentsMap() { - if (this.localRoot?.childComponentsMap) { - return this.localRoot.childComponentsMap; - } - const localMap = {}; - localMap[this.path] = this; - return localMap; + return this.root.childComponentsMap; } get data() { @@ -561,9 +557,10 @@ export default class Component extends Element { * @returns {void} */ set rowIndex(value) { - setComponentScope(this.component, 'dataIndex', value); - setComponentScope(this.component, 'dataPath', componentPath(this.component, COMPONENT_PATH.DATA)); - setComponentScope(this.component, 'localDataPath', componentPath(this.component, COMPONENT_PATH.LOCAL_DATA)); + this.paths = FormioUtils.getComponentPaths(this.component, this.parent?.component, { + ...(this.parent?.paths || {}), + ...{ dataIndex: value } + }); this._rowIndex = value; } @@ -650,7 +647,7 @@ export default class Component extends Element { } get path() { - return this.component.scope?.dataPath; + return this.paths.dataPath; } set path(path) { @@ -1491,7 +1488,7 @@ export default class Component extends Element { this.refresh(this.data, changed, flags); } else if ( - (changePath && getComponentPath(changed.instance) === refreshData) && changed && changed.instance && + (changePath && (changed.instance?.paths?.localPath === refreshData)) && changed && changed.instance && // Make sure the changed component is not in a different "context". Solves issues where refreshOn being set // in fields inside EditGrids could alter their state from other rows (which is bad). this.inContext(changed.instance) @@ -3422,7 +3419,7 @@ export default class Component extends Element { if (flags.silentCheck) { return []; } - let isDirty = this.dirty || flags.dirty; + let isDirty = (flags.dirty === false) ? false : (this.dirty || flags.dirty); if (this.options.alwaysDirty) { isDirty = true; } @@ -3449,6 +3446,7 @@ export default class Component extends Element { data, row, value: this.validationValue, + paths: this.paths, path: this.path || this.component.key, instance: this, form: this.root ? this.root._form : {}, diff --git a/src/components/_classes/nested/NestedComponent.js b/src/components/_classes/nested/NestedComponent.js index ebcf7744b2..a8e155e004 100644 --- a/src/components/_classes/nested/NestedComponent.js +++ b/src/components/_classes/nested/NestedComponent.js @@ -2,7 +2,7 @@ import _ from 'lodash'; import Field from '../field/Field'; import Components from '../../Components';'' -import { getArrayFromComponentPath, getStringFromComponentPath, getRandomComponentId } from '../../../utils/utils'; +import { getComponentPaths, getRandomComponentId, componentMatches, getBestMatch, getStringFromComponentPath } from '../../../utils/utils'; import { process as processAsync, processSync } from '@formio/core/process'; /** @@ -217,6 +217,10 @@ export default class NestedComponent extends Field { */ set rowIndex(value) { this._rowIndex = value; + this.paths = getComponentPaths(this.component, this.parent?.component, { + ...(this.parent?.paths || {}), + ...{ dataIndex: value } + }); this.eachComponent((component) => { component.rowIndex = value; }); @@ -326,62 +330,36 @@ export default class NestedComponent extends Field { } /** - * Returns a component provided a key. This performs a deep search within the - * component tree. + * Returns a component provided a key. This performs a deep search within the component tree. * @param {string} path - The path to the component. - * @param {Function} [fn] - Called with the component once found. - * @param {string} [originalPath] - The original path to the component. * @returns {any} - The component that is located. */ - getComponent(path, fn, originalPath) { - originalPath = originalPath || getStringFromComponentPath(path); - if (this.componentsMap.hasOwnProperty(originalPath)) { - if (fn) { - return fn(this.componentsMap[originalPath]); - } - else { - return this.componentsMap[originalPath]; - } - } - - path = getArrayFromComponentPath(path); - const pathStr = originalPath; - const newPath = _.clone(path); - let key = newPath.shift(); - const remainingPath = newPath; - let comp = null; - let possibleComp = null; - - if (_.isNumber(key)) { - key = remainingPath.shift(); - } - - if (!_.isString(key)) { - return comp; - } - - this.everyComponent((component, components) => { - const matchPath = component.hasInput && component.path ? pathStr.includes(component.path) : true; - if (component.component.key === key) { - possibleComp = component; - if (matchPath) { - comp = component; - if (remainingPath.length > 0 && 'getComponent' in component) { - comp = component.getComponent(remainingPath, fn, originalPath); - } - else if (fn) { - fn(component, components); - } - return false; - } - } + getComponent(path) { + path = getStringFromComponentPath(path); + const matches = { + path: undefined, + fullPath: undefined, + localPath: undefined, + fullLocalPath: undefined, + dataPath: undefined, + localDataPath: undefined, + key: undefined, + }; + this.everyComponent((component) => { + // All searches are relative to this component so replace this path from the child paths. + componentMatches(component.component, { + path: component.paths?.path?.replace(new RegExp(`^${this.paths?.path}\\.?`), ''), + fullPath: component.paths?.fullPath?.replace(new RegExp(`^${this.paths?.fullPath}\\.?`), ''), + localPath: component.paths?.localPath?.replace(new RegExp(`^${this.paths?.localPath}\\.?`), ''), + fullLocalPath: component.paths?.fullLocalPath?.replace(new RegExp(`^${this.paths?.fullLocalPath}\\.?`), ''), + dataPath: component.paths?.dataPath?.replace(new RegExp(`^${this.paths?.dataPath}\\.?`), ''), + localDataPath: component.paths?.localDataPath?.replace(new RegExp(`^${this.paths?.localDataPath}\\.?`), ''), + }, path, this.rowIndex, matches, (type, match) => { + match.instance = component; + return match; + }); }); - - if (!comp) { - comp = possibleComp; - } - - return comp; + return getBestMatch(matches)?.instance; } /** @@ -763,12 +741,12 @@ export default class NestedComponent extends Field { ); } - validationProcessor({ scope, data, row, instance, component }, flags) { + validationProcessor({ scope, data, row, instance, paths }, flags) { const { dirty } = flags; if (this.root.hasExtraPages && this.page !== this.root.page) { - instance = this.childComponentsMap?.hasOwnProperty(component.path) - ? this.childComponentsMap[component.path] - : this.getComponent(component.path); + instance = this.componentsMap?.hasOwnProperty(paths.dataPath) + ? this.componentsMap[paths.dataPath] + : this.getComponent(paths.dataPath); } if (!instance) { return; @@ -809,6 +787,8 @@ export default class NestedComponent extends Field { instances: this.componentsMap, data: data, scope: { errors: [] }, + parent: this.component, + parentPaths: this.paths, processors: [ { process: validationProcessorProcess, diff --git a/src/components/_classes/nestedarray/NestedArrayComponent.js b/src/components/_classes/nestedarray/NestedArrayComponent.js index 9751dcdce1..0167ace9bf 100644 --- a/src/components/_classes/nestedarray/NestedArrayComponent.js +++ b/src/components/_classes/nestedarray/NestedArrayComponent.js @@ -2,8 +2,8 @@ import _ from 'lodash'; import { Utils } from '@formio/core/utils'; -const { componentPath, COMPONENT_PATH, setComponentScope } = Utils; -import { componentValueTypes, getStringFromComponentPath, isLayoutComponent } from '../../../utils/utils'; +const { getComponentPaths } = Utils; +import { componentValueTypes, isLayoutComponent } from '../../../utils/utils'; import Component from '../component/Component'; import NestedDataComponent from '../nesteddata/NestedDataComponent'; @@ -32,9 +32,10 @@ export default class NestedArrayComponent extends NestedDataComponent { } set rowIndex(value) { - setComponentScope(this.component, 'dataIndex', value); - setComponentScope(this.component, 'dataPath', componentPath(this.component, COMPONENT_PATH.DATA)); - setComponentScope(this.component, 'localDataPath', componentPath(this.component, COMPONENT_PATH.LOCAL_DATA)); + this.paths = getComponentPaths(this.component, this.parent?.component, { + ...(this.parent?.paths || {}), + ...{ dataIndex: value } + }); this._rowIndex = value; } @@ -116,50 +117,6 @@ export default class NestedArrayComponent extends NestedDataComponent { }, 'show')); } - getComponent(path, fn, originalPath) { - originalPath = originalPath || getStringFromComponentPath(path); - if (this.componentsMap.hasOwnProperty(originalPath)) { - if (fn) { - return fn(this.componentsMap[originalPath]); - } - else { - return this.componentsMap[originalPath]; - } - } - path = Array.isArray(path) ? path : [path]; - let key = path.shift(); - const remainingPath = path; - let result = []; - let possibleComp = null; - let comp = null; - let rowIndex = null; - - if (_.isNumber(key)) { - rowIndex = key; - key = remainingPath.shift(); - } - if (!_.isString(key)) { - return result; - } - - this.everyComponent((component, components) => { - if (component.component.key === key) { - possibleComp = component; - if (remainingPath.length > 0 && 'getComponent' in component) { - comp = component.getComponent(remainingPath, fn, originalPath); - } - else if (fn) { - fn(component, components); - } - result = rowIndex !== null ? comp : result.concat(comp || possibleComp); - } - }, rowIndex); - if ((!result || result.length === 0) && possibleComp) { - result = rowIndex !== null ? possibleComp : [possibleComp]; - } - return result; - } - everyComponent(fn, rowIndex, options = {}) { if (_.isObject(rowIndex)) { options = rowIndex; diff --git a/src/components/datagrid/DataGrid.js b/src/components/datagrid/DataGrid.js index 3fcb67f7af..76b81e31e6 100644 --- a/src/components/datagrid/DataGrid.js +++ b/src/components/datagrid/DataGrid.js @@ -1,6 +1,6 @@ import _ from 'lodash'; import NestedArrayComponent from '../_classes/nestedarray/NestedArrayComponent'; -import { fastCloneDeep, getFocusableElements, setComponentScope } from '../../utils/utils'; +import { fastCloneDeep, getFocusableElements } from '../../utils/utils'; export default class DataGridComponent extends NestedArrayComponent { static schema(...extend) { @@ -576,6 +576,7 @@ export default class DataGridComponent extends NestedArrayComponent { const options = _.clone(this.options); options.name += `[${rowIndex}]`; options.row = `${rowIndex}-${colIndex}`; + options.rowIndex = rowIndex; let columnComponent; @@ -587,7 +588,6 @@ export default class DataGridComponent extends NestedArrayComponent { columnComponent = { ...col, id: (col.id + rowIndex) }; } - setComponentScope(this.component, 'dataIndex', rowIndex); const component = this.createComponent(columnComponent, options, row); component.parentDisabled = !!this.disabled; component.rowIndex = rowIndex; @@ -725,53 +725,6 @@ export default class DataGridComponent extends NestedArrayComponent { this.rows.forEach((row, index) => _.forIn(row, (component) => component.data = this.dataValue[index])); } - getComponent(path, fn) { - path = Array.isArray(path) ? path : [path]; - const [key, ...remainingPath] = path; - let result = []; - if (_.isNumber(key) && remainingPath.length) { - const compKey = remainingPath.pop(); - result = this.rows[key][compKey]; - // If the component is inside a Layout Component, try to find it among all the row's components - if (!result) { - Object.entries(this.rows[key]).forEach(([, comp]) => { - if ('getComponent' in comp) { - const possibleResult = comp.getComponent([compKey], fn); - if (possibleResult) { - result = possibleResult; - } - } - }); - } - if (result && _.isFunction(fn)) { - fn(result, this.getComponents()); - } - if (remainingPath.length && 'getComponent' in result) { - return result.getComponent(remainingPath, fn); - } - return result; - } - if (!_.isString(key)) { - return result; - } - - this.everyComponent((component, components) => { - if (component.component.key === key) { - let comp = component; - if (remainingPath.length > 0 && 'getComponent' in component) { - comp = component.getComponent(remainingPath, fn); - } - else if (fn) { - fn(component, components); - } - - result = result.concat(comp); - } - }); - - return result.length > 0 ? result : null; - } - toggleGroup(element, index) { element.classList.toggle('collapsed'); _.each(this.refs.chunks[index], row => { diff --git a/src/components/datamap/DataMap.js b/src/components/datamap/DataMap.js index 0178846883..68526eab38 100644 --- a/src/components/datamap/DataMap.js +++ b/src/components/datamap/DataMap.js @@ -264,6 +264,7 @@ export default class DataMapComponent extends DataGridComponent { options.events = new EventEmitter(); options.name += `[${rowIndex}]`; options.row = `${rowIndex}`; + options.rowIndex = rowIndex; const components = {}; components['__key'] = this.createComponent(this.keySchema, options, { __key: this.builderMode ? this.defaultRowKey : key }); diff --git a/src/components/editgrid/EditGrid.js b/src/components/editgrid/EditGrid.js index 819cf1872d..8f9ebe6a7b 100644 --- a/src/components/editgrid/EditGrid.js +++ b/src/components/editgrid/EditGrid.js @@ -1097,6 +1097,7 @@ export default class EditGridComponent extends NestedArrayComponent { const options = _.clone(this.options); options.name += `[${rowIndex}]`; options.row = `${rowIndex}-${colIndex}`; + options.rowIndex = rowIndex; options.onChange = (flags = {}, changed, modified) => { if (changed.instance.root?.id && (this.root?.id !== changed.instance.root.id)) { changed.instance.root.triggerChange(flags, changed, modified); @@ -1116,7 +1117,7 @@ export default class EditGridComponent extends NestedArrayComponent { ...flags, changed, }, editRow.data, editRow.components); - this.validateRow(editRow, false); + this.validateRow(editRow, false, false); } if (this.variableTypeComponentsIndexes.length) { @@ -1169,22 +1170,21 @@ export default class EditGridComponent extends NestedArrayComponent { validateRow(editRow, dirty, forceSilentCheck, fromSubmission) { editRow.errors = []; if (this.shouldValidateRow(editRow, dirty, fromSubmission)) { - const silentCheck = (this.component.rowDrafts && !this.shouldValidateDraft(editRow)) || forceSilentCheck; + const silentCheck = forceSilentCheck === false ? false : ((this.component.rowDrafts && !this.shouldValidateDraft(editRow)) || forceSilentCheck); const rootValue = fastCloneDeep(this.rootValue); const editGridValue = _.get(rootValue, this.path, []); editGridValue[editRow.rowIndex] = editRow.data; _.set(rootValue, this.path, editGridValue); const validationProcessorProcess = (context) => this.validationProcessor(context, { dirty, silentCheck }); const errors = processSync({ - components: fastCloneDeep(this.component.components).map((component) => { - component.parentPath = `${this.path}[${editRow.rowIndex}]`; - return component; - }), + components: this.component.components, data: rootValue, row: editRow.data, process: 'validateRow', instances: this.componentsMap, scope: { errors: [] }, + parent: this.component, + parentPaths: this.paths, processors: [ { process: validationProcessorProcess, diff --git a/src/components/editgrid/EditGrid.unit.js b/src/components/editgrid/EditGrid.unit.js index e2b0d551e7..72bc865710 100644 --- a/src/components/editgrid/EditGrid.unit.js +++ b/src/components/editgrid/EditGrid.unit.js @@ -971,12 +971,12 @@ describe('EditGrid Component', () => { setTimeout(() => { assert.equal(editGrid.editRows.length, 1); checkHeader(2); - const checkbox = editGrid.getComponent('checkbox')[0]; + const checkbox = editGrid.getComponent('checkbox'); checkbox.setValue(true); setTimeout(() => { checkHeader(2); - assert.equal(editGrid.getComponent('textArea')[0].visible, true); + assert.equal(editGrid.getComponent('textArea').visible, true); clickAddRow(); setTimeout(() => { @@ -1306,11 +1306,11 @@ describe('EditGrid Component', () => { editGrid1.addRow(); setTimeout(() => { - const btn = editGrid1.getComponent('setPanelValue')[0]; + const btn = editGrid1.getComponent('setPanelValue'); const clickEvent = new Event('click'); btn.refs.button.dispatchEvent(clickEvent); setTimeout(() => { - const conditionalEditGrid = editGrid1.getComponent('editGrid')[0]; + const conditionalEditGrid = editGrid1.getComponent('editGrid'); assert.deepEqual(conditionalEditGrid.dataValue, [{ textField:'testyyyy' }]); assert.equal(conditionalEditGrid.editRows.length, 1); done(); diff --git a/src/components/form/Form.js b/src/components/form/Form.js index c9d36281cf..a1c3892259 100644 --- a/src/components/form/Form.js +++ b/src/components/form/Form.js @@ -3,13 +3,7 @@ import _ from 'lodash'; import Component from '../_classes/component/Component'; import ComponentModal from '../_classes/componentModal/ComponentModal'; import EventEmitter from 'eventemitter3'; -import { - isMongoId, - eachComponent, - getStringFromComponentPath, - getArrayFromComponentPath, - componentValueTypes -} from '../../utils/utils'; +import {isMongoId, eachComponent, componentValueTypes} from '../../utils/utils'; import { Formio } from '../../Formio'; import Form from '../../Form'; @@ -154,15 +148,11 @@ export default class FormComponent extends Component { } } - getComponent(path, fn) { - path = getArrayFromComponentPath(path); - if (path[0] === 'data') { - path.shift(); - } - const originalPathStr = `${this.path}.data.${getStringFromComponentPath(path)}`; - if (this.subForm) { - return this.subForm.getComponent(path, fn, originalPathStr); + getComponent(path) { + if (!this.subForm) { + return null; } + return this.subForm.getComponent(path); } /* eslint-disable max-statements */ @@ -233,6 +223,7 @@ export default class FormComponent extends Component { if (this.options.skipDraftRestore) { options.skipDraftRestore = this.options.skipDraftRestore; } + options.parent = this; return options; } /* eslint-enable max-statements */ @@ -448,6 +439,9 @@ export default class FormComponent extends Component { return (new Form(form, this.getSubOptions())).ready.then((instance) => { this.subForm = instance; this.subForm.currentForm = this; + const componentsMap = this.componentsMap; + const formComponentsMap = this.subForm.componentsMap; + _.assign(componentsMap, formComponentsMap); this.subForm.parent = this; this.subForm.parentVisible = this.visible; this.subForm.on('change', () => { @@ -466,6 +460,8 @@ export default class FormComponent extends Component { this.valueChanged = this.hasSetValue; this.onChange(); return this.subForm; + }).catch((err) => { + console.log(err); }); }).then((subForm) => { this.updateSubWizards(subForm); @@ -527,21 +523,6 @@ export default class FormComponent extends Component { return Promise.resolve(); } - get subFormData() { - return this.dataValue?.data || {}; - } - - checkComponentValidity(data, dirty, row, options, errors = []) { - options = options || {}; - const silentCheck = options.silentCheck || false; - - if (this.subForm && !this.isNestedWizard) { - return this.subForm.checkValidity(this.subFormData, dirty, null, silentCheck, errors); - } - - return super.checkComponentValidity(data, dirty, row, options, errors); - } - checkComponentConditions(data, flags, row) { const visible = super.checkComponentConditions(data, flags, row); @@ -551,14 +532,14 @@ export default class FormComponent extends Component { } if (this.subForm) { - return this.subForm.checkConditions(this.subFormData); + return this.subForm.checkConditions(data, flags, row); } // There are few cases when subForm is not loaded when a change is triggered, // so we need to perform checkConditions after it is ready, or some conditional fields might be hidden in View mode else if (this.subFormReady) { this.subFormReady.then(() => { if (this.subForm) { - return this.subForm.checkConditions(this.subFormData); + return this.subForm.checkConditions(data, flags, row); } }); } @@ -568,7 +549,7 @@ export default class FormComponent extends Component { calculateValue(data, flags, row) { if (this.subForm) { - return this.subForm.calculateValue(this.subFormData, flags); + return this.subForm.calculateValue(data, flags, row); } return super.calculateValue(data, flags, row); diff --git a/src/components/selectboxes/SelectBoxes.js b/src/components/selectboxes/SelectBoxes.js index 6919738b63..85a8198259 100644 --- a/src/components/selectboxes/SelectBoxes.js +++ b/src/components/selectboxes/SelectBoxes.js @@ -307,7 +307,7 @@ export default class SelectBoxesComponent extends RadioComponent { return super.setCustomValidity(_.filter(messages, (message) => message.ruleName !=='invalidValueProperty'), dirty, external); } else { return super.setCustomValidity(messages, dirty, external); - }; + } } validateValueAvailability(setting, value) { diff --git a/src/utils/formUtils.js b/src/utils/formUtils.js index c8a9136876..16cedb052b 100644 --- a/src/utils/formUtils.js +++ b/src/utils/formUtils.js @@ -10,7 +10,12 @@ const { resetComponentScope, isComponentNestedDataType, componentPath, - setComponentPaths, + getComponentPaths, + componentMatches, + getBestMatch, + getComponentFromPath, + getComponentValue, + findComponents, eachComponentDataAsync, eachComponentData, getComponentKey, @@ -42,17 +47,6 @@ const { getItemTemplateKeys } = Utils; -/** - * Deprecated version of findComponents. Renamed to searchComponents. - * @param {import('@formio/core').Component[]} components - The components to find components within. - * @param {object} query - The query to use when searching for the components. - * @returns {import('@formio/core').Component[]} - The result of the component that is found. - */ -export function findComponents(components, query) { - console.warn('formio.js/utils findComponents is deprecated. Use searchComponents instead.'); - return searchComponents(components, query); -} - export { flattenComponents, guid, @@ -64,7 +58,12 @@ export { resetComponentScope, isComponentNestedDataType, componentPath, - setComponentPaths, + getComponentPaths, + componentMatches, + getBestMatch, + getComponentFromPath, + getComponentValue, + findComponents, eachComponentDataAsync, eachComponentData, getComponentKey, diff --git a/src/utils/utils.js b/src/utils/utils.js index 3a4302c8f5..ad38386a2d 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -368,15 +368,16 @@ function getRow(component, row, instance, conditional) { instance = _.cloneDeep(component); } const dataParent = getDataParentComponent(instance); - const parentPath = dataParent ? getComponentPath(dataParent) : null; - const isTriggerCondtionComponentPath = condition.when || !condition.conditions - ? condition.when?.startsWith(parentPath) - : _.some(condition.conditions, cond => cond.component.startsWith(parentPath)); - - if (dataParent && isTriggerCondtionComponentPath) { - const newRow = {}; - _.set(newRow, parentPath, row); - row = newRow; + if (dataParent) { + const parentPath = dataParent.paths?.localDataPath; + const isTriggerCondtionComponentPath = condition.when || !condition.conditions + ? condition.when?.startsWith(dataParent.paths?.localPath) + : _.some(condition.conditions, cond => cond.component.startsWith(dataParent.paths?.localPath)); + if (isTriggerCondtionComponentPath) { + const newRow = {}; + _.set(newRow, parentPath, row); + row = newRow; + } } return row; @@ -1655,7 +1656,7 @@ export function getComponentPathWithoutIndicies(path = '') { * @returns {string} - Path to the component */ export function getComponentPath(component) { - return component.component.scope?.dataPath; + return component.paths.localDataPath; } /** diff --git a/test/unit/DataGrid.unit.js b/test/unit/DataGrid.unit.js index 2677611c38..b1287c2a76 100644 --- a/test/unit/DataGrid.unit.js +++ b/test/unit/DataGrid.unit.js @@ -445,18 +445,18 @@ describe('DataGrid Component', () => { it('Should retain previous checkboxes checked property when add another is pressed (checked)', () => { return Harness.testCreate(DataGridComponent, withCheckboxes).then((component) => { - component.childComponentsMap['dataGrid[0].radio'].element.querySelector('input').click(); + component.componentsMap['dataGrid[0].radio'].element.querySelector('input').click(); component.addRow(); - assert.equal(component.childComponentsMap['dataGrid[0].radio'].element.querySelector('input').checked, true); + assert.equal(component.componentsMap['dataGrid[0].radio'].element.querySelector('input').checked, true); }); }); it('Should retain previous checkboxes checked property when add another is pressed (unchecked)', () => { return Harness.testCreate(DataGridComponent, withCheckboxes).then((component) => { - component.childComponentsMap['dataGrid[0].radio'].element.querySelector('input').click(); - component.childComponentsMap['dataGrid[0].radio'].element.querySelector('input').click(); + component.componentsMap['dataGrid[0].radio'].element.querySelector('input').click(); + component.componentsMap['dataGrid[0].radio'].element.querySelector('input').click(); component.addRow(); - assert.equal(component.childComponentsMap['dataGrid[0].radio'].element.querySelector('input').checked, false); + assert.equal(component.componentsMap['dataGrid[0].radio'].element.querySelector('input').checked, false); }); }); diff --git a/test/unit/EditGrid.unit.js b/test/unit/EditGrid.unit.js index dfd949a8b4..47773d913c 100644 --- a/test/unit/EditGrid.unit.js +++ b/test/unit/EditGrid.unit.js @@ -595,7 +595,7 @@ describe('EditGrid Component', () => { setTimeout(() => { editGrid.editRow(0).then(() => { - const textField = form.getComponent(['editGrid', 0, 'form', 'textField']); + const textField = form.getComponent(['editGrid', 0, 'form', 'data', 'textField']); textField.setValue('someValue'); @@ -971,12 +971,12 @@ describe('EditGrid Component', () => { setTimeout(() => { assert.equal(editGrid.editRows.length, 1); checkHeader(2); - const checkbox = editGrid.getComponent('checkbox')[0]; + const checkbox = editGrid.getComponent('checkbox'); checkbox.setValue(true); setTimeout(() => { checkHeader(2); - assert.equal(editGrid.getComponent('textArea')[0].visible, true); + assert.equal(editGrid.getComponent('textArea').visible, true); clickAddRow(); setTimeout(() => { @@ -1211,7 +1211,7 @@ describe('EditGrid Component', () => { }).catch(done); }); - it('Should show validation when saving a row with required conditional filed inside container', (done) => { + it('Should show validation when saving a row with required conditional field inside container', (done) => { const form = _.cloneDeep(comp12); const element = document.createElement('div'); @@ -1306,11 +1306,11 @@ describe('EditGrid Component', () => { editGrid1.addRow(); setTimeout(() => { - const btn = editGrid1.getComponent('setPanelValue')[0]; + const btn = editGrid1.getComponent('setPanelValue'); const clickEvent = new Event('click'); btn.refs.button.dispatchEvent(clickEvent); setTimeout(() => { - const conditionalEditGrid = editGrid1.getComponent('editGrid')[0]; + const conditionalEditGrid = editGrid1.getComponent('editGrid'); assert.deepEqual(conditionalEditGrid.dataValue, [{ textField:'testyyyy' }]); assert.equal(conditionalEditGrid.editRows.length, 1); done(); diff --git a/test/unit/Form.unit.js b/test/unit/Form.unit.js index 19867a3bbb..3ad51c6e19 100644 --- a/test/unit/Form.unit.js +++ b/test/unit/Form.unit.js @@ -192,7 +192,9 @@ describe('Form Component', () => { const preview = form.element.querySelector('[ref="openModal"]'); assert(preview, 'Should contain element to open a modal window'); done(); - }).catch(done); + }).catch(function(err) { + done(err); + }); }); }); diff --git a/test/unit/Formio.unit.js b/test/unit/Formio.unit.js index 55d4f29665..1ddaf99a21 100644 --- a/test/unit/Formio.unit.js +++ b/test/unit/Formio.unit.js @@ -1955,7 +1955,8 @@ describe('Formio.js Tests', () => { components: [ { defaultPermission: 'read', - key: 'groupField' + key: 'groupField', + type: 'hidden' } ] }, @@ -2012,7 +2013,8 @@ describe('Formio.js Tests', () => { components: [ { defaultPermission: 'create', - key: 'groupField' + key: 'groupField', + type: 'hidden' } ] }, @@ -2069,7 +2071,8 @@ describe('Formio.js Tests', () => { components: [ { defaultPermission: 'write', - key: 'groupField' + key: 'groupField', + type: 'hidden' } ] }, @@ -2126,7 +2129,8 @@ describe('Formio.js Tests', () => { components: [ { defaultPermission: 'admin', - key: 'groupField' + key: 'groupField', + type: 'hidden' } ] }, @@ -2200,7 +2204,8 @@ describe('Formio.js Tests', () => { components: [ { defaultPermission: 'read', - key: 'groupField' + key: 'groupField', + type: 'hidden' } ] }, diff --git a/test/unit/NestedComponent.unit.js b/test/unit/NestedComponent.unit.js index 1fa954b1b7..6b994d093c 100644 --- a/test/unit/NestedComponent.unit.js +++ b/test/unit/NestedComponent.unit.js @@ -248,11 +248,16 @@ describe('NestedComponent class', () => { const dataGrid = childForm.components[1]; const tabs = childForm.components[2]; - assert(textField.path === 'textField'); - assert(dataGrid.path === 'dataGrid'); - assert(dataGrid.components[0].path === 'dataGrid[0].textField'); - assert(tabs.path === 'tabs'); - assert(tabs.tabs[0][0].path === 'tabsTextfield'); + assert(textField.paths.localDataPath === 'textField'); + assert(textField.path === 'form.data.textField'); + assert(dataGrid.path === 'form.data.dataGrid'); + assert(dataGrid.paths.localDataPath === 'dataGrid'); + assert(dataGrid.components[0].path === 'form.data.dataGrid[0].textField'); + assert(dataGrid.components[0].paths.localDataPath === 'dataGrid[0].textField'); + assert(tabs.paths.localPath === ''); + assert(tabs.path === 'form.data'); + assert(tabs.tabs[0][0].path === 'form.data.tabsTextfield') + assert(tabs.tabs[0][0].paths.localDataPath === 'tabsTextfield'); done(); }) .catch(done); diff --git a/test/unit/Webform.unit.js b/test/unit/Webform.unit.js index 28cb768d2f..bd48bcac52 100644 --- a/test/unit/Webform.unit.js +++ b/test/unit/Webform.unit.js @@ -135,8 +135,8 @@ describe('Webform tests', function() { form.setForm(formsWithSimpleConditionals.form2).then(() => { const compWithDuplicatedKey1 = form.getComponent('container.textField'); - const compWithDuplicatedKey2 = form.getComponent('dataGrid.container.textField')[0]; - const conditionalCompShownOnDupl1Or2 = form.getComponent('dataGrid.number')[0]; + const compWithDuplicatedKey2 = form.getComponent('dataGrid[0].container.textField'); + const conditionalCompShownOnDupl1Or2 = form.getComponent('dataGrid[0].number'); const dataGrid = form.getComponent('dataGrid'); assert.equal(conditionalCompShownOnDupl1Or2.visible, false); compWithDuplicatedKey1.setValue('6'); @@ -146,14 +146,14 @@ describe('Webform tests', function() { compWithDuplicatedKey1.setValue('7'); setTimeout(() => { - const conditionalCompShownOnDupl1Or2 = form.getComponent('dataGrid.number')[0]; + const conditionalCompShownOnDupl1Or2 = form.getComponent('dataGrid[0].number'); assert.equal(conditionalCompShownOnDupl1Or2.visible, false); compWithDuplicatedKey2.setValue('5'); setTimeout(() => { assert.equal(conditionalCompShownOnDupl1Or2.visible, true); dataGrid.addRow(); setTimeout(() => { - const conditionalComp2ShownOnDupl1Or2 = form.getComponent('dataGrid.number')[1]; + const conditionalComp2ShownOnDupl1Or2 = form.getComponent('dataGrid[1].number'); assert.equal(conditionalCompShownOnDupl1Or2.visible, true); assert.equal(conditionalComp2ShownOnDupl1Or2.visible, false); compWithDuplicatedKey1.setValue('6'); @@ -645,8 +645,8 @@ describe('Webform tests', function() { const form = new Webform(formElement); form.setForm(formWithEventLogicInHiddenComponent).then(() => { - const regesteredAddress = form.getComponent('registeredAddressInformation').getComponent('streetAddress')[0]; - const address = form.getComponent('addressInformation').getComponent('streetAddress')[0]; + const regesteredAddress = form.getComponent('registeredAddressInformation').getComponent('streetAddress'); + const address = form.getComponent('addressInformation').getComponent('streetAddress'); assert.equal(address.visible, true); assert.equal(regesteredAddress.visible, false); diff --git a/test/unit/WebformBuilder.unit.js b/test/unit/WebformBuilder.unit.js index 3352982454..963fd0f9d7 100644 --- a/test/unit/WebformBuilder.unit.js +++ b/test/unit/WebformBuilder.unit.js @@ -330,7 +330,7 @@ describe('WebformBuilder tests', function() { }, ], }).then(() => { - const textField = builder.webform.getComponent(['dataGrid', 'textField'])[0]; + const textField = builder.webform.getComponent('dataGrid[0].textField'); textField.refs.removeComponent.dispatchEvent( new MouseEvent('click', { view: window, bubbles: true, From 6ff4457f1a8ba95de732f222a48c09983e0562c2 Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Sun, 17 Nov 2024 19:50:27 -0600 Subject: [PATCH 06/10] Adding other scope options to the processors. --- src/components/_classes/component/Component.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/_classes/component/Component.js b/src/components/_classes/component/Component.js index 771a1f89f2..23058c9614 100644 --- a/src/components/_classes/component/Component.js +++ b/src/components/_classes/component/Component.js @@ -8,7 +8,7 @@ import { processOne, processOneSync, validateProcessInfo } from '@formio/core/pr import { Formio } from '../../../Formio'; import * as FormioUtils from '../../../utils/utils'; import { - fastCloneDeep, boolValue, isInsideScopingComponent, currentTimezone, getScriptPlugin + fastCloneDeep, boolValue, isInsideScopingComponent, currentTimezone, getScriptPlugin, getContextualRowData } from '../../../utils/utils'; import Element from '../../../Element'; import ComponentModal from '../componentModal/ComponentModal'; @@ -3342,6 +3342,9 @@ export default class Component extends Element { * @returns {string} - The message to show when the component is invalid. */ invalidMessage(data, dirty, ignoreCondition, row) { + if (!row) { + row = getContextualRowData(this.component, data, this.paths); + } if (!ignoreCondition && !this.checkCondition(row, data)) { return ''; } @@ -3362,6 +3365,8 @@ export default class Component extends Element { data, row, path: this.path || this.component.key, + parent: this.parent?.component, + paths: this.paths, scope: validationScope, instance: this, processors: [ @@ -3446,6 +3451,7 @@ export default class Component extends Element { data, row, value: this.validationValue, + parent: this.parent?.component, paths: this.paths, path: this.path || this.component.key, instance: this, From 0c8ba4b33592c0093ec996b43f39f345ee687a52 Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Mon, 18 Nov 2024 11:39:19 -0600 Subject: [PATCH 07/10] Fixing the eachComponent reference in webform to use the correct one. --- src/Webform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Webform.js b/src/Webform.js index 45baa8f5a0..f53b265983 100644 --- a/src/Webform.js +++ b/src/Webform.js @@ -1775,7 +1775,7 @@ export default class Webform extends NestedDataComponent { return; } const captchaComponent = []; - eachComponent(this.components, (component) => { + this.eachComponent((component) => { if (/^(re)?captcha$/.test(component.type) && component.component.eventType === 'formLoad') { captchaComponent.push(component); } From 305d7f0bd3f98b787e5dc5e7a6dcb8c28be897fa Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Mon, 18 Nov 2024 12:54:00 -0600 Subject: [PATCH 08/10] Fixing edit grid row validations. --- src/Wizard.js | 2 +- src/components/editgrid/EditGrid.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Wizard.js b/src/Wizard.js index 7e8f6ecfa8..3f6dee273b 100644 --- a/src/Wizard.js +++ b/src/Wizard.js @@ -432,7 +432,7 @@ export default class Wizard extends Webform { attachHeader() { const isAllowPrevious = this.isAllowPrevious(); - this.attachTooltips(this.refs[`${this.wizardKey}-tooltip`], this.currentPanel.tooltip); + this.attachTooltips(this.refs[`${this.wizardKey}-tooltip`], this.currentPanel?.tooltip); if (this.isBreadcrumbClickable() || isAllowPrevious) { this.refs[`${this.wizardKey}-link`]?.forEach((link, index) => { diff --git a/src/components/editgrid/EditGrid.js b/src/components/editgrid/EditGrid.js index 8f9ebe6a7b..317296f6f0 100644 --- a/src/components/editgrid/EditGrid.js +++ b/src/components/editgrid/EditGrid.js @@ -1184,7 +1184,10 @@ export default class EditGridComponent extends NestedArrayComponent { instances: this.componentsMap, scope: { errors: [] }, parent: this.component, - parentPaths: this.paths, + parentPaths: { + ...this.paths, + dataIndex: editRow.rowIndex + }, processors: [ { process: validationProcessorProcess, From 259ad94e300c748da2e03a6cc2b94380df4139b6 Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Mon, 18 Nov 2024 21:22:43 -0600 Subject: [PATCH 09/10] Fixing the conditions to use the correct path. --- package.json | 2 +- src/Webform.js | 7 ++--- .../_classes/component/Component.js | 1 + .../_classes/nested/NestedComponent.js | 1 + src/components/form/Form.js | 2 +- .../conditionOperators/DateGreaterThan.js | 4 +-- src/utils/conditionOperators/IsEmptyValue.js | 4 +-- src/utils/conditionOperators/IsEqualTo.js | 4 +-- src/utils/utils.js | 7 +++-- test/unit/Tags.unit.js | 26 ++++++++++--------- yarn.lock | 15 +++++++---- 11 files changed, 41 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index dd333b3f62..e84c9874d0 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.189.e38e07a", "@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/Webform.js b/src/Webform.js index f53b265983..4c2242ce7d 100644 --- a/src/Webform.js +++ b/src/Webform.js @@ -1519,7 +1519,7 @@ export default class Webform extends NestedDataComponent { }); } - submitForm(options = {}) { + submitForm(options = {}, local = false) { this.clearServerErrors(); return new Promise((resolve, reject) => { @@ -1554,6 +1554,7 @@ export default class Webform extends NestedDataComponent { return reject("Invalid Submission"); } const errors = this.validate(submission.data, { + local, dirty: true, silentCheck: false, process: "submit", @@ -1573,11 +1574,11 @@ export default class Webform extends NestedDataComponent { this.everyComponent((comp) => { if (submission._vnote && comp.type === "form" && comp.component.reference) { - _.get(submission.data, comp.path, {})._vnote = submission._vnote; + _.get(submission.data, local ? comp.paths?.localDataPath : comp.path, {})._vnote = submission._vnote; } const { persistent } = comp.component; if (persistent === "client-only") { - _.unset(submission.data, comp.path); + _.unset(submission.data, local ? comp.paths?.localDataPath : comp.path); } }); diff --git a/src/components/_classes/component/Component.js b/src/components/_classes/component/Component.js index 23058c9614..d451bda131 100644 --- a/src/components/_classes/component/Component.js +++ b/src/components/_classes/component/Component.js @@ -3450,6 +3450,7 @@ export default class Component extends Element { component: this.component, data, row, + local: !!flags.local, value: this.validationValue, parent: this.parent?.component, paths: this.paths, diff --git a/src/components/_classes/nested/NestedComponent.js b/src/components/_classes/nested/NestedComponent.js index a8e155e004..b05c4f680f 100644 --- a/src/components/_classes/nested/NestedComponent.js +++ b/src/components/_classes/nested/NestedComponent.js @@ -786,6 +786,7 @@ export default class NestedComponent extends Field { components, instances: this.componentsMap, data: data, + local: !!flags.local, scope: { errors: [] }, parent: this.component, parentPaths: this.paths, diff --git a/src/components/form/Form.js b/src/components/form/Form.js index a1c3892259..9eb8b552be 100644 --- a/src/components/form/Form.js +++ b/src/components/form/Form.js @@ -596,7 +596,7 @@ export default class FormComponent extends Component { } this.subForm.nosubmit = false; this.subForm.submitted = true; - return this.subForm.submitForm().then(result => { + return this.subForm.submitForm({}, true).then(result => { this.subForm.loading = false; this.subForm.showAllErrors = false; this.dataValue = result.submission; diff --git a/src/utils/conditionOperators/DateGreaterThan.js b/src/utils/conditionOperators/DateGreaterThan.js index 8324ce7025..dca3235296 100644 --- a/src/utils/conditionOperators/DateGreaterThan.js +++ b/src/utils/conditionOperators/DateGreaterThan.js @@ -18,7 +18,7 @@ export default class DateGeaterThan extends ConditionOperator { } execute(options, functionName = 'isAfter') { - const { value, instance, conditionComponentPath } = options; + const { value, instance, path } = options; if (!value) { return false; @@ -27,7 +27,7 @@ export default class DateGeaterThan extends ConditionOperator { let conditionTriggerComponent = null; if (instance?.root?.getComponent) { - conditionTriggerComponent = instance.root.getComponent(conditionComponentPath); + conditionTriggerComponent = instance.root.getComponent(path); } if ( conditionTriggerComponent && conditionTriggerComponent.isPartialDay && conditionTriggerComponent.isPartialDay(value)) { diff --git a/src/utils/conditionOperators/IsEmptyValue.js b/src/utils/conditionOperators/IsEmptyValue.js index 5514771721..40137bede8 100644 --- a/src/utils/conditionOperators/IsEmptyValue.js +++ b/src/utils/conditionOperators/IsEmptyValue.js @@ -14,11 +14,11 @@ export default class IsEmptyValue extends ConditionOperator { return false; } - execute({ value, instance, conditionComponentPath }) { + execute({ value, instance, path }) { const isEmptyValue = _.isEmpty(_.isNumber(value)? String(value): value); if (instance?.root?.getComponent) { - const conditionTriggerComponent = instance.root.getComponent(conditionComponentPath); + const conditionTriggerComponent = instance.root.getComponent(path); return conditionTriggerComponent?.isEmpty ? conditionTriggerComponent.isEmpty() : isEmptyValue; } diff --git a/src/utils/conditionOperators/IsEqualTo.js b/src/utils/conditionOperators/IsEqualTo.js index 4ee93895a7..59ac42144c 100644 --- a/src/utils/conditionOperators/IsEqualTo.js +++ b/src/utils/conditionOperators/IsEqualTo.js @@ -11,7 +11,7 @@ export default class IsEqualTo extends ConditionOperator { return 'Is Equal To'; } - execute({ value, comparedValue, instance, conditionComponentPath }) { + execute({ value, comparedValue, instance, path }) { if ((value || value === false) && comparedValue && typeof value !== typeof comparedValue && _.isString(comparedValue)) { try { comparedValue = JSON.parse(comparedValue); @@ -21,7 +21,7 @@ export default class IsEqualTo extends ConditionOperator { } if (instance?.root?.getComponent) { - const conditionTriggerComponent = instance.root.getComponent(conditionComponentPath); + const conditionTriggerComponent = instance.root.getComponent(path); if ( conditionTriggerComponent diff --git a/src/utils/utils.js b/src/utils/utils.js index ad38386a2d..edee77c8ae 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -245,13 +245,12 @@ function getConditionalPathsRecursive(conditionPaths, data) { const conditionalPaths = instance?.parent?.type === 'datagrid' || instance?.parent?.type === 'editgrid' ? [] : getConditionalPathsRecursive(splittedConditionPath, data); - if (conditionalPaths.length>0) { + if (conditionalPaths.length > 0) { return conditionalPaths.map((path) => { const value = getComponentActualValue(path, data, row); - const ConditionOperator = ConditionOperators[operator]; return ConditionOperator - ? new ConditionOperator().getResult({ value, comparedValue, instance, component, conditionComponentPath }) + ? new ConditionOperator().getResult({ value, comparedValue, instance, component, path }) : true; }); } @@ -259,7 +258,7 @@ function getConditionalPathsRecursive(conditionPaths, data) { const value = getComponentActualValue(conditionComponentPath, data, row); const СonditionOperator = ConditionOperators[operator]; return СonditionOperator - ? new СonditionOperator().getResult({ value, comparedValue, instance, component, conditionComponentPath }) + ? new СonditionOperator().getResult({ value, comparedValue, instance, component, path: conditionComponentPath }) : true; } }); diff --git a/test/unit/Tags.unit.js b/test/unit/Tags.unit.js index a80c010218..10f362dcc3 100644 --- a/test/unit/Tags.unit.js +++ b/test/unit/Tags.unit.js @@ -143,20 +143,22 @@ describe('Tags Component', function() { const element = document.createElement('div'); Formio.createForm(element, comp6).then(form => { - const tags = form.getComponent('tags'); - // tags.setValue(['1', '2', '3']); - Harness.setTagsValue(['test', 'test1', 'test2'], tags); - tags.choices.input.element.focus(); - setTimeout(() => { - assert.equal(tags.errors.length, 0, 'Tags should be valid while changing'); - tags.choices.input.element.dispatchEvent(new Event('blur')); - + const tags = form.getComponent('tags'); + // tags.setValue(['1', '2', '3']); + Harness.setTagsValue(['test', 'test1', 'test2'], tags); + tags.choices.input.element.focus(); + setTimeout(() => { - assert.equal(tags.errors.length, 1, 'Should set error after Tags component was blurred'); - done(); - }, 500); - }, 350); + assert.equal(tags.errors.length, 0, 'Tags should be valid while changing'); + tags.choices.input.element.dispatchEvent(new Event('blur')); + + setTimeout(() => { + assert.equal(tags.errors.length, 1, 'Should set error after Tags component was blurred'); + done(); + }, 500); + }, 350); + }, 10); }).catch(done); }); }); diff --git a/yarn.lock b/yarn.lock index 2ab2a0c002..0f2e60da74 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.189.e38e07a": + version "2.1.0-dev.189.e38e07a" + resolved "https://registry.npmjs.org/@formio/core/-/core-2.1.0-dev.189.e38e07a.tgz#6206322a8a644704651eaa03732f3b93aa2139ac" + integrity sha512-TXa3xTYdaYKWNvc1WzummmNqekFrGhiJmXFecJAqQhmT1SG0dgELE2lH99jHN6AAj5+8VL0TKFKYtqgUy48kHQ== 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.npmjs.org/dompurify/-/dompurify-3.2.0.tgz#53c414317c51503183696fcdef6dd3f916c607ed" + integrity sha512-AMdOzK44oFWqHEi0wpOqix/fUNY707OmoeFDnbi3Q5I8uOpy21ufUA5cDJPr0bosxrflOVD/H2DMSvuGKJGfmQ== + downloadjs@^1.4.7: version "1.4.7" resolved "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz#f69f96f940e0d0553dac291139865a3cd0101e3c" From 722333b5e17b0448d22decb5d062cba08b8faa10 Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Tue, 19 Nov 2024 08:05:29 -0600 Subject: [PATCH 10/10] Changing timing to trigger validations. --- test/unit/TextField.unit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/TextField.unit.js b/test/unit/TextField.unit.js index 5016e3b0b2..16e81a3bf0 100644 --- a/test/unit/TextField.unit.js +++ b/test/unit/TextField.unit.js @@ -374,7 +374,7 @@ describe('TextField Component', () => { if (_.isEqual(value, lastValue)) { done(); } - }, 300); + }, 350); }).catch(done); }); };