skip to Main Content

I have a form that has two input fields that are mutually dependent on each other. They can both be blank but if one is filled out, the other has to be filled out. They must also both be numbers from 100-999.

When a user fills out one field, I would like an error message to show on the other field indicating that it must be filled out.

When a user deletes their input from one input and the other input is also empty, I would like any error messages on the other field to be removed as well.

const ERROR_MESSAGE = 'Enter a number between 100 and 999'

const field1Validation = yup.string().when('field2', val => {
    if (val?.[0]?.length > 0) {
        return yup.string().required(ERROR_MESSAGE).test('validityTest', ERROR_MESSAGE, val =>
            !isNaN(Number(val)) && Number(val) >= 100 && Number(val) <= 999
        )
    }
    return yup.string().notRequired()
})

const field2Validation = yup.string().when('field1', val => {
    if (val?.[0]?.length > 0) {
        yup.string().required(ERROR_MESSAGE).test('validityTest', ERROR_MESSAGE, val =>
            !isNaN(Number(val)) && Number(val) >= 100 && Number(val) <= 999
        )
    }
    return yup.string().notRequired()
})

const validationSchema = yup.object().shape({
    field1: field1Validation,
    field2: field2Validation
}, [['field1', 'field2'], ['field2', 'field1' ]]) // I don't quite know how this cyclic dependency thing here works

export const MyForm = () => {
    const formMethods = useForm({
        resolver: useYupValidationResolver(validationSchema),
        mode: 'onTouched'
    })
    const { handleSubmit, register, formState } = formMethods
    const { errors } = formState

    const onSubmit = submitData => { console.log(submitData) }

    return (
        <FormProvider {...formMethods}>
            <form onSubmit={handleSubmit(onSubmit)}>
                <input type="text" {...register('field1')}
                <span>{errors['field1'].message}</span>

                <input type="text" {...register('field2')}
                <span>{errors['field2'].message}</span>
            </form>
        </FormProvider>
    )
}

My sample code is above. What I’m seeing happen is that when I type a valid input into the first field, no error is shown on the second field saying that it’s now required. Then, when I delete the input from the first field, the error message that was on the second field does not go away.

Any help would be greatly appreciated.

2

Answers


  1. To address the issues you mentioned, you can modify your validation logic and form structure. Here’s a revised version of your code:

    import { useForm, FormProvider } from 'react-hook-form';
    import { yupResolver } from '@hookform/resolvers/yup';
    import * as yup from 'yup';
    
    const ERROR_MESSAGE = 'Enter a number between 100 and 999';
    
    const validationSchema = yup.object().shape({
        field1: yup.string().when('field2', {
            is: (val) => val?.length > 0,
            then: yup.string().required(ERROR_MESSAGE).test('validityTest', ERROR_MESSAGE, (val) =>
                !isNaN(Number(val)) && Number(val) >= 100 && Number(val) <= 999
            ),
            otherwise: yup.string().notRequired()
        }),
        field2: yup.string().when('field1', {
            is: (val) => val?.length > 0,
            then: yup.string().required(ERROR_MESSAGE).test('validityTest', ERROR_MESSAGE, (val) =>
                !isNaN(Number(val)) && Number(val) >= 100 && Number(val) <= 999
            ),
            otherwise: yup.string().notRequired()
        })
    });
    
    export const MyForm = () => {
        const methods = useForm({
            resolver: yupResolver(validationSchema)
        });
        const { handleSubmit, register, formState: { errors, dirtyFields } } = methods;
    
        const onSubmit = (data) => {
            console.log(data);
        };
    
        return (
            <FormProvider {...methods}>
                <form onSubmit={handleSubmit(onSubmit)}>
                    <input type="text" {...register('field1')} />
                    {errors.field1 && <span>{errors.field1.message}</span>}
                    {errors.field2 && !dirtyFields.field1 && <span>{errors.field2.message}</span>}
    
                    <input type="text" {...register('field2')} />
                    {errors.field2 && <span>{errors.field2.message}</span>}
                    {errors.field1 && !dirtyFields.field2 && <span>{errors.field1.message}</span>}
    
                    <button type="submit">Submit</button>
                </form>
            </FormProvider>
        );
    };
    
    Login or Signup to reply.
  2. Here is the working code:

    import "./App.css";
    import React, { useEffect, useMemo, useRef } from "react";
    import { useForm } from "react-hook-form";
    import * as yup from "yup";
    import useYupValidationResolver from "./useYupResolver";
    
    function App() {
        const isInitialLoadingRef = useRef(true);
        const ERROR_MESSAGE = "Enter a number between 100 and 999";
    
        const field1Validation = yup.string().when("field2", (val) => {
            if (val?.[0]?.length > 0) {
                return yup
                    .string()
                    .required(ERROR_MESSAGE)
                    .test(
                        "validityTest",
                        ERROR_MESSAGE,
                        (val) =>
                            !isNaN(Number(val)) &&
                            Number(val) >= 100 &&
                            Number(val) <= 999
                    );
            }
            return yup.string().notRequired();
        });
    
        const field2Validation = yup.string().when("field1", (val) => {
            if (val?.[0]?.length > 0) {
                return yup
                    .string()
                    .required(ERROR_MESSAGE)
                    .test(
                        "validityTest",
                        ERROR_MESSAGE,
                        (val) =>
                            !isNaN(Number(val)) &&
                            Number(val) >= 100 &&
                            Number(val) <= 999
                    );
            }
            return yup.string().notRequired();
        });
    
        const validationSchema = useMemo(
            () =>
                yup.object().shape(
                    {
                        field1: field1Validation,
                        field2: field2Validation,
                        field3: yup.string().required("Fields is required!"),
                    },
                    ["field2", "field1"]
                ),
            []
        );
    
        const formMethods = useForm({
            resolver: useYupValidationResolver(validationSchema),
        });
    
        const { handleSubmit, register, formState, watch, trigger } = formMethods;
        const { errors } = formState;
    
        useEffect(() => {
            if (isInitialLoadingRef.current) {
                isInitialLoadingRef.current = false;
                return;
            }
            trigger();
        }, [watch("field1"), watch("field2"), watch("field3")]);
    
        const onSubmit = (submitData) => {
            console.log(submitData);
        };
    
        return (
            <form onSubmit={handleSubmit(onSubmit)}>
                <input type="text" {...register("field1")} />
                <span>{errors["field1"]?.message}</span>
    
                <input type="text" {...register("field2")} />
                <span>{errors["field2"]?.message}</span>
    
                <input type="text" {...register("field3")} />
                <span>{errors["field3"]?.message}</span>
    
                <button type="submit">Submit</button>
            </form>
        );
    }
    
    export default App;
    

    Explanation:

    First I assume that useYupValidationResolver from the OP code is the hook provided in the official documentation in Yup. After that I want to point to an article in the official documentation – Modes explained. I can see in the OP code there is mode onTouched, which validate the specific input field after its blur event. I tried to use onChange, but it works only for 1 input and validates only himself after its change event. I took a deeper look and found that – trigger function. This function triggers validation for the fields according an array or to all fields, it no value is passed. I wrapped that function in useEffect with dependency array of the values of the field. I added 1 additional field for testing purpose. I added one reference for the initial loading of the component, since without it, the validation will be executed on mounting of the component and will show the validation errors, which I do not think is a good UX. I changed the array for the validation schema – this is enough to work. And removed the FormProvider, since it is not necessary in that case.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search