colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
32.64k stars 1.13k forks source link

Zod/ React Hook Form - Dynamic Field Array - Conditional Field #3626

Open danielbattat opened 1 month ago

danielbattat commented 1 month ago

I am trying to create a validation scheme for conditional fields within a field array.

For example, the data can include a list of persons, where first and last name are always required, but address is only required if a box is checked off indicating 'different from above'.

Valid data can look like this:

[
  {
      'firstName': 'John', //required string
      'lastName': 'Smith', //required string
      'addressSameAsAbove': false, //required bool,
      'address': //required string
 },
 {
      'firstName': 'John', //required string
      'lastName': 'Smith', //required string
      'addressSameAsAbove': true, //required bool,
      //address not required
 }

I was able to accomplish this using refine, however, since the form structure is created dynamically, I need to be able to create the schema dynamically as well based on a form schema like this:

[
  {
    'type': 'Array',
    'children': [
       { type: 'text', name: 'firstName', required: true },
       { type: 'text', name: 'lastName', required: true },
       { type: 'bool', name: 'addressSameAsAbove', required: true },
       { type: 'text', name: 'address', required: { field: 'addressSameAsAbove', condition: false } },
   ]
]

I am able to create the schema dynamically for all fields by creating an object for each 'type' of field, however, I am unable to create the conditional validation based on another field.

Any direction / insight would be appreciated. Is this possible using zod?

generalleger commented 1 month ago

I had a similar challenge. The solution I came up with was to dynamically create a new schema that is passed into the react-hook-form zodResolver function based on the current fields in the form.

I start with a master schema object that has all possible fields in the form, their related validations, and a bunch of other stuff. Then, when the user does something that causes the shape of the form to change, the app calls my custom getValidationSchema function with the current list of fields as the argument. getValidationSchema builds and returns the new schema which is then passed back into zodResolver.

Hope this helps.

danielbattat commented 1 month ago

I had a similar challenge. The solution I came up with was to dynamically create a new schema that is passed into the react-hook-form zodResolver function based on the current fields in the form.

I start with a master schema object that has all possible fields in the form, their related validations, and a bunch of other stuff. Then, when the user does something that causes the shape of the form to change, the app calls my custom getValidationSchema function with the current list of fields as the argument. getValidationSchema builds and returns the new schema which is then passed back into zodResolver.

Hope this helps.

Thank you! How do "refresh" RHF with the new schema? There is no method to update the resolver

generalleger commented 1 month ago

I had a similar challenge. The solution I came up with was to dynamically create a new schema that is passed into the react-hook-form zodResolver function based on the current fields in the form. I start with a master schema object that has all possible fields in the form, their related validations, and a bunch of other stuff. Then, when the user does something that causes the shape of the form to change, the app calls my custom getValidationSchema function with the current list of fields as the argument. getValidationSchema builds and returns the new schema which is then passed back into zodResolver. Hope this helps.

Thank you! How do "refresh" RHF with the new schema? There is no method to update the resolver

Correct. You have to force the component to rerender. In my case, I had to rebuild the entire form when the schema changed, so that was forcing the component to rerender.

I'm not sure about the particulars of your situation, but the easiest way is probably to add a call to RHF's watch() method in the component. That's probably not the most elegant solution, since it would create a rerender on every keystroke into the form. But I am sure you can find a way to suppress unnecessary rerenders if it actually impacts the user experience.