reactjstypescriptnext.jsreact-hook-formzod

Why is my conditional validation not working using zod and react-hook-form?


Good Day! I'm having a little trouble after refactoring my code, I created a conditional field wherein if the user selected Yes there will an additional field below wherein the user must answer it. And if the user selected No it will be just an empty string.

The problem I'm encountering right now, is when the selected Yes the additional field are not set to required.

Steps.ts

const steps = [
  {
    id: 'Step 1',
    name: 'General Information',
    fields: ['title', 'email', 'fullname', 'contact_person', 'department']
  },
  {
    id: 'Step 2',
    name: 'Date, Time and Purpose',
    fields: ['event_date, start_time', 'end_time', 'purpose']
  },
  {
    id: "Step 3",
    name: "Dry Run",
    fields: ['does_have_dry_run', 'dry_run_date', 'dry_run_start_time', 'dry_run_end_time', 'does_have_assistance', 'name_of_assistance']
  },
  {
    id: "Step 4",
    name: "Type of Service",
    fields: ['meeting_type_option', 'meeting_type_service', 'meeting_type_link']
  },
  {
    id: "Steps 5",
    name: "Type of Service",

  }
]

Form.tsx


const CreateScheduleDialog = ({ open, setOpen, pickedDate }: Props) => {

  const [optionalDate, setOptionalDate] = useState<Date | undefined>(
    new Date()
  );
  const [step, setStep] = useState(0);
  const currentStep = steps[step];


  const form = useForm<CreateAppointmentSchemaType>({
    resolver: zodResolver(CreateAppointmentSchema),
    defaultValues: {
      does_have_dry_run: false,
      dry_run_date: new Date(),
      dry_run_start_time: '',
      dry_run_end_time: '',
   
    }
  })

  type FieldName = keyof CreateAppointmentSchemaType;


  const nextStep = async () => {
    const fields = steps[step].fields as FieldName[];
    const isValid = await form.trigger(fields, { shouldFocus: true });

    if (!isValid) return;

    setStep((prevStep) => prevStep + 1);
  };

  const prevStep = () => {
    if (step > 0) {
      setStep(step - 1);
    }
  };

  const handleClick = () => {
    form.resetField("dry_run_date");
    form.resetField("dry_run_start_time");
    form.resetField("dry_run_end_time");
  };

  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button disabled={open !== false ? false : true} className='w-full' color="primary-foreground">Add Schedule</Button>
      </DialogTrigger>
      <DialogContent className={cn(`max-w-[400px] md:max-w-[800px]`)}>
        <DialogHeader>
          <DialogTitle>{currentStep.name}</DialogTitle>
          <DialogDescription>
            Step {step + 1} of {steps.length}
          </DialogDescription>
        </DialogHeader>
        <Form {...form}>
        
          {step == 2 && (
            <div className='flex flex-col gap-y-2'>
              <FormField
                control={form.control}
                name="does_have_dry_run"
                render={({ field }) => (
                  <FormItem className="space-y-3">
                    <FormLabel>
                      (Optional) Preferred Meeting Date / Dry Run
                    </FormLabel>
                    <FormControl>
                      <RadioGroup
                        onValueChange={(value) =>
                          field.onChange(value === "true")
                        }
                        defaultValue={String(field.value)}
                        className="flex flex-col space-y-1"
                      >
                        <FormItem className="flex items-center space-x-3 space-y-0">
                          <FormControl>
                            <RadioGroupItem
                              onClick={handleClick}

                              value="false" />
                          </FormControl>
                          <FormLabel className="font-normal">
                            None / No
                          </FormLabel>
                        </FormItem>
                        <FormItem className="flex items-center space-x-3 space-y-0">
                          <FormControl>
                            <RadioGroupItem value="true" />
                          </FormControl>
                          <FormLabel className="font-normal">
                            Yes
                          </FormLabel>
                        </FormItem>
                      </RadioGroup>
                    </FormControl>

                    {field.value === true && (
                      <FormItem>
                        <div className="flex flex-col gap-2 pt-2">
                          <Label>(Dry Run) Time of Event</Label>
                          <div className="flex flex-col gap-2">
                            <FormField
                              control={form.control}
                              name="dry_run_date"
                              render={({ field }) => (
                                <FormItem className="flex flex-col">
                                  <FormLabel>Date</FormLabel>
                                  <Popover>
                                    <PopoverTrigger asChild>
                                      <FormControl>
                                        <Button
                                          variant={"outline"}
                                          className={cn(
                                            "w-[240px] pl-3 text-left font-normal",
                                            !field.value &&
                                            "text-muted-foreground"
                                          )}
                                        >
                                          {field.value ? (
                                            format(field.value, "PPP")
                                          ) : (
                                            <span>Pick a date</span>
                                          )}
                                          <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
                                        </Button>
                                      </FormControl>
                                    </PopoverTrigger>
                                    <PopoverContent
                                      className="w-auto p-0"
                                      align="start"
                                    >
                                      <Calendar
                                        mode="single"
                                        disabled={(date) =>
                                          new Date(date) <= new Date()
                                        } // Disable past dates and today's date
                                        selected={optionalDate}
                                        onSelect={field.onChange}
                                        initialFocus
                                      />
                                    </PopoverContent>
                                  </Popover>
                                  <FormMessage />
                                </FormItem>
                              )}
                            />
                            <FormField
                              control={form.control}
                              name="dry_run_start_time"
                              render={({ field }) => (
                                <FormItem>
                                  <FormLabel>Start</FormLabel>
                                  <FormControl>
                                    <Input type="time" placeholder="Start" {...field} />
                                  </FormControl>
                                  <FormMessage />
                                </FormItem>
                              )}
                            />
                            <FormField
                              control={form.control}
                              name="dry_run_end_time"
                              render={({ field }) => (
                                <FormItem>
                                  <FormLabel>End</FormLabel>
                                  <FormControl>
                                    <Input type="time" placeholder="End" {...field} />
                                  </FormControl>
                                  <FormMessage />
                                </FormItem>
                              )}
                            />

                          </div>
                        </div>
                      </FormItem>
                    )}

                    <FormMessage />
                  </FormItem>
                )}
              />

          
            </div>
          )}
         
        </Form>
        <DialogFooter>
          <Button onClick={prevStep} disabled={step === 0}>Back</Button>
          {step === steps.length - 1 ? (
            <Button
              disabled={!form.formState.isValid}>
              Submit
            </Button>
          ) : (
            <Button
              onClick={nextStep}
            >
              Next
            </Button>
          )}
        </DialogFooter>
      </DialogContent>

    </Dialog>
  )
}

