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,
});
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,
});