Skip to content

ZOD validation issue with isStepValid #2

@MJUrian-Learner

Description

@MJUrian-Learner

When using resolver to zod schema, the isStepValid returns false even if its on first step

// index.tsx
"use client";

import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { useEffect, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

import { STEPPER_FORM_KEYS } from "@/lib/constants/hook-stepper-constants";
import { StepperFormKeysType, StepperFormValues } from "@/types/hook-stepper";

import StepperIndicator from "../shared/stepper-indicator";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import { Button } from "../ui/button";
import { toast } from "../ui/use-toast";
import AddressInfo from "./address-info";
import ApplicantInfo from "./applicant-info";
import EmploymentInfo from "./employment-info";
import FinancialInfo from "./financial-info";
import LoanDetails from "./loan-details";
import { stepperSchema } from "./schema";

function getStepContent(step: number) {
switch (step) {
case 1:
return ;
case 2:
return ;
case 3:
return ;
case 4:
return ;
case 5:
return ;
default:
return "Unknown step";
}
}

const HookMultiStepForm = () => {
const [activeStep, setActiveStep] = useState(1);
const [erroredInputName, setErroredInputName] = useState("");

const methods = useForm({
mode: "onTouched",
resolver: zodResolver(stepperSchema),
});

const {
trigger,
handleSubmit,
setError,
formState: { isSubmitting, errors },
} = methods;

// focus errored input on submit
useEffect(() => {
const erroredInputElement =
document.getElementsByName(erroredInputName)?.[0];
if (erroredInputElement instanceof HTMLInputElement) {
erroredInputElement.focus();
setErroredInputName("");
}
}, [erroredInputName]);

const onSubmit = async (formData: StepperFormValues) => {
console.log({ formData });
// simulate api call
await new Promise((resolve, reject) => {
setTimeout(() => {
// resolve({
// title: "Success",
// description: "Form submitted successfully",
// });
reject({
message: "There was an error submitting form",
// message: "Field error",
// errorKey: "fullName",
});
}, 2000);
})
.then(({ title, description }) => {
toast({
title,
description,
});
})
.catch(({ message: errorMessage, errorKey }) => {
if (
errorKey &&
Object.values(STEPPER_FORM_KEYS)
.flatMap((fieldNames) => fieldNames)
.includes(errorKey)
) {
let erroredStep: number;
// get the step number based on input name
for (const [key, value] of Object.entries(STEPPER_FORM_KEYS)) {
if (value.includes(errorKey as never)) {
erroredStep = Number(key);
}
}
// set active step and error
setActiveStep(erroredStep);
setError(errorKey as StepperFormKeysType, {
message: errorMessage,
});
setErroredInputName(errorKey);
} else {
setError("root.formError", {
message: errorMessage,
});
}
});
};

const handleNext = async () => {
const isStepValid = await trigger(undefined, { shouldFocus: true });
console.log({ isStepValid });
if (isStepValid) setActiveStep((prevActiveStep) => prevActiveStep + 1);
};

const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};

return (



{errors.root?.formError && (


Form Error
{errors.root?.formError?.message}

)}
<FormProvider {...methods}>

{getStepContent(activeStep)}

<Button
type="button"
className="w-[100px]"
variant="secondary"
onClick={handleBack}
disabled={activeStep === 1}
>
Back

{activeStep === 5 ? (

Submit

) : (

Next

)}




);
};

export default HookMultiStepForm;

//schema.ts

import { z } from "zod";

export const stepperSchema = z.object({
// Step 1: Applicant Info
fullName: z.string().min(1, "Full Name is required"),
dob: z.date().refine((date) => date <= new Date(), {
message: "Date of Birth must be in the past",
}),
email: z.string().email("Invalid email address"),
phone: z.string().min(10, "Phone number must be at least 10 digits"),

// Step 2: Address Info
address: z.string().min(1, "Address is required"),
city: z.string().min(1, "City is required"),
state: z.string().min(1, "State is required"),
zipCode: z.string().length(5, "ZIP Code must be 5 digits"),

// Step 3: Employment Info
employmentStatus: z.string().min(1, "Employment Status is required"),
employerName: z.string().min(1, "Employer Name is required"),
jobTitle: z.string().min(1, "Job Title is required"),
annualIncome: z.number().min(1, "Annual Income is required"),

// Step 4: Loan Details
loanAmount: z.number().min(1, "Loan Amount is required"),
loanPurpose: z.string().min(1, "Loan Purpose is required"),
repaymentTerms: z.string().min(1, "Repayment Terms are required"),
repaymentStartDate: z.date().refine((date) => date >= new Date(), {
message: "Repayment Start Date must be in the future",
}),

// Step 5: Financial Info
bankName: z.string().min(1, "Bank Name is required"),
accountNumber: z.string().min(1, "Account Number is required"),
routingNumber: z.string().min(9, "Routing Number must be 9 digits"),
creditScore: z.number().min(300).max(850, "Credit Score must be between 300 and 850"),
});

// Optional: Partial schema for each step
export const step1Schema = stepperSchema.pick({
fullName: true,
dob: true,
email: true,
phone: true,
});

export const step2Schema = stepperSchema.pick({
address: true,
city: true,
state: true,
zipCode: true,
});

export const step3Schema = stepperSchema.pick({
employmentStatus: true,
employerName: true,
jobTitle: true,
annualIncome: true,
});

export const step4Schema = stepperSchema.pick({
loanAmount: true,
loanPurpose: true,
repaymentTerms: true,
repaymentStartDate: true,
});

export const step5Schema = stepperSchema.pick({
bankName: true,
accountNumber: true,
routingNumber: true,
creditScore: true,
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions