Open martin-dimi opened 1 year ago
I was simply missing: mode: 'onChange'
on the form.
In the tutorial, the preview seems to be using mode: 'onTouched'
but the example code does not reflect that.
Worth updating?
The documentation for Form component could be more elaborative. It is difficult for a beginner to follow through.
I was simply missing: mode: 'onChange' on the form. In the tutorial, the preview seems to be using mode: 'onTouched' but the example code does not reflect that. Worth updating?
Maybe a good idea to explicitly define the mode in the example so it's clear the prop exists
I was simply missing:
mode: 'onChange'
on the form. In the tutorial, the preview seems to be usingmode: 'onTouched'
but the example code does not reflect that. Worth updating?
I have the same problem, where in the code did you add the mode?
Add it to the useForm hook.
const form = useForm({
...,
mode: "onChange"
})
To anyone else wondering, none of the solutions above worked for me. The workaround was to remove zod validation.
previous:
const form = z.object({
otp: z.string().min(INPUT_NUM, `OTP must be ${INPUT_NUM} characters long`),
});
type ClientForm = z.infer<typeof form>;
const multiForm = useForm<ClientForm>({
defaultValues: {
otp: "",
},
mode: "onChange",
});
const onSubmit = async (data: ClientForm) => {}
after:
interface FormData {
otp: string;
}
const multiForm = useForm({
defaultValues: {
otp: "",
},
});
const onSubmit = async (data: FormData) => {}
Problem for me was that zod would kick in if I submitted once, got an error from the server, then tried to clear the errors and try again. Once I started typing the second time around, my zod error would appear asking for 6 character input. I don't want the error to display though if the user is still typing it in the second time around. The only workaround I could find was to do manual validation, and not use zod. This solution actually works as expected. Very frustrating behavior by this library.
Full final working context:
"use client";
import { Button } from "@/components/ui/button";
import { OtpStyledInput } from "@/components/ui/input-otp";
import { useForm } from "react-hook-form";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { useSearchParams } from "next/navigation";
import { verifyOTP } from "@/components/actions";
import { useEffect, useState } from "react";
import { Spinner } from "@/components/ui/spinner";
const INPUT_NUM = 6;
interface FormData {
otp: string;
}
export default function VerifyOTPPageComponent() {
const searchParams = useSearchParams();
const [inputKey, setInputKey] = useState(0);
const [show, setShow] = useState(false);
const multiForm = useForm({
defaultValues: {
otp: "",
},
});
const otpValue = multiForm.watch("otp");
useEffect(() => {
if (otpValue.length === INPUT_NUM) {
multiForm.handleSubmit(onSubmit)();
}
}, [otpValue]);
const onSubmit = async (data: FormData) => {
setShow(true);
if (data.otp.length !== INPUT_NUM) {
multiForm.setError("otp", {
message: `OTP must be ${INPUT_NUM} characters long`,
});
multiForm.setFocus("otp");
setInputKey((prevKey) => prevKey + 1);
setShow(false);
return;
}
try {
await verifyOTP(
decodeURIComponent(searchParams.get("phone") || ""),
data.otp,
);
multiForm.reset();
} catch (error: unknown) {
console.error("Error during OTP verification:", error);
multiForm.reset();
multiForm.clearErrors();
multiForm.setError("otp", { message: (error as Error).message });
multiForm.setFocus("otp");
setInputKey((prevKey) => prevKey + 1);
}
setShow(false);
};
return (
<div className="flex flex-col items-center justify-center h-screen">
<div className="bg-white dark:bg-gray-950 shadow-lg rounded-lg p-8 w-full max-w-md">
<h2 className="text-2xl font-bold mb-4">Verify OTP</h2>
<p className="text-gray-500 dark:text-gray-400 mb-6">
Enter the 6-digit one-time password sent to your phone.
</p>
<Form {...multiForm}>
<form onSubmit={multiForm.handleSubmit(onSubmit)}>
<FormField
control={multiForm.control}
name="otp"
render={({ field: { ...newField }, fieldState }) => (
<FormControl>
<>
<FormItem>
<OtpStyledInput
key={inputKey}
{...newField}
numInputs={INPUT_NUM}
shouldAutoFocus={true}
/>
</FormItem>
<FormItem>
<div className="flex justify-end mt-6">
<Button type="submit" className="w-full">
{show ? (
<Spinner
show={show}
size="small"
className="text-primary-foreground"
/>
) : (
"Verify OTP"
)}
</Button>
</div>
</FormItem>
<FormMessage>
{fieldState.invalid && (
<div className="text-red-600 text-sm mt-2 text-center">
{fieldState.invalid}
</div>
)}
</FormMessage>
</>
</FormControl>
)}
/>
</form>
</Form>
</div>
</div>
);
}
@ProductOfAmerica removing the zodResolver was very helpful for tracking down a bug, thanks for the tip
Hi, I pretty much have followed the guide https://ui.shadcn.com/docs/forms/react-hook-form step by step, but unfortunately, I get no validation error messages. I've checked and the
resolver
method on theuseForm
is not getting called.The code is pretty much 1-1 to the guide but still:
All of the Form components + Input are again direct copies from shadcn