Last active
May 22, 2024 10:31
-
-
Save amitasaurus/4a2f186847455fe74467709ca4d869b8 to your computer and use it in GitHub Desktop.
Form Validation with RHF & Zod
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//a dynamic version where you can send schema from backend via API and form is rendered on frontend | |
import { useId } from 'react'; | |
import { FormSchema } from './form'; | |
import { useForm } from 'react-hook-form'; | |
import { zodResolver } from '@hookform/resolvers/zod'; | |
import * as z from 'zod'; | |
type Props = {}; | |
type FormField = Array<Record<string, string | undefined>>; | |
type ShapeType = typeof FormSchema.shape; | |
type Keys = keyof ShapeType; | |
type ShapeField = | |
| z.ZodString | |
| z.ZodEffects<z.ZodNumber, number, number> | |
| z.ZodNumber; | |
type IFormInput = z.infer<typeof FormSchema>; | |
function getSchemaDetails(shape: ShapeType): FormField { | |
const results: FormField = []; | |
function getFieldType(shapeField: ShapeField): string | undefined { | |
if (shapeField instanceof z.ZodString) { | |
return 'string'; | |
} else if (shapeField instanceof z.ZodNumber) { | |
return 'number'; | |
} else if (shapeField instanceof z.ZodEffects) { | |
return getFieldType(shapeField._def.schema); | |
} | |
} | |
for (const key in shape) { | |
if (Object.prototype.hasOwnProperty.call(shape, key)) { | |
results.push({ | |
key: key, | |
value: getFieldType(shape[key as Keys]), | |
}); | |
} | |
} | |
return results; | |
} | |
const formData = getSchemaDetails(FormSchema.shape); | |
export default function DynamicForm({}: Props) { | |
const { | |
register, | |
handleSubmit, | |
formState: { errors }, | |
} = useForm<IFormInput>({ | |
resolver: zodResolver(FormSchema), | |
}); | |
const onSubmit = (data: IFormInput) => { | |
console.log(data); | |
}; | |
return ( | |
<div> | |
<form> | |
{formData.map((e) => ( | |
<> | |
<div | |
className="border-2 border-solid rounded flex items-center mb-4" | |
key={useId()} | |
> | |
<input | |
{...register(e.key as Keys, { | |
valueAsNumber: e.value === 'number', | |
})} | |
type={e.value} | |
placeholder={e.key} | |
className="text-gray-700 h-10 py-1 pr-3 w-full px-2" | |
/> | |
</div> | |
{errors[e.key as Keys]?.message && ( | |
<p className="text-red-700 mb-4"> | |
{errors[e.key as Keys]?.message} | |
</p> | |
)} | |
</> | |
))} | |
<div className="text-center mt-6 md:mt-12"> | |
<button | |
onClick={handleSubmit(onSubmit)} | |
className="bg-indigo-600 hover:bg-indigo-700 text-white text-xl py-2 px-4 md:px-6 rounded transition-colors duration-300" | |
> | |
Sign Up <span className="far fa-paper-plane ml-2"></span> | |
</button> | |
</div> | |
</form> | |
</div> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useForm } from 'react-hook-form'; | |
import { zodResolver } from '@hookform/resolvers/zod'; | |
import * as z from 'zod'; | |
export const FormSchema = z.object({ | |
username: z | |
.string() | |
.min(3, 'Username must not be lesser than 3 characters') | |
.max(25, 'Username must not be greater than 25 characters') | |
.regex( | |
/^[a-zA-Z0-9_]+$/, | |
'The username must contain only letters, numbers and underscore (_)' | |
), | |
email: z.string().email('Invalid email. Email must be a valid email address'), | |
password: z | |
.string() | |
.min(3, 'Password must not be lesser than 3 characters') | |
.max(16, 'Password must not be greater than 16 characters'), | |
fullName: z.string().min(3, 'Name must not be lesser than 3 characters'), | |
age: z.number().refine( | |
(age) => { | |
return Number(age) >= 18; | |
}, | |
{ message: 'You must be 18 years or older' } | |
), | |
}); | |
type IFormInput = z.infer<typeof FormSchema>; | |
export default function Form() { | |
const { | |
register, | |
handleSubmit, | |
formState: { errors }, | |
} = useForm<IFormInput>({ | |
resolver: zodResolver(FormSchema), | |
}); | |
const onSubmit = (data: IFormInput) => { | |
console.log(data); | |
}; | |
return ( | |
<div> | |
<div className="mx-auto"> | |
<div className="box bg-white p-6 md:px-12 md:pt-12 border-t-10 border-solid border-indigo-600"> | |
<h2 className="text-3xl text-gray-800 text-center"> | |
Create Your Account | |
</h2> | |
<form onSubmit={handleSubmit(onSubmit)}> | |
<div className="signup-form mt-6 md:mt-12"> | |
<div className="border-2 border-solid rounded flex items-center mb-4"> | |
<input | |
{...register('username')} | |
type="text" | |
placeholder="Username" | |
className="text-gray-700 h-10 py-1 pr-3 w-full px-2" | |
autoComplete="username" | |
/> | |
</div> | |
{errors?.username?.message && ( | |
<p className="text-red-700 mb-4">{errors.username.message}</p> | |
)} | |
<div className="border-2 border-solid rounded flex items-center mb-4"> | |
<input | |
{...register('email')} | |
type="text" | |
placeholder="E-mail" | |
className="text-gray-700 h-10 py-1 pr-3 w-full px-2" | |
autoComplete="email" | |
/> | |
</div> | |
{errors?.email?.message && ( | |
<p className="text-red-700 mb-4">{errors.email.message}</p> | |
)} | |
<div className="border-2 border-solid rounded flex items-center mb-4"> | |
<input | |
{...register('password')} | |
type="password" | |
placeholder="Password" | |
className="text-gray-700 h-10 py-1 pr-3 w-full px-2" | |
autoComplete="current-password webauthn" | |
/> | |
</div> | |
{errors?.password?.message && ( | |
<p className="text-red-700 mb-4">{errors.password.message}</p> | |
)} | |
<div className="border-2 border-solid rounded flex items-center mb-4"> | |
<input | |
{...register('fullName')} | |
type="text" | |
placeholder="Full name" | |
className="text-gray-700 h-10 py-1 pr-3 w-full px-2" | |
autoComplete="name" | |
/> | |
</div> | |
{errors?.fullName?.message && ( | |
<p className="text-red-700 mb-4">{errors.fullName.message}</p> | |
)} | |
<div className="border-2 border-solid rounded flex items-center mb-4"> | |
<input | |
{...register('age', { valueAsNumber: true })} | |
type="number" | |
placeholder="Age" | |
className="text-gray-700 h-10 py-1 pr-3 w-full px-2" | |
/> | |
</div> | |
{errors?.age?.message && ( | |
<p className="text-red-700 mb-4">{errors.age.message}</p> | |
)} | |
<p className="text-sm text-center mt-6"> | |
By signing up, you agree to our{' '} | |
<a href="#" className="text-indigo-600 hover:underline"> | |
Terms | |
</a>{' '} | |
and{' '} | |
<a href="#" className="text-indigo-600 hover:underline"> | |
Privacy Policy | |
</a> | |
</p> | |
<div className="text-center mt-6 md:mt-12"> | |
<button | |
onClick={handleSubmit(onSubmit)} | |
className="bg-indigo-600 hover:bg-indigo-700 text-white text-xl py-2 px-4 md:px-6 rounded transition-colors duration-300" | |
> | |
Sign Up <span className="far fa-paper-plane ml-2"></span> | |
</button> | |
</div> | |
</div> | |
</form> | |
<div className="border-t border-solid mt-6 md:mt-12 pt-4"> | |
<p className="text-gray-500 text-center"> | |
Already have an account,{' '} | |
<a href="#" className="text-indigo-600 hover:underline"> | |
Sign In | |
</a> | |
</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment