From 40df22b57604624b91281b31189c23ba00309c09 Mon Sep 17 00:00:00 2001 From: Alex Molodyh <140214274+amolodyh-hyland@users.noreply.github.com> Date: Tue, 2 Dec 2025 08:30:23 -0800 Subject: [PATCH] AAE-40028 Added improvement to ignore field validation if it is inside a group/section that is hidden (#11362) * [AAE-40028] Added improvement to ignore field validation if it is inside a group/section that is hidden --- docs/core/models/form-field.model.md | 1 + docs/core/models/form-model.md | 3 + .../widgets/core/form-field-validator.spec.ts | 932 ++++++++++++++++++ .../widgets/core/form-field-validator.ts | 18 +- .../widgets/core/form-field.model.ts | 2 + .../widgets/core/form.model.spec.ts | 312 ++++++ .../components/widgets/core/form.model.ts | 124 +++ 7 files changed, 1383 insertions(+), 9 deletions(-) diff --git a/docs/core/models/form-field.model.md b/docs/core/models/form-field.model.md index ffee2a75ce..92ad9b1cc5 100644 --- a/docs/core/models/form-field.model.md +++ b/docs/core/models/form-field.model.md @@ -43,6 +43,7 @@ Contains the value and metadata for a field of a [`Form`](../../../lib/process-s | displayText | string | | Displayed text for Hyperlink widgets | | isVisible | boolean | true | Is the field shown on the form? | | visibilityCondition | [`WidgetVisibilityModel`](../../../lib/core/src/lib/form/models/widget-visibility.model.ts) | null | Defines a expression that determines whether the field is visible or not, based on its logical relation to values in other fields | +| checkParentVisibilityForValidation | boolean | false | When enabled (and `FormModel.enableParentVisibilityCheck` is true), validation will be skipped if the field's parent container/group/section is hidden, even if the field itself is visible. | | enableFractions | boolean | false | Are numeric values allowed to contain a decimal point? | | currency | string | null | Currency symbol for Amount widgets | | dateDisplayFormat | string | | Date/time display format template | diff --git a/docs/core/models/form-model.md b/docs/core/models/form-model.md index f72dc6a945..a04e9a356b 100644 --- a/docs/core/models/form-model.md +++ b/docs/core/models/form-model.md @@ -41,6 +41,7 @@ Contains the value and metadata for a form. |isValid | true||is form valid| |processVariables| ProcessVariableModel[] | []|process variables| |variables| FormVariableModel[] | []|variables| +|enableParentVisibilityCheck| boolean | false|Enables checking parent container/group/section visibility for validation. When enabled, fields with `checkParentVisibilityForValidation: true` will skip validation if their parent is hidden.| ## Methods @@ -112,3 +113,5 @@ Contains the value and metadata for a form. Changes variable value - `loadInjectedFieldValidators(injectedFieldValidators: FormFieldValidator[]): void` Checks it there are any injectedValidators and adds them to the array of field validators. +- `isFieldOrParentHidden(field: FormFieldModel): boolean` + Checks if a field or any of its parent containers/groups/sections is hidden. Returns true if the field should skip validation. Only checks parent visibility if `enableParentVisibilityCheck` is true and the field has `checkParentVisibilityForValidation` set to true. diff --git a/lib/core/src/lib/form/components/widgets/core/form-field-validator.spec.ts b/lib/core/src/lib/form/components/widgets/core/form-field-validator.spec.ts index 69761d3718..2b3c21a19d 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field-validator.spec.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field-validator.spec.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { ContainerModel } from './container.model'; import { ErrorMessageModel } from './error-message.model'; import { FormFieldTypes } from './form-field-types'; import { @@ -201,6 +202,143 @@ describe('FormFieldValidator', () => { expect(validator.validate(field)).toBe(false); }); + + it('should skip validation for hidden field', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TEXT, + required: true, + value: null + }); + field.isVisible = false; + expect(validator.validate(field)).toBe(true); + }); + + it('should skip validation for field in hidden group when opt-in is enabled', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true, checkParentVisibilityForValidation: true }] } + } + ] + }; + const form = new FormModel(formJson); + form.enableParentVisibilityCheck = true; + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = null; + expect(validator.validate(field)).toBe(true); + }); + + it('should not skip validation for field in hidden group when opt-in is disabled - backward compatible', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true, checkParentVisibilityForValidation: false }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = null; + expect(validator.validate(field)).toBe(false); + }); + + it('should skip validation for field in hidden section when opt-in is enabled', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'section1', + type: FormFieldTypes.SECTION, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true, checkParentVisibilityForValidation: true }] } + } + ] + }; + const form = new FormModel(formJson); + form.enableParentVisibilityCheck = true; + const container = form.fields[0] as ContainerModel; + container.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = null; + expect(validator.validate(field)).toBe(true); + }); + + it('should not skip validation for field in hidden section when opt-in is disabled - backward compatible', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'section1', + type: FormFieldTypes.SECTION, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true, checkParentVisibilityForValidation: false }] } + } + ] + }; + const form = new FormModel(formJson); + const container = form.fields[0] as ContainerModel; + container.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = null; + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation when opt-in property defaults to false - backward compatible', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = null; + expect(field.checkParentVisibilityForValidation).toBe(false); + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation for visible field with invalid value', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TEXT, + required: true, + value: null + }); + field.isVisible = true; + expect(validator.validate(field)).toBe(false); + }); }); describe('NumberFieldValidator', () => { @@ -264,6 +402,99 @@ describe('FormFieldValidator', () => { expect(validator.validate(field)).toBe(false); expect(field.validationSummary).not.toBeNull(); }); + + it('should skip validation for hidden field', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.NUMBER, + value: 'not-a-number' + }); + field.isVisible = false; + expect(validator.validate(field)).toBe(true); + }); + + it('should skip validation for field in hidden group when opt-in is enabled', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.NUMBER, checkParentVisibilityForValidation: true }] } + } + ] + }; + const form = new FormModel(formJson); + form.enableParentVisibilityCheck = true; + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = 'not-a-number'; + expect(validator.validate(field)).toBe(true); + }); + + it('should not skip validation for field in hidden group when opt-in is disabled - backward compatible', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.NUMBER, checkParentVisibilityForValidation: false }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = 'not-a-number'; + field.validationSummary = new ErrorMessageModel(); + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation when opt-in property defaults to false - backward compatible', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.NUMBER }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = 'not-a-number'; + field.validationSummary = new ErrorMessageModel(); + expect(field.checkParentVisibilityForValidation).toBe(false); + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation for visible field with invalid value', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.NUMBER, + value: 'not-a-number' + }); + field.isVisible = true; + field.validationSummary = new ErrorMessageModel(); + expect(validator.validate(field)).toBe(false); + }); }); describe('MinLengthFieldValidator', () => { @@ -315,6 +546,101 @@ describe('FormFieldValidator', () => { expect(validator.validate(field)).toBe(false); expect(field.validationSummary).not.toBeNull(); }); + + it('should skip validation for hidden field', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TEXT, + value: 'ab', + minLength: 5 + }); + field.isVisible = false; + expect(validator.validate(field)).toBe(true); + }); + + it('should skip validation for field in hidden group when opt-in is enabled', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, minLength: 5, checkParentVisibilityForValidation: true }] } + } + ] + }; + const form = new FormModel(formJson); + form.enableParentVisibilityCheck = true; + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = 'ab'; + expect(validator.validate(field)).toBe(true); + }); + + it('should not skip validation for field in hidden group when opt-in is disabled - backward compatible', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, minLength: 5, checkParentVisibilityForValidation: false }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = 'ab'; + field.validationSummary = new ErrorMessageModel(); + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation when opt-in property defaults to false - backward compatible', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, minLength: 5 }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = 'ab'; + field.validationSummary = new ErrorMessageModel(); + expect(field.checkParentVisibilityForValidation).toBe(false); + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation for visible field with invalid value', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TEXT, + value: 'ab', + minLength: 5 + }); + field.isVisible = true; + field.validationSummary = new ErrorMessageModel(); + expect(validator.validate(field)).toBe(false); + }); }); describe('MaxLengthFieldValidator', () => { @@ -404,6 +730,101 @@ describe('FormFieldValidator', () => { expect(isSupported).toBe(false); }); }); + + it('should skip validation for hidden field', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TEXT, + value: '12345', + maxLength: 3 + }); + field.isVisible = false; + expect(validator.validate(field)).toBe(true); + }); + + it('should skip validation for field in hidden group when opt-in is enabled', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, maxLength: 3, checkParentVisibilityForValidation: true }] } + } + ] + }; + const form = new FormModel(formJson); + form.enableParentVisibilityCheck = true; + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = '12345'; + expect(validator.validate(field)).toBe(true); + }); + + it('should not skip validation for field in hidden group when opt-in is disabled (backward compatible)', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, maxLength: 3, checkParentVisibilityForValidation: false }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = '12345'; + field.validationSummary = new ErrorMessageModel(); + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation when opt-in property defaults to false (backward compatible)', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, maxLength: 3 }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = '12345'; + field.validationSummary = new ErrorMessageModel(); + expect(field.checkParentVisibilityForValidation).toBe(false); + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation for visible field with invalid value', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TEXT, + value: '12345', + maxLength: 3 + }); + field.isVisible = true; + field.validationSummary = new ErrorMessageModel(); + expect(validator.validate(field)).toBe(false); + }); }); describe('MinValueFieldValidator', () => { @@ -474,6 +895,101 @@ describe('FormFieldValidator', () => { expect(validator.validate(field)).toBe(false); expect(field.validationSummary).not.toBeNull(); }); + + it('should skip validation for hidden field', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.NUMBER, + value: '5', + minValue: '10' + }); + field.isVisible = false; + expect(validator.validate(field)).toBe(true); + }); + + it('should skip validation for field in hidden group when opt-in is enabled', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.NUMBER, minValue: '10', checkParentVisibilityForValidation: true }] } + } + ] + }; + const form = new FormModel(formJson); + form.enableParentVisibilityCheck = true; + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = '5'; + expect(validator.validate(field)).toBe(true); + }); + + it('should not skip validation for field in hidden group when opt-in is disabled (backward compatible)', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.NUMBER, minValue: '10', checkParentVisibilityForValidation: false }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = '5'; + field.validationSummary = new ErrorMessageModel(); + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation when opt-in property defaults to false (backward compatible)', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.NUMBER, minValue: '10' }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = '5'; + field.validationSummary = new ErrorMessageModel(); + expect(field.checkParentVisibilityForValidation).toBe(false); + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation for visible field with invalid value', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.NUMBER, + value: '5', + minValue: '10' + }); + field.isVisible = true; + field.validationSummary = new ErrorMessageModel(); + expect(validator.validate(field)).toBe(false); + }); }); describe('MaxValueFieldValidator', () => { @@ -544,6 +1060,101 @@ describe('FormFieldValidator', () => { expect(validator.validate(field)).toBe(false); expect(field.validationSummary).not.toBeNull(); }); + + it('should skip validation for hidden field', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.NUMBER, + value: '15', + maxValue: '10' + }); + field.isVisible = false; + expect(validator.validate(field)).toBe(true); + }); + + it('should skip validation for field in hidden group when opt-in is enabled', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.NUMBER, maxValue: '10', checkParentVisibilityForValidation: true }] } + } + ] + }; + const form = new FormModel(formJson); + form.enableParentVisibilityCheck = true; + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = '15'; + expect(validator.validate(field)).toBe(true); + }); + + it('should not skip validation for field in hidden group when opt-in is disabled (backward compatible)', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.NUMBER, maxValue: '10', checkParentVisibilityForValidation: false }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = '15'; + field.validationSummary = new ErrorMessageModel(); + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation when opt-in property defaults to false (backward compatible)', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.NUMBER, maxValue: '10' }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = '15'; + field.validationSummary = new ErrorMessageModel(); + expect(field.checkParentVisibilityForValidation).toBe(false); + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation for visible field with invalid value', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.NUMBER, + value: '15', + maxValue: '10' + }); + field.isVisible = true; + field.validationSummary = new ErrorMessageModel(); + expect(validator.validate(field)).toBe(false); + }); }); describe('RegExFieldValidator', () => { @@ -602,6 +1213,102 @@ describe('FormFieldValidator', () => { expect(validator.validate(field)).toBe(false); }); + + it('should skip validation for hidden field', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TEXT, + value: 'invalid', + regexPattern: '^valid$' + }); + field.isVisible = false; + expect(validator.validate(field)).toBe(true); + }); + + it('should skip validation for field in hidden group when opt-in is enabled', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { + 1: [{ id: 'field1', type: FormFieldTypes.TEXT, regexPattern: '^valid$', checkParentVisibilityForValidation: true }] + } + } + ] + }; + const form = new FormModel(formJson); + form.enableParentVisibilityCheck = true; + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = 'invalid'; + expect(validator.validate(field)).toBe(true); + }); + + it('should not skip validation for field in hidden group when opt-in is disabled (backward compatible)', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { + 1: [{ id: 'field1', type: FormFieldTypes.TEXT, regexPattern: '^valid$', checkParentVisibilityForValidation: false }] + } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = 'invalid'; + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation when opt-in property defaults to false (backward compatible)', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, regexPattern: '^valid$' }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = 'invalid'; + expect(field.checkParentVisibilityForValidation).toBe(false); + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation for visible field with invalid value', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TEXT, + value: 'invalid', + regexPattern: '^valid$' + }); + field.isVisible = true; + expect(validator.validate(field)).toBe(false); + }); }); describe('FixedValueFieldValidator', () => { @@ -659,6 +1366,139 @@ describe('FormFieldValidator', () => { expect(validator.validate(field)).toBe(false); }); + + it('should skip validation for hidden field', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TYPEAHEAD, + value: 'Invalid', + options: [ + { id: '1', name: 'Option 1' }, + { id: '2', name: 'Option 2' } + ] + }); + field.isVisible = false; + expect(validator.validate(field)).toBe(true); + }); + + it('should skip validation for field in hidden group when opt-in is enabled', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { + 1: [ + { + id: 'field1', + type: FormFieldTypes.TYPEAHEAD, + checkParentVisibilityForValidation: true, + options: [ + { id: '1', name: 'Option 1' }, + { id: '2', name: 'Option 2' } + ] + } + ] + } + } + ] + }; + const form = new FormModel(formJson); + form.enableParentVisibilityCheck = true; + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = 'Invalid'; + expect(validator.validate(field)).toBe(true); + }); + + it('should not skip validation for field in hidden group when opt-in is disabled (backward compatible)', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { + 1: [ + { + id: 'field1', + type: FormFieldTypes.TYPEAHEAD, + checkParentVisibilityForValidation: false, + options: [ + { id: '1', name: 'Option 1' }, + { id: '2', name: 'Option 2' } + ] + } + ] + } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = 'Invalid'; + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation when opt-in property defaults to false (backward compatible)', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { + 1: [ + { + id: 'field1', + type: FormFieldTypes.TYPEAHEAD, + options: [ + { id: '1', name: 'Option 1' }, + { id: '2', name: 'Option 2' } + ] + } + ] + } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = 'Invalid'; + expect(field.checkParentVisibilityForValidation).toBe(false); + expect(validator.validate(field)).toBe(false); + }); + + it('should not skip validation for visible field with invalid value', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TYPEAHEAD, + value: 'Invalid', + options: [ + { id: '1', name: 'Option 1' }, + { id: '2', name: 'Option 2' } + ] + }); + field.isVisible = true; + expect(validator.validate(field)).toBe(false); + }); }); describe('DecimalFieldValidator', () => { @@ -727,5 +1567,97 @@ describe('FormFieldValidator', () => { expect(decimalValidator.validate(field)).toBe(true); }); + + it('should skip validation for hidden field', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.DECIMAL, + value: '1.22', + precision: 1 + }); + field.isVisible = false; + expect(decimalValidator.validate(field)).toBe(true); + }); + + it('should skip validation for field in hidden group when opt-in is enabled', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.DECIMAL, precision: 1, checkParentVisibilityForValidation: true }] } + } + ] + }; + const form = new FormModel(formJson); + form.enableParentVisibilityCheck = true; + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = '1.22'; + expect(decimalValidator.validate(field)).toBe(true); + }); + + it('should not skip validation for field in hidden group when opt-in is disabled - backward compatible', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.DECIMAL, precision: 1, checkParentVisibilityForValidation: false }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = '1.22'; + expect(decimalValidator.validate(field)).toBe(false); + }); + + it('should not skip validation when opt-in property defaults to false - backward compatible', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.DECIMAL, precision: 1 }] } + } + ] + }; + const form = new FormModel(formJson); + const group = form.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = form.getFieldById('field1'); + field.isVisible = true; + field.value = '1.22'; + expect(field.checkParentVisibilityForValidation).toBe(false); + expect(decimalValidator.validate(field)).toBe(false); + }); + + it('should not skip validation for visible field with invalid value', () => { + const form = new FormModel(); + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.DECIMAL, + value: '1.22', + precision: 1 + }); + field.isVisible = true; + expect(decimalValidator.validate(field)).toBe(false); + }); }); }); diff --git a/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts b/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts index 97a399967f..7f26286f69 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts @@ -51,7 +51,7 @@ export class RequiredFieldValidator implements FormFieldValidator { } validate(field: FormFieldModel): boolean { - if (this.isSupported(field) && field.isVisible) { + if (this.isSupported(field) && field.form && !field.form.isFieldOrParentHidden(field)) { if (field.type === FormFieldTypes.RADIO_BUTTONS) { const option = field.options.find((opt) => opt.id === field.value); return !!option; @@ -89,7 +89,7 @@ export class NumberFieldValidator implements FormFieldValidator { } validate(field: FormFieldModel): boolean { - if (this.isSupported(field) && field.isVisible) { + if (this.isSupported(field) && field.form && !field.form.isFieldOrParentHidden(field)) { if (field.value === null || field.value === undefined || field.value === '') { return true; } @@ -116,7 +116,7 @@ export class MinLengthFieldValidator implements FormFieldValidator { } validate(field: FormFieldModel): boolean { - if (this.isSupported(field) && field.value && field.isVisible) { + if (this.isSupported(field) && field.value && field.form && !field.form.isFieldOrParentHidden(field)) { if (field.value.length >= field.minLength) { return true; } @@ -139,7 +139,7 @@ export class MaxLengthFieldValidator implements FormFieldValidator { } validate(field: FormFieldModel): boolean { - if (this.isSupported(field) && field.value && field.isVisible) { + if (this.isSupported(field) && field.value && field.form && !field.form.isFieldOrParentHidden(field)) { if (field.value.toString().length <= this.getMaxLength(field)) { return true; } @@ -166,7 +166,7 @@ export class MinValueFieldValidator implements FormFieldValidator { } validate(field: FormFieldModel): boolean { - if (this.isSupported(field) && field.value && field.isVisible) { + if (this.isSupported(field) && field.value && field.form && !field.form.isFieldOrParentHidden(field)) { const value: number = +field.value; const minValue: number = +field.minValue; @@ -190,7 +190,7 @@ export class MaxValueFieldValidator implements FormFieldValidator { } validate(field: FormFieldModel): boolean { - if (this.isSupported(field) && field.value && field.isVisible) { + if (this.isSupported(field) && field.value && field.form && !field.form.isFieldOrParentHidden(field)) { const value: number = +field.value; const maxValue: number = +field.maxValue; @@ -214,7 +214,7 @@ export class RegExFieldValidator implements FormFieldValidator { } validate(field: FormFieldModel): boolean { - if (this.isSupported(field) && field.value && field.isVisible) { + if (this.isSupported(field) && field.value && field.form && !field.form.isFieldOrParentHidden(field)) { if (field.value.length > 0 && field.value.match(new RegExp('^' + field.regexPattern + '$'))) { return true; } @@ -253,7 +253,7 @@ export class FixedValueFieldValidator implements FormFieldValidator { } validate(field: FormFieldModel): boolean { - if (this.isSupported(field) && field.isVisible) { + if (this.isSupported(field) && field.form && !field.form.isFieldOrParentHidden(field)) { if (this.hasStringValue(field) && this.hasOptions(field) && !this.hasValidNameOrValidId(field)) { field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_VALUE'; return false; @@ -271,7 +271,7 @@ export class DecimalFieldValidator implements FormFieldValidator { } validate(field: FormFieldModel): boolean { - const shouldValidateField = this.isSupported(field) && field.isVisible; + const shouldValidateField = this.isSupported(field) && field.form && !field.form.isFieldOrParentHidden(field); if (!shouldValidateField) { return true; diff --git a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts index e5a0e00362..1539d75c16 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts @@ -85,6 +85,7 @@ export class FormFieldModel extends FormWidgetModel { displayText: string; isVisible: boolean = true; visibilityCondition: WidgetVisibilityModel = null; + checkParentVisibilityForValidation: boolean = false; enableFractions: boolean = false; currency: string = null; dateDisplayFormat: string = this.defaultDateFormat; @@ -219,6 +220,7 @@ export class FormFieldModel extends FormWidgetModel { this.hyperlinkUrl = json.hyperlinkUrl; this.displayText = json.displayText; this.visibilityCondition = formFieldVisibilityConditionHandler.getVisibilityCondition(this.id, json.visibilityCondition, parent); + this.checkParentVisibilityForValidation = json.checkParentVisibilityForValidation ?? false; this.enableFractions = json.enableFractions; this.currency = json.currency; this.dateDisplayFormat = json.dateDisplayFormat || this.getDefaultDateFormat(json); diff --git a/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts b/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts index 5a01f50c85..0bc180995a 100644 --- a/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts +++ b/lib/core/src/lib/form/components/widgets/core/form.model.spec.ts @@ -807,4 +807,316 @@ describe('FormModel', () => { expect(allFields[3].id).toBe('text-in-section'); }); }); + + describe('FormModel - isFieldOrParentHidden', () => { + let form: FormModel; + + beforeEach(() => { + form = new FormModel(); + }); + + it('should return true for directly hidden field', () => { + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TEXT + }); + field.isVisible = false; + expect(form.isFieldOrParentHidden(field)).toBe(true); + }); + + it('should return false for visible field with no hidden parent', () => { + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TEXT + }); + field.isVisible = true; + expect(form.isFieldOrParentHidden(field)).toBe(false); + }); + + it('should return false for field in hidden group when opt-in is disabled - backward compatible', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true, checkParentVisibilityForValidation: false }] } + } + ] + }; + const testForm = new FormModel(formJson); + const group = testForm.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = testForm.getFieldById('field1'); + field.isVisible = true; + expect(testForm.isFieldOrParentHidden(field)).toBe(false); + }); + + it('should return true for field in hidden group when opt-in is enabled', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true, checkParentVisibilityForValidation: true }] } + } + ] + }; + const testForm = new FormModel(formJson); + testForm.enableParentVisibilityCheck = true; + const group = testForm.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = testForm.getFieldById('field1'); + field.isVisible = true; + expect(testForm.isFieldOrParentHidden(field)).toBe(true); + }); + + it('should return false for field in hidden section when opt-in is disabled - backward compatible', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'section1', + type: FormFieldTypes.SECTION, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true, checkParentVisibilityForValidation: false }] } + } + ] + }; + const testForm = new FormModel(formJson); + const container = testForm.fields[0] as ContainerModel; + container.field.isVisible = false; + const field = testForm.getFieldById('field1'); + field.isVisible = true; + expect(testForm.isFieldOrParentHidden(field)).toBe(false); + }); + + it('should return true for field in hidden section - opt-in is enabled', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'section1', + type: FormFieldTypes.SECTION, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true, checkParentVisibilityForValidation: true }] } + } + ] + }; + const testForm = new FormModel(formJson); + testForm.enableParentVisibilityCheck = true; + const container = testForm.fields[0] as ContainerModel; + container.field.isVisible = false; + const field = testForm.getFieldById('field1'); + field.isVisible = true; + expect(testForm.isFieldOrParentHidden(field)).toBe(true); + }); + + it('should return true for nested structure - section in group when opt-in is enabled', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { + 1: [ + { + id: 'section1', + type: FormFieldTypes.SECTION, + numberOfColumns: 1, + fields: { + 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true, checkParentVisibilityForValidation: true }] + } + } + ] + } + } + ] + }; + const testForm = new FormModel(formJson); + testForm.enableParentVisibilityCheck = true; + const group = testForm.fields[0] as ContainerModel; + group.field.isVisible = false; + const sectionField = testForm.getFieldById('section1'); + sectionField.isVisible = true; + const field = testForm.getFieldById('field1'); + field.isVisible = true; + expect(testForm.isFieldOrParentHidden(field)).toBe(true); + }); + + it('should return true for nested structure - section in section when opt-in is enabled', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'outerSection', + type: FormFieldTypes.SECTION, + numberOfColumns: 1, + fields: { + 1: [ + { + id: 'innerSection', + type: FormFieldTypes.SECTION, + numberOfColumns: 1, + fields: { + 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true, checkParentVisibilityForValidation: true }] + } + } + ] + } + } + ] + }; + const testForm = new FormModel(formJson); + testForm.enableParentVisibilityCheck = true; + const container = testForm.fields[0] as ContainerModel; + container.field.isVisible = false; + const innerSectionField = testForm.getFieldById('innerSection'); + innerSectionField.isVisible = true; + const field = testForm.getFieldById('field1'); + field.isVisible = true; + expect(testForm.isFieldOrParentHidden(field)).toBe(true); + }); + + it('should return false for visible field with visible parents - ContainerModel', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { + 1: [ + { + id: 'section1', + type: FormFieldTypes.SECTION, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true }] } + } + ] + } + } + ] + }; + const testForm = new FormModel(formJson); + const group = testForm.fields[0] as ContainerModel; + group.field.isVisible = true; + const sectionField = testForm.getFieldById('section1'); + sectionField.isVisible = true; + const field = testForm.getFieldById('field1'); + field.isVisible = true; + expect(testForm.isFieldOrParentHidden(field)).toBe(false); + }); + + it('should return false for field with no parent - root level', () => { + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TEXT + }); + field.isVisible = true; + form.fields = [field]; + expect(form.isFieldOrParentHidden(field)).toBe(false); + }); + + it('should return false for null field', () => { + expect(form.isFieldOrParentHidden(null as any)).toBe(false); + }); + + it('should return false for undefined field', () => { + expect(form.isFieldOrParentHidden(undefined as any)).toBe(false); + }); + + it('should return false for field with null form', () => { + const fieldWithoutForm = new FormFieldModel(null as any, { + id: 'field1', + type: FormFieldTypes.TEXT + }); + fieldWithoutForm.isVisible = true; + const testForm = new FormModel(); + expect(testForm.isFieldOrParentHidden(fieldWithoutForm)).toBe(false); + }); + + it('should return false for form with empty fields array', () => { + form.fields = []; + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TEXT + }); + field.isVisible = true; + expect(form.isFieldOrParentHidden(field)).toBe(false); + }); + + it('should use ID comparison to find parent field', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true, checkParentVisibilityForValidation: true }] } + } + ] + }; + const testForm = new FormModel(formJson); + testForm.enableParentVisibilityCheck = true; + const group = testForm.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = testForm.getFieldById('field1'); + field.isVisible = true; + const fieldWithSameId = new FormFieldModel(testForm, { + id: 'field1', + type: FormFieldTypes.TEXT, + checkParentVisibilityForValidation: true + }); + fieldWithSameId.isVisible = true; + expect(testForm.isFieldOrParentHidden(fieldWithSameId)).toBe(true); + }); + + it('should return false when opt-in property defaults to false - backward compatible', () => { + const formJson = { + id: 'test-form', + name: 'Test Form', + fields: [ + { + id: 'group1', + type: FormFieldTypes.GROUP, + numberOfColumns: 1, + fields: { 1: [{ id: 'field1', type: FormFieldTypes.TEXT, required: true }] } + } + ] + }; + const testForm = new FormModel(formJson); + const group = testForm.fields[0] as ContainerModel; + group.field.isVisible = false; + const field = testForm.getFieldById('field1'); + field.isVisible = true; + expect(field.checkParentVisibilityForValidation).toBe(false); + expect(testForm.isFieldOrParentHidden(field)).toBe(false); + }); + + it('should return true for directly hidden field - opt-in is enabled', () => { + const field = new FormFieldModel(form, { + id: 'field1', + type: FormFieldTypes.TEXT, + checkParentVisibilityForValidation: true + }); + field.isVisible = false; + expect(form.isFieldOrParentHidden(field)).toBe(true); + }); + }); }); diff --git a/lib/core/src/lib/form/components/widgets/core/form.model.ts b/lib/core/src/lib/form/components/widgets/core/form.model.ts index 009b4e5103..9fd0841353 100644 --- a/lib/core/src/lib/form/components/widgets/core/form.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form.model.ts @@ -19,6 +19,7 @@ import { FormFieldEvent } from '../../../events/form-field.event'; import { ValidateFormFieldEvent } from '../../../events/validate-form-field.event'; import { ValidateFormEvent } from '../../../events/validate-form.event'; import { ContainerModel } from './container.model'; +import { ContainerColumnModel } from './container-column.model'; import { FormFieldTypes } from './form-field-types'; import { FormFieldModel } from './form-field.model'; import { FormValues } from './form-values'; @@ -95,6 +96,7 @@ export class FormModel implements ProcessFormModel { isValid = true; processVariables: ProcessVariableModel[] = []; variables: FormVariableModel[] = []; + enableParentVisibilityCheck: boolean = false; constructor( json?: any, @@ -532,4 +534,126 @@ export class FormModel implements ProcessFormModel { private loadInjectedFieldValidators(injectedFieldValidators: FormFieldValidator[]): void { this.fieldValidators = injectedFieldValidators ? [...FORM_FIELD_VALIDATORS, ...injectedFieldValidators] : [...FORM_FIELD_VALIDATORS]; } + + /** + * Checks if a field or any of its parent containers/groups/sections is hidden. + * Returns true if the field should skip validation (field or parent is hidden). + * + * Parent visibility is only checked if: + * - `enableParentVisibilityCheck` is true + * - `field.checkParentVisibilityForValidation` is true (field opt-in enabled) + * + * @param field The form field to check + * @returns true if field or parent is hidden, false otherwise + */ + isFieldOrParentHidden(field: FormFieldModel): boolean { + if (!field) { + return false; + } + + if (!field.isVisible) { + return true; + } + + if (this.enableParentVisibilityCheck && field.checkParentVisibilityForValidation) { + return this.hasHiddenParent(field); + } + + return false; + } + + /** + * Checks if the given field has a hidden parent container/group/section. + * + * @param targetField The form field to check + * @returns true if field has a hidden parent, false otherwise + */ + private hasHiddenParent(targetField: FormFieldModel): boolean { + if (!targetField || !this.fields || this.fields.length === 0) { + return false; + } + + for (const rootElement of this.fields) { + const parent = this.findParentInElement(rootElement, targetField); + if (parent && !parent.isVisible) { + return true; + } + } + + return false; + } + + /** + * Recursively searches for a field within an element (container/group/section). + * Returns the parent element if field is found within it, null otherwise. + * + * @param element The container/group/section to search in + * @param targetField The form field to find + * @returns Parent element if field found, null otherwise + */ + private findParentInElement(element: ContainerModel | FormFieldModel, targetField: FormFieldModel): ContainerModel | FormFieldModel | null { + if (!element || !targetField) { + return null; + } + + const columns = this.getColumnsFromElement(element); + if (!columns || columns.length === 0) { + return null; + } + + return this.searchFieldsInColumns(columns, element, targetField); + } + + private getColumnsFromElement(element: ContainerModel | FormFieldModel): ContainerColumnModel[] | null { + if (element instanceof ContainerModel) { + return element.field?.columns || null; + } else if (element instanceof FormFieldModel && element.type === FormFieldTypes.SECTION) { + return element.columns || null; + } + return null; + } + + private searchFieldsInColumns( + columns: ContainerColumnModel[], + parentElement: ContainerModel | FormFieldModel, + targetField: FormFieldModel + ): ContainerModel | FormFieldModel | null { + for (const column of columns) { + if (!column?.fields || column.fields.length === 0) { + continue; + } + + const result = this.searchFieldsInColumn(column.fields, parentElement, targetField); + if (result) { + return result; + } + } + + return null; + } + + private searchFieldsInColumn( + fields: FormFieldModel[], + parentElement: ContainerModel | FormFieldModel, + targetField: FormFieldModel + ): ContainerModel | FormFieldModel | null { + for (const field of fields) { + if (!field) { + continue; + } + + if (field.id === targetField.id) { + return parentElement; + } + + if (field.type === FormFieldTypes.SECTION) { + const nestedParent = this.findParentInElement(field, targetField); + if (nestedParent) { + return !parentElement.isVisible ? parentElement : nestedParent; + } + } + } + + return null; + } }