Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to conditionally validate at least one of n values are set? #176

Closed
nandastone opened this issue Feb 20, 2018 · 31 comments
Closed

How to conditionally validate at least one of n values are set? #176

nandastone opened this issue Feb 20, 2018 · 31 comments

Comments

@nandastone
Copy link

nandastone commented Feb 20, 2018

I have 5 form fields, at last one of which must be populated for validation to succeed. I attempted this, but soon ran into cyclic reference issues:

tag_business: yup.string().when(['tag_cost_centre', 'tag_project_code', 'tag_internal_order', 'tag_business'], {
  is: (tag_cost_centre, tag_project_code, tag_internal_order, tag_business) => {
    return !(tag_cost_centre, tag_project_code, tag_internal_order, tag_business)
  },
  then: yup.string().required()
})

What would be the best way to implement this?

@jquense
Copy link
Owner

jquense commented Feb 20, 2018

That's right but don't include the field itself there. That's why you are getting cycles, your telling yup the field depends on itself, which doesn't make sense

@nandastone nandastone changed the title How to conditionally validate at least one of x values are set? How to conditionally validate at least one of n values are set? Feb 21, 2018
@nandastone
Copy link
Author

That makes sense, however, I have fixed the issue and am still getting cyclic errors. Here's a reduced test case:

https://runkit.com/nandastone/5a8ceae6d80f770012eb7270

Is yup running validation for sibling fields when they are referenced in when()?

@jquense
Copy link
Owner

jquense commented Feb 21, 2018

Yeah this is a bit of a gotcha. Fields that depend on each other need to sorted so they are "constructed" in the right order, e.g. if depend on field A in field B, yup needs to cast and coerce the value in field A before it's handed to B. What's happening here tho is you are only adding a validation in the condition, so there isn't actually any need to order anything validation happens after everything is constructed already. because of the flexibility and programmatic nature of yup it can't distinguish between those two cases.

There is an ugly escape hatch: using the shape() method directly that tells yup to ignore the ordering for a pair of edges: https://runkit.com/5908cecfe97ebf0012f2e3c9/5a8d87d03a470f00122666d8

@nandastone
Copy link
Author

I don't quite understand the low-level reason you've explained, but appreciate you taking the time to help.

Your example worked well, until I extended the validation to depend on more than one other field:

https://runkit.com/nandastone/5a8e0bf15ae96a0012e235a7

Thoughts?

@jquense
Copy link
Owner

jquense commented Mar 2, 2018

var schema = yup.object().shape({
  a: yup.string().when(['b', 'c'], {
    is: (b, c) => !b && !c,
    then: yup.string().required()
  }),
  b: yup.string().when(['a', 'c'], {
    is: (a, c) => !a && !c,
    then: yup.string().required()
  }),
  c: yup.string().when(['a', 'b'], {
    is: (a, b) => !a && !b,
    then: yup.string().required()
  })
}, [['a', 'b'], ['a', 'c'], ['b','c']]) // <-- HERE!!!!!!!!

@pmonty
Copy link

pmonty commented Jul 11, 2018

Has there been a resolution to this? I am experiencing the same thing. I upgraded the code a bit in one of the links to this and get cyclic dependency issue

var schema = yup.object().shape({
  a: yup.string().when(['b', 'c', 'd'], {
    is: (b, c, d) => !b && !c && !d,
    then: yup.string().required()
  }),
  b: yup.string().when(['a', 'c', 'd'], {
    is: (a, c, d) => !a && !c && !d,
    then: yup.string().required()
  }),
  c: yup.string().when(['a', 'b', 'd'], {
    is: (a, b, d) => !a && !b && !d,
    then: yup.string().required()
  }),
  d: yup.string().when(['a', 'b', 'c'], {
    is: (a, b, c) => !a && !b && !c,
    then: yup.string().required()
  })
}, [['a', 'b', 'c'], ['b', 'c', 'd'], ['a','c', 'd'], ['a','b','d']]) // <-- HERE!!!!!!!!

@matthew-socialcore
Copy link

matthew-socialcore commented Nov 29, 2018

Hi @pmonty the issue in your last snippet I believe is because you aren't properly enumerating the list of 'edges' in your validation schema.

Pretty much in the array of combinations at the end of the .shape(...) call you need to list all the pair-wise (2-tuples) combinations - in your example you have triples (i.e. ['a','b','c']).
Because you have 4 fields, you'd expect to have 6 pairs in your array.

Anyway, I did this and it worked fine for me.

@jmpolitzer
Copy link

jmpolitzer commented Jan 3, 2019

I have an array of objects, and for each object, I have dependent fields. My code:

const schema = Yup.object().shape({
  colleagues: Yup.array().of(
    Yup.object().shape({
      name: Yup.string().when('role', {
        is: role => role.length > 0,
        then: Yup.string().required('Colleague name is required.')
      }),
      role: Yup.string().when('name', {
        is: name => name.length > 0,
        then: Yup.string().required('Colleague role is required.')
      })
    }, ['name', 'role'])
  ),
  email: Yup.string()
    .email('Email is not valid.')
    .required('Email is required.'),
});

When I remove the when validation on name or role, the validation works perfectly, and I receive errors for all fields. However, when I include both, and enumerate the dependencies of the array objects, I don't receive errors for anything, including the top-level email field. Since these are objects inside of an array, am I specifying the paths incorrectly?

The solutions here seem straightforward enough, so I'm sure I'm missing something small. Any help would be much appreciated.

@jmpolitzer
Copy link

It turns out that role and name were coming back as undefined, so the validation never got to the conditional check of the object in the array. Changing the conditional check to role && role.length > 0 seems to have resolved this issue.

@sporteman
Copy link

@jmpolitzer Thanks, it saved me!

@loolooii
Copy link

loolooii commented Jun 3, 2019

var schema = yup.object().shape({
  a: yup.string().when(['b', 'c'], {
    is: (b, c) => !b && !c,
    then: yup.string().required()
  }),
  b: yup.string().when(['a', 'c'], {
    is: (a, c) => !a && !c,
    then: yup.string().required()
  }),
  c: yup.string().when(['a', 'b'], {
    is: (a, b) => !a && !b,
    then: yup.string().required()
  })
}, [['a', 'b'], ['a', 'c'], ['b','c']]) // <-- HERE!!!!!!!!

This does not work anymore! Turns out that is function now only accepts 1 param. So now I can validate at most 2 fields of which 1 is required.

So the type of is function is now boolean | ((value: any) => boolean). Any reason why this has changed? @jquense

@loolooii
Copy link

loolooii commented Jun 3, 2019

For now I got it work by changing the type to boolean | ((...value: any) => boolean). Any chance this will be fixed in a coming release?

@dguo
Copy link

dguo commented Jun 10, 2019

@loolooii, I ran into the same TypeScript issue, and it looks like you just have to upgrade @types/yup to v0.26.14 or later. The issue was fixed in DefinitelyTyped/DefinitelyTyped#35885.

@rmrotek
Copy link

rmrotek commented Sep 25, 2019

instead of using examples above, i just created a 'ghost' field in yup validation schema that checks specified fields, alot less code and no cyclic errors

yup.object().shape({
        a: yup.string(),
        b: yup.string(),
        AorB: yup.bool().when(['a', 'b'], {
            is: (a, b) => (!a && !b) || (!!a  && !!b),
            then: yup.bool().required('some error msg'),
            otherwise: yup.bool()
        })
    })

This will generate an error and prevent submit.

When using Formik Fields just remember to include additional errors from this 'ghost' field, like:

<Field
        error={!!(errors && errors.a || (errors as any).AorB)}
        helperText={errors && errors.b || (errors as any).AorB)}
/>

(errors as any) because TypeScript will not see this error (field is not in initialValues)

@albert-medcheck
Copy link

I only want to check heightUnit if heightValue is > 0.

heightValue: Yup.number().positive().when(['heightUnit'], {
      is: val => !!val,
      then: Yup.number(),
      otherwise: Yup.number().negative('Unit is required')
})

@dlamoureux
Copy link

#176 (comment) This comment save my day !

@octoXme
Copy link

octoXme commented May 8, 2020

#176 (comment) This comment save my day !

Could you please provide me an example? I couldn't get it working :(

@ibrambe
Copy link

ibrambe commented Jun 9, 2020

This works for me:

const phoneRegex = /^\+([0-9]{1,3})*\.([0-9]{5,16})$/;

yup.object().shape({
    phoneNumber: yup.string()
        .matches(phoneRegex)
        .when('mobileNumber', {
            is: val => !!val,
            then: yup.string(),
            otherwise: yup.string()required('phone or mobile number required')
        }),
    mobileNumber: yup.string()
        .matches(phoneRegex)
        .when('phoneNumber', {
            is: val => !!val,
            then: yup.string(),
            otherwise: yup.string().required('phone or mobile number required')
        })
}, [['mobileNumber', 'phoneNumber']]);

@ashutoshtrack
Copy link

Hi @pmonty the issue in your last snippet I believe is because you aren't properly enumerating the list of 'edges' in your validation schema.

Pretty much in the array of combinations at the end of the .shape(...) call you need to list all the pair-wise (2-tuples) combinations - in your example you have triples (i.e. ['a','b','c']).
Because you have 4 fields, you'd expect to have 6 pairs in your array.

Anyway, I did this and it worked fine for me.

this is working for me

