Skip to content

Commit

Permalink
Merge pull request #88 from formio/FIO-8264-fix-falsy-validation
Browse files Browse the repository at this point in the history
FIO-8264: update validate required
  • Loading branch information
AlexeyNikipelau authored Apr 25, 2024
2 parents 4c92769 + ef597b5 commit 7ada46f
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 23 deletions.
10 changes: 9 additions & 1 deletion src/process/validation/rules/__tests__/fixtures/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,14 @@ export const simpleRadioField: RadioComponent = {
input: true,
};

export const simpleCheckBoxField = {
label: 'Checkbox',
tableView: true,
key: 'component',
type: 'checkbox',
input: true,
};

export const hiddenRequiredField: HiddenComponent = {
type: 'hidden',
key: 'someData',
Expand Down Expand Up @@ -234,4 +242,4 @@ export const requiredNonInputField: any = {
validate: {
required: true
}
};
};
77 changes: 74 additions & 3 deletions src/process/validation/rules/__tests__/validateRequired.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ import { expect } from 'chai';

import { FieldError } from 'error';
import { validateRequired } from '../validateRequired';
import { conditionallyHiddenRequiredHiddenField, hiddenRequiredField, requiredNonInputField, simpleTextField } from './fixtures/components';
import {
conditionallyHiddenRequiredHiddenField,
hiddenRequiredField,
requiredNonInputField,
simpleTextField,
simpleSelectBoxes,
simpleRadioField,
simpleCheckBoxField,
} from './fixtures/components';
import { processOne } from 'processes/processOne';
import { generateProcessorContext } from './fixtures/util';
import { ProcessorsContext, ValidationScope } from 'types';
import { ProcessorsContext, SelectBoxesComponent, ValidationScope } from 'types';
import { validateAllProcess, validateProcessInfo } from 'processes/validation';