Now what I did is I created a zod wherein I use .superRefine so I can add validation but it's not working properly.

zod.ts

import { z } from "zod";


export const CreateAppointmentSchema = z.object({

  // //SET 3
  does_have_dry_run: z.boolean({
    required_error: "Please select if yes or no"
  }),
  dry_run_date: z.coerce.date().optional(),
  dry_run_start_time: z.string().optional(),
  dry_run_end_time: z.string().optional(),

  })
  .superRefine(({
    does_have_dry_run,
    dry_run_date,
    dry_run_start_time,
    dry_run_end_time,

    }, ctx) => {
    if (does_have_dry_run === true) {
      if (!dry_run_date) {
        ctx.addIssue({
          code: 'custom',
          message: 'Please provide information in the missing field.',
          path: ['dry_run_date']
        })
      }
      if (!dry_run_start_time) {
        ctx.addIssue({
          code: 'custom',
          message: 'Please provide information in the missing field.',
          path: ['dry_run_start_time']
        })
      }
      if (!dry_run_end_time) {
        ctx.addIssue({
          code: 'custom',
          message: 'Please provide information in the missing field.',
          path: ['dry_run_end_time']
        })
      }
    }

});

export type CreateAppointmentSchemaType = z.infer<typeof CreateAppointmentSchema>

Solution

  • if the user selects 'yes' in the dropdown then the following fields will be required. You can modify your zod schema like this to get the desired results

    const CreateAppointmentSchema = z
      .object({
        dryrun: z.string(),
        dry_run_start_time: z.string(),
        dry_run_end_time: z.string(),
      })
      .partial()
      .superRefine((v, ctx) => {
        if (v.dryrun == "no" || (v.dry_run_start_time && v.dry_run_end_time))
          return true;
    
        if (!v.dry_run_start_time) {
          ctx.addIssue({
            code: z.ZodIssueCode.custom,
            message: "dryrun start time is  required",
            path: ["dry_run_start_time"],
          });
        }
        if (!v.dry_run_end_time) {
          ctx.addIssue({
            code: z.ZodIssueCode.custom,
            message: "dryrun end time is  required",
            path: ["dry_run_end_time"],
          });
        }
      });
    
    

    here is a working example of the solution as well: https://replit.com/@MubashirWaheed/zodValidation#app/page.tsx