reactjstypescriptreact-hook-formzod

How can I disable next button if the user didn't complete the first step?


so I've created a multi-step form using shadcn , react-hook form and zod. I've created 3 types of zod schema and joined them using z.union from zod. The problem I'm encountering right now is I can still press the next button even though I didn't complete the input.

Appointment.ts

export const AppointmentSchema = z.union([
    FirstPartSchema,
    SecondPartSchema,
    ThirdPartSchema,
])

//There came from different Ts files
export const FirstPartSchema= z.object({
    title: z.string(),
  });

export const SecondPartSchema= z.object({
    name: z.string(),
  });


export const ThirdPartSchema= z.object({
    description: z.string(),
  });

This is my form.tsx.

const steps = [
  {
    id: 'Step 1',
    name: 'General Information',
    fields: ['title']
  },
  {
    id: 'Step 2',
    name: 'Date , Time and Assistance ',
    fields: ['name']
  },
  {
    id: "Step 3",
    name: "Dry Run",
    fields: ['description']
  },
]

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

  const [step, setStep] = useState(0);
  const currentStep = steps[step];

  const nextStep = () => {
    if (step < steps.length - 1) {
      setStep(step + 1);
    }
  };

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

  const form = useForm<AppointmentSchemaType>({
    resolver: zodResolver(AppointmentSchema),
    defaultValues: {
    }
  })

  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 == 0 && (
            <div className='flex flex-col gap-y-2'>
              <FormField
                control={form.control}
                name="title"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Title</FormLabel>
                    <FormControl>
                      <Input placeholder="Title" {...field} />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />
            </div>
          )}
          {step == 1 && (
            <div>
              <FormField
                control={form.control}
                name="name"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Name</FormLabel>
                    <FormControl>
                      <Input placeholder="Name" {...field} />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />
            </div>
          )}
          {step == 2 && (
            <div>
              <FormField
                control={form.control}
                name="description"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Description</FormLabel>
                    <FormControl>
                      <Input placeholder="Description" {...field} />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />
            </div>
          )}
        </Form>
        <DialogFooter>
          <Button onClick={prevStep} disabled={step === 0}>Back</Button>
          {step === steps.length - 1 ? (
            <Button
            // onClick={form.handleSubmit(onSubmit)}
            >Submit</Button>
          ) : (
            <Button onClick={nextStep}>Next</Button>
          )}
        </DialogFooter>
      </DialogContent>

    </Dialog>
  )
}

Solution

  • First you can use this flag in your function

    const nextStep = async () => {
        const isValid = await form.trigger(); // Trigger validation for all fields
        if (isValid) {
          setStep((prevStep) => prevStep + 1);
        }
      }
    

    instead of doing this

    <Button onClick={prevStep} disabled={step === 0}>
      Back
    </Button>
    {step === steps.length - 1 ? (
      <Button
        // onClick={form.handleSubmit(onSubmit)}
      >
        Submit
      </Button>
    ) : (
      <Button onClick={nextStep}>
        Next
      </Button>
    )}
    

    you can use this now

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

    Disable the "Next" button (disabled={!form.formState.isValid}) based on form.formState.isValid. This prevents the user from advancing to the next step if the current step's fields are invalid.