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
This commit is contained in:
Alex Molodyh
2025-12-02 08:30:23 -08:00
committed by GitHub
parent 3287263794
commit 40df22b576
7 changed files with 1383 additions and 9 deletions
+1
View File
@@ -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 |
+3
View File
@@ -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.
@@ -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);
});
});
});
@@ -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;
@@ -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);
@@ -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);
});
});
});
@@ -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;
}
}