it('Validating a simple component that is required and not present in the data will return a field error', async () => {
Expand Down Expand Up @@ -116,7 +124,7 @@ it('Should not validate a non input comonent', async () => {
expect(context.scope.errors.length).to.equal(0);
});

it('Should validate a conditionally hidden compoentn with validateWhenHidden flag set to true', async () => {
it('Should validate a conditionally hidden component with validateWhenHidden flag set to true', async () => {
const component = {...simpleTextField};
component.validate = { required: true };
component.validateWhenHidden = true;
Expand All @@ -132,3 +140,66 @@ it('Should validate a conditionally hidden compoentn with validateWhenHidden fla
expect(context.scope.errors.length).to.equal(1);
expect(context.scope.errors[0] && context.scope.errors[0].errorKeyOrMessage).to.equal('required');
});

it('Validating a simple radio component that is required and present in the data with value set to false will return null', async () => {
const component = { ...simpleRadioField, validate: { required: true }, values: [
{
label: 'Yes',
value: 'true',
},
{
label: 'No',
value: 'false',
}] };
const data = { component: false };
const context = generateProcessorContext(component, data);
const result = await validateRequired(context);
expect(result).to.equal(null);
});


it('Validating a simple selectbox that is required and present in the data with value set to 0 will return null', async () => {
const component = { ...simpleSelectBoxes, validate: { required: true }, values: [
{
label: 'true',
value: 'true',
},
{
label: 'Null',
value: '0',
}] };
const data = { component: 0 };
const context = generateProcessorContext(component, data);
const result = await validateRequired(context);
expect(result).to.equal(null);
});

it('Validating a simple selectbox that is required and present in the data with value set to false will return a FieldError', async () => {
const component: SelectBoxesComponent = { ...simpleSelectBoxes, validate: { required: true }, values: [
{
label: 'true',
value: 'true',
},
{
label: 'false',
value: 'false',
}]
};
const data = {
component: {
true: false,
false: false
}
};
const context = generateProcessorContext(component, data);
const result = await validateRequired(context);
expect(result).to.be.instanceOf(FieldError);
});

it('Validating a simple checkbox that is required and present in the data with value set to false will return a FieldError', async () => {
const component = { ...simpleCheckBoxField, validate: { required: true } };
const data = { component: false };
const context = generateProcessorContext(component, data);
const result = await validateRequired(context);
expect(result).to.be.instanceOf(FieldError);
});
65 changes: 49 additions & 16 deletions src/process/validation/rules/validateRequired.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,57 @@
import { FieldError } from 'error';
import { AddressComponentDataObject, RuleFn, RuleFnSync, ValidationContext } from 'types';
import {
AddressComponentDataObject,
RuleFn,
RuleFnSync,
ValidationContext,
AddressComponent,
DayComponent
} from 'types';
import { isEmptyObject } from '../util';
import { ProcessorInfo } from 'types/process/ProcessorInfo';

const isAddressComponent = (component: any): component is AddressComponent => {
return component.type === 'address';
}

const isDayComponent = (component: any): component is DayComponent => {
return component.type === 'day';
}

const isAddressComponentDataObject = (value: any): value is AddressComponentDataObject => {
return value !== null && typeof value === 'object' && value.mode && value.address && typeof value.address === 'object';
}

// Checkboxes and selectboxes consider false to be falsy, whereas other components with
// settable values (e.g. radio, select, datamap, container, etc.) consider it truthy
const isComponentThatCannotHaveFalseValue = (component: any): boolean => {
return component.type === 'checkbox' || component.type === 'selectboxes'
}

const valueIsPresent = (value: any, considerFalseTruthy: boolean): boolean => {
// Evaluate for 3 out of 6 falsy values ("", null, undefined), don't check for 0
// and only check for false under certain conditions
if (value === null || value === undefined || value === "" || (!considerFalseTruthy && value === false)) {
return false;
}
// Evaluate for empty object
else if (isEmptyObject(value)) {
return false;
}
// Evaluate for empty array
else if (Array.isArray(value) && value.length === 0) {
return false;
}
// Recursively evaluate
else if (typeof value === 'object') {
return Object.values(value).some((val) => valueIsPresent(val, considerFalseTruthy));
}
return true;
}

export const shouldValidate = (context: ValidationContext) => {
const { component } = context;
if (component.validate?.required) {
if (component.validate?.required && !component.hidden) {
return true;
}
return false;
Expand All @@ -25,25 +67,16 @@ export const validateRequiredSync: RuleFnSync = (context: ValidationContext) =>
if (!shouldValidate(context)) {
return null;
}
if (
(value === null || value === undefined || isEmptyObject(value) || (!!value === false && value !== 0)) &&
!component.hidden
) {
return error;
}
else if (Array.isArray(value) && value.length === 0) {
return error;
}
else if (component.type === 'address' && isAddressComponentDataObject(value)) {
if (isAddressComponent(component) && isAddressComponentDataObject(value)) {
return isEmptyObject(value.address) ? error : Object.values(value.address).every((val) => !!val) ? null : error;
}
else if (component.type === 'day' && value === '00/00/0000') {
else if (isDayComponent(component) && value === '00/00/0000') {
return error;
}
else if (typeof value === 'object' && value !== null) {
return Object.values(value).some((val) => !!val) ? null : error;
else if (isComponentThatCannotHaveFalseValue(component)) {
return !valueIsPresent(value, false) ? error : null;
}
return null;
return !valueIsPresent(value, true) ? error : null;
};

export const validateRequiredInfo: ProcessorInfo<ValidationContext, FieldError | null> = {
Expand Down
20 changes: 17 additions & 3 deletions src/types/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,16 +328,30 @@ export type ListComponent = BaseComponent & {
valueProperty?: string;
};

export type RadioComponent = ListComponent & {
type StaticValuesRadioComponent = ListComponent & {
values: { label: string; value: string; shortcut?: string }[];
data?: {
url?: string;
dataSrc?: "values";
fieldSet?: boolean;
optionsLabelPosition?: string;
inline?: boolean;
};

type UrlValuesRadioComponent = ListComponent & {
data: {
url: string;
headers: {
key: string;
value: string;
}[];
};
dataSrc: 'url';
fieldSet?: boolean;
optionsLabelPosition?: string;
inline?: boolean;
};

export type RadioComponent = StaticValuesRadioComponent | UrlValuesRadioComponent;

export type RecaptchaComponent = BaseComponent;

type StaticValuesSelectData = {
Expand Down

0 comments on commit 7ada46f

Please sign in to comment.