Skip to content

Commit

Permalink
Fix error when validating the form with non blocking validations (#10…
Browse files Browse the repository at this point in the history
…3629)

* Fix error when validating the form with non blocking validations

issue: #102338

* Add a flag to only validate blocking validation when validating the form

* Refactor bypass validation

Co-authored-by: Sébastien Loix <sabee77@gmail.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people committed Jul 13, 2021
1 parent 9ccbbef commit 516480e
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { act } from 'react-dom/test-utils';
import { registerTestBed } from '../shared_imports';

import { Form, UseField } from '../components';
import React from 'react';
import { useForm } from '.';
import { emptyField } from '../../helpers/field_validators';
import { FieldHook, FieldValidateResponse, VALIDATION_TYPES } from '..';

describe('useField() hook', () => {
describe('field.validate()', () => {
const EMPTY_VALUE = ' ';

test('It should not invalidate a field with arrayItem validation when isBlocking is false', async () => {
let fieldHook: FieldHook;

const TestField = ({ field }: { field: FieldHook }) => {
fieldHook = field;
return null;
};

const TestForm = () => {
const { form } = useForm();

return (
<Form form={form}>
<UseField
path="test-path"
component={TestField}
config={{
validations: [
{
validator: emptyField('error-message'),
type: VALIDATION_TYPES.ARRAY_ITEM,
isBlocking: false,
},
],
}}
/>
</Form>
);
};

registerTestBed(TestForm)();

let validateResponse: FieldValidateResponse;

await act(async () => {
validateResponse = await fieldHook!.validate({
value: EMPTY_VALUE,
validationType: VALIDATION_TYPES.ARRAY_ITEM,
});
});

// validation fails for ARRAY_ITEM with a non-blocking validation error
expect(validateResponse!).toEqual({
isValid: false,
errors: [
{
code: 'ERR_FIELD_MISSING',
path: 'test-path',
message: 'error-message',
__isBlocking__: false,
validationType: 'arrayItem',
},
],
});

// expect the field to be valid because the validation error is non-blocking
expect(fieldHook!.isValid).toBe(true);
});

test('It should invalidate an arrayItem field when isBlocking is true', async () => {
let fieldHook: FieldHook;

const TestField = ({ field }: { field: FieldHook }) => {
fieldHook = field;
return null;
};

const TestForm = () => {
const { form } = useForm();

return (
<Form form={form}>
<UseField
path="test-path"
component={TestField}
config={{
validations: [
{
validator: emptyField('error-message'),
type: VALIDATION_TYPES.ARRAY_ITEM,
isBlocking: true,
},
],
}}
/>
</Form>
);
};

registerTestBed(TestForm)();

let validateResponse: FieldValidateResponse;

await act(async () => {
validateResponse = await fieldHook!.validate({
value: EMPTY_VALUE,
validationType: VALIDATION_TYPES.ARRAY_ITEM,
});
});

// validation fails for ARRAY_ITEM with a blocking validation error
expect(validateResponse!).toEqual({
isValid: false,
errors: [
{
code: 'ERR_FIELD_MISSING',
path: 'test-path',
message: 'error-message',
__isBlocking__: true,
validationType: 'arrayItem',
},
],
});

// expect the field to be invalid because the validation error is blocking
expect(fieldHook!.isValid).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
FieldValidateResponse,
ValidationError,
FormData,
ValidationConfig,
} from '../types';
import { FIELD_TYPES, VALIDATION_TYPES } from '../constants';

Expand Down Expand Up @@ -189,10 +190,12 @@ export const useField = <T, FormType = FormData, I = T>(
{
formData,
value: valueToValidate,
onlyBlocking: runOnlyBlockingValidations,
validationTypeToValidate,
}: {
formData: any;
value: I;
onlyBlocking: boolean;
validationTypeToValidate?: string;
},
clearFieldErrors: FieldHook['clearErrors']
Expand All @@ -203,10 +206,31 @@ export const useField = <T, FormType = FormData, I = T>(

// By default, for fields that have an asynchronous validation
// we will clear the errors as soon as the field value changes.
clearFieldErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]);
clearFieldErrors([
validationTypeToValidate ?? VALIDATION_TYPES.FIELD,
VALIDATION_TYPES.ASYNC,
]);

cancelInflightValidation();

const doByPassValidation = ({
type: validationType,
isBlocking,
}: ValidationConfig<FormType, string, I>) => {
if (
typeof validationTypeToValidate !== 'undefined' &&
validationType !== validationTypeToValidate
) {
return true;
}

if (runOnlyBlockingValidations && isBlocking === false) {
return true;
}

return false;
};

const runAsync = async () => {
const validationErrors: ValidationError[] = [];

Expand All @@ -219,10 +243,7 @@ export const useField = <T, FormType = FormData, I = T>(
type: validationType = VALIDATION_TYPES.FIELD,
} = validation;

if (
typeof validationTypeToValidate !== 'undefined' &&
validationType !== validationTypeToValidate
) {
if (doByPassValidation(validation)) {
continue;
}

Expand Down Expand Up @@ -265,10 +286,7 @@ export const useField = <T, FormType = FormData, I = T>(
type: validationType = VALIDATION_TYPES.FIELD,
} = validation;

if (
typeof validationTypeToValidate !== 'undefined' &&
validationType !== validationTypeToValidate
) {
if (doByPassValidation(validation)) {
continue;
}

Expand Down Expand Up @@ -344,6 +362,7 @@ export const useField = <T, FormType = FormData, I = T>(
formData = __getFormData$().value,
value: valueToValidate = value,
validationType,
onlyBlocking = false,
} = validationData;

setIsValidated(true);
Expand Down Expand Up @@ -377,6 +396,7 @@ export const useField = <T, FormType = FormData, I = T>(
formData,
value: valueToValidate,
validationTypeToValidate: validationType,
onlyBlocking,
},
clearErrors
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ import React, { useEffect } from 'react';
import { act } from 'react-dom/test-utils';

import { registerTestBed, getRandomString, TestBed } from '../shared_imports';

import { emptyField } from '../../helpers/field_validators';
import { Form, UseField } from '../components';
import {
FormSubmitHandler,
OnUpdateHandler,
FormHook,
ValidationFunc,
FieldConfig,
} from '../types';
VALIDATION_TYPES,
} from '..';
import { useForm } from './use_form';

interface MyForm {
Expand Down Expand Up @@ -501,4 +502,74 @@ describe('useForm() hook', () => {
expect(isValid).toBeUndefined(); // Make sure it is "undefined" and not "false".
});
});

describe('form.validate()', () => {
test('should not invalidate a field with arrayItem validation when validating a form', async () => {
const TestComp = () => {
const { form } = useForm();
formHook = form;

return (
<Form form={form}>
<UseField
path="test-path"
config={{
validations: [
{
validator: emptyField('error-message'),
type: VALIDATION_TYPES.ARRAY_ITEM,
isBlocking: false,
},
],
}}
/>
</Form>
);
};

registerTestBed(TestComp)();

let isValid: boolean = false;

await act(async () => {
isValid = await formHook!.validate();
});

expect(isValid).toBe(true);
});

test('should invalidate a field with a blocking arrayItem validation when validating a form', async () => {
const TestComp = () => {
const { form } = useForm();
formHook = form;

return (
<Form form={form}>
<UseField
path="test-path"
config={{
validations: [
{
validator: emptyField('error-message'),
type: VALIDATION_TYPES.ARRAY_ITEM,
isBlocking: true,
},
],
}}
/>
</Form>
);
};

registerTestBed(TestComp)();

let isValid: boolean = false;

await act(async () => {
isValid = await formHook!.validate();
});

expect(isValid).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,14 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
}, [fieldsToArray]);

const validateFields: FormHook<T, I>['__validateFields'] = useCallback(
async (fieldNames) => {
async (fieldNames, onlyBlocking = false) => {
const fieldsToValidate = fieldNames
.map((name) => fieldsRefs.current[name])
.filter((field) => field !== undefined);

const formData = getFormData$().value;
const validationResult = await Promise.all(
fieldsToValidate.map((field) => field.validate({ formData }))
fieldsToValidate.map((field) => field.validate({ formData, onlyBlocking }))
);

if (isMounted.current === false) {
Expand Down Expand Up @@ -315,7 +315,8 @@ export function useForm<T extends FormData = FormData, I extends FormData = T>(
if (fieldsToValidate.length === 0) {
isFormValid = fieldsArray.every(isFieldValid);
} else {
({ isFormValid } = await validateFields(fieldsToValidate.map((field) => field.path)));
const fieldPathsToValidate = fieldsToValidate.map((field) => field.path);
({ isFormValid } = await validateFields(fieldPathsToValidate, true));
}

setIsValid(isFormValid);
Expand Down
5 changes: 4 additions & 1 deletion src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ export interface FormHook<T extends FormData = FormData, I extends FormData = T>
__addField: (field: FieldHook) => void;
__removeField: (fieldNames: string | string[]) => void;
__validateFields: (
fieldNames: string[]
fieldNames: string[],
/** Run only blocking validations */
onlyBlocking?: boolean
) => Promise<{ areFieldsValid: boolean; isFormValid: boolean | undefined }>;
__updateFormDataAt: (field: string, value: unknown) => void;
__updateDefaultValueAt: (field: string, value: unknown) => void;
Expand Down Expand Up @@ -137,6 +139,7 @@ export interface FieldHook<T = unknown, I = T> {
formData?: any;
value?: I;
validationType?: string;
onlyBlocking?: boolean;
}) => FieldValidateResponse | Promise<FieldValidateResponse>;
reset: (options?: { resetValue?: boolean; defaultValue?: T }) => unknown | undefined;
// Flag to indicate if the field value will be included in the form data outputted
Expand Down

0 comments on commit 516480e

Please sign in to comment.