const AddusersSchema= object().shape({ lastName: string().when(['firstname', 'emailId', 'userType1'], { is: (firstname,emailId,userType1 ) => firstname || emailid || userType1, then: string().required() }), firstname: string().when(['lastName', 'userType1','emailId'], { is: ( lastsame, emailId , userType1) => lastsame || emailId || userType1, then: string().required() }), emailId: string().when(['firstname', 'lastName', 'userType1'], { is: (firstname,lastsame1, userType1) => firstname || lastsame1 || userType1, then: string().required() }).matches(/(?=.*[@$!%*#?&])/, 'Please enter valid email.'), userType1: string().when(['firstname','lastname','emailId'], { is: ( lastsame, emailId , userType1) => lastsame || emailId || userType1, then: string().required() }) },[['firstname','lastName'], ['firstname', 'emailId'] , [ 'userType1','emailId'] , ['lastName', 'userType1'], ['lastName', 'emailId'], ['firstname','userType1']]);

@dineshdsv3
Copy link

var schema = yup.object().shape({
  a: yup.string().when(['b', 'c'], {
    is: (b, c) => !b && !c,
    then: yup.string().required()
  }),
  b: yup.string().when(['a', 'c'], {
    is: (a, c) => !a && !c,
    then: yup.string().required()
  }),
  c: yup.string().when(['a', 'b'], {
    is: (a, b) => !a && !b,
    then: yup.string().required()
  })
}, [['a', 'b'], ['a', 'c'], ['b','c']]) // <-- HERE!!!!!!!!

it is throwing Uncaught Error: Cyclic dependency, the node was: "b". Someone, Please, help me with the working code. I required only one of the fields

@HeathNaylor
Copy link

Is this still a problem or is there a documented solution?

@daniele-barbosa-ifood
Copy link

I'm trying something like

 a: yup.object().when(['b'], {
    is: (b) => !b,
    then: yup.object().required()
  }),
  b: yup.string().when(['a'], {
    is: (a) => !a,
    then: yup.string().required()
  }),

And getting
Error: Cyclic dependency :(
I want that at least one of them is defined

@bchenSyd
Copy link

I can follow what the author said but I think it's a horrible issue and should be mentioned in the document

@akk6
Copy link

akk6 commented Mar 24, 2021

Hi All,

Is this a correct way to mention your schema. I was getting the cyclic dependency issue too but the below schema resolved my issue.Just wanted to confirm if the below way of declaring the schema is correct.Please note that field field4 and field5 are not involved in the interdependent set of fields (1,2,3)

schema = Yup.object().shape({
field1: Yup.string().when(['field2', 'field3'], {
is: (field2, field3) => !field2 && !field3,
then: Yup.string().required()
}),
field2: Yup.string().when(['field1', 'field3'], {
is: (field1, field3) => !field1 && !field3,
then: Yup.string().required()
}),
field3: Yup.string().when(['field1', 'field2'], {
is: (field1, field2) => !field1 && !field2,
then: Yup.string().required()
}),
field4: Yup.string().max(30, "Maximum 30 characters are allowed").required("This is a mandatory field"),
field5: Yup.string().max(30, "Maximum 30 characters are allowed").required("This is a mandatory field")

}
,[['field1', 'field2'], ['field1', 'field3'], ['field2','field3']]
)

@m-nathani
Copy link

var schema = yup.object().shape({
  a: yup.string().when(['b', 'c'], {
    is: (b, c) => !b && !c,
    then: yup.string().required()
  }),
  b: yup.string().when(['a', 'c'], {
    is: (a, c) => !a && !c,
    then: yup.string().required()
  }),
  c: yup.string().when(['a', 'b'], {
    is: (a, b) => !a && !b,
    then: yup.string().required()
  })
}, [['a', 'b'], ['a', 'c'], ['b','c']]) // <-- HERE!!!!!!!!

this worked... we need to add the cyclic dependent fields into the dependency of Yup object..

its more of like react hooks if i am not wrong..

@ruturaj-thakur
Copy link

var schema = yup.object().shape({
  a: yup.string().when(['b', 'c'], {
    is: (b, c) => !b && !c,
    then: yup.string().required()
  }),
  b: yup.string().when(['a', 'c'], {
    is: (a, c) => !a && !c,
    then: yup.string().required()
  }),
  c: yup.string().when(['a', 'b'], {
    is: (a, b) => !a && !b,
    then: yup.string().required()
  })
}, [['a', 'b'], ['a', 'c'], ['b','c']]) // <-- HERE!!!!!!!!

how can we add type error message along with this?

@bschwartz757
Copy link

I'm having an issue getting this to work. I have 3 nested fields, and I want them to be optional, but if one of the the three is filled then they all need to be filled. Here's my code (the parent birthdate object is nested inside the overall schema object, if that matters):

  birthdate: Yup.object(
    {
      month: Yup.string().when(['day', 'year'], {
        is: (day, year) => (day && day.length > 0) || (year && year.length > 0),
        then: Yup.string().test('length', 'Please enter two digits', (value) =>
          value ? /^\d{2}$/.test(value) : true
        ),
      }),
      day: Yup.string().when(['month', 'year'], {
        is: (month, year) =>
          (month && month.length > 0) || (year && year.length > 0),
        then: Yup.string().test('length', 'Please enter two digits', (value) =>
          value ? /^\d{2}$/.test(value) : true
        ),
      }),
      year: Yup.string().when(['month', 'day'], {
        is: (month, day) =>
          (month && month.length > 0) || (day && day.length > 0),
        then: Yup.string().test('length', 'Please enter four digits', (value) =>
          value ? /^\d{4}$/.test(value) : true
        ),
      }),
    },
    [
      ['day', 'year'],
      ['month', 'year'],
      ['month', 'day'],
    ]
  ),

I'm getting error: cyclic dependency (node was "year").

Can anyone point me in the right direction?

@bschwartz757
Copy link

I found the fix for my issue in #661 - I wasn't getting that you need to chain .shape() specifically - so Yup.object({...}, [...]) won't work - it HAS TO BE Yup.object().shape({...}, [...]).

@krutiamrutiya
Copy link

@nandastone

You can also use the test() function in yup validation to test whether the value is present or not in another dependent field. I tried in my case in which I have two dependent dropdown fields.

Ex: In my case when I select a delivery_mode as a "Courier" then my Courier Name field is required and when I select the other delivery mode then it is a non-mandatory field.

like this:

delivery_mode: Yup.object().required("Delivery Mode is required"),
  courier_name: Yup.object().test(
    "Test courier name validation",
    "Courier Name is required",
    (value, { parent }) => {
      if (!value && parent?.delivery_mode?.label === "Courier") {
        return false;
      } else {
        return true;
      }
    },
  )

and I also add the condition in isRequired prop in the form for true or false

like this:
isRequired={values?.delivery_mode?.label == "Courier" ? true: false}

@konampruthvirajkumar
Copy link

it all works fine with this approach but I cannot override the required in then with notRequired in otherwise. yup always return with the validation error for other fields if the condition is satisfied for one of the n values

@minhajul-karim
Copy link

If I have 4 fields i.e. a, b, c, & d, then the pair-wise combination array at the end of shape() would be [["a", "b"], ["a", "c"], ["a", "d"], ["b", "c"], ["b", "d"], ["c", "d"]]
The schema for the selection of any one field out of four fields:

  const validationSchema = yup.object().shape(
    {
      courseName: yup
        .string()
        .when(["programName", "contentBundleName", "contentRoleName"], {
          is: (a, b, c) => !a && !b && !c,
          then: () => yup.string().required()
        }),
      programName: yup
        .string()
        .when(["courseName", "contentBundleName", "contentRoleName"], {
          is: (a, b, c) => !a && !b && !c,
          then: () => yup.string().required()
        }),
      contentBundleName: yup
        .string()
        .when(["courseName", "programName", "contentRoleName"], {
          is: (a, b, c) => !a && !b && !c,
          then: () => yup.string().required()
        }),
      contentRoleName: yup
        .string()
        .when(["courseName", "programName", "contentBundleName"], {
          is: (a, b, c) => !a && !b && !c,
          then: () => yup.string().required()
        })
    },
    [
      ["courseName", "programName"],
      ["courseName", "contentBundleName"],
      ["courseName", "contentRoleName"],
      ["programName", "contentBundleName"],
      ["programName", "contentRoleName"],
      ["contentBundleName", "contentRoleName"]
    ]
  );

brandondorner added a commit to department-of-veterans-affairs/caseflow that referenced this issue Nov 16, 2023
Awesome conditional checkbox validation code found here:
jquense/yup#176 (comment)
craigrva pushed a commit to department-of-veterans-affairs/caseflow that referenced this issue Nov 17, 2023
…9876)

* Refactor ConditionContent rendering

There's two things that get done in this commit.

1. Add a "middle content" section that is conditionally displayed.
This displays the "includes" text for all but the `daysWaiting`
condition. As a result of this some css changes were made to correctly
display this text.

2. Remove the getConditionContent() function and instead change that
to a memoized conditionContent variable. I can't find an article on it
but I was taught to avoid calling functions that render React
components within jsx. I believe it caused rendering/state issues but
idk I can't find anything on it so who knows. At the least this should
have better performance using useMemo

* Add DecisionReviewType condition for `ReportPage`

For: https://jira.devops.va.gov/browse/APPEALS-30938

Add the DecisionReviewType condition field inputs for the Generate
Task Report effort.

* Upgrade scss_lint gem

Goes from scss_lint (0.58.0) to scss_lint (0.60.0).

* Add DecisionReviewType validation

Awesome conditional checkbox validation code found here:
jquense/yup#176 (comment)

* Update margins of Conditions section

This is to better match the designs.

* Fix ConditionContainer useMemo call

There was a useMemo call that didn't work as intended so it's now
being removed.

* Fix failing snapshot
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests