Init.

Install Form by Shadcn

>_ Terminal

npx shadcn-ui@latest add form input

Create Your Form

Note that everything is in the same file for demo purpose. You should divide this in multiple files for a better DX.

form.tsx

"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { FC } from "react";
import {
  Control,
  FieldPath,
  FieldValues,
  useForm,
  useFormContext,
} from "react-hook-form";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input, InputProps } from "@/components/ui/input";

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

const formSchema = z
  .object({
    username: z.string().min(5, {
      message: "Le nom d'utilisateur est trop court.",
    }),
    email: z.string().email({ message: "Ce champ doit être un email valide." }),
    age: z.coerce
      .number({ message: "Vous devez rentrer un chiffre." })
      .min(13, { message: "Age minimum : 13 ans" }),
    password: z
      .string()
      .min(8, { message: "Le mot de passe doit faire au moins 8 caractères" }),
    passwordConfirm: z
      .string()
      .min(8, { message: "Le mot de passe doit faire au moins 8 caractères." }),
  })
  .refine((data) => data.password === data.passwordConfirm, {
    message: "Les mots de passes sont différents.",
    path: ["passwordConfirm"],
  });

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

type FormInputProps<T extends FieldValues> = {
  control?: Control<T>;
  name: FieldPath<T>;
  type: InputProps["type"];
  label?: string;
  description?: string;
};

const FormInput = <T extends FieldValues>({
  control,
  name,
  type,
  label,
  description,
}: FormInputProps<T>) => {
  const { control: defaultControl } = useFormContext<T>();
  return (
    <FormField
      control={control || defaultControl}
      name={name}
      render={({ field }) => (
        <FormItem>
          {!!label && <FormLabel>{label}</FormLabel>}
          <FormControl>
            <Input type={type} {...field} />
          </FormControl>
          {!!description && <FormDescription>{description}</FormDescription>}
          <FormMessage />
        </FormItem>
      )}
    />
  );
};

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

const Formulaire: FC = () => {
  // 1. Define your form.
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      email: "",
      age: undefined,
      password: "",
      passwordConfirm: "",
    },
  });

  // 2. Define a submit handler.
  function onSubmit(values: z.infer<typeof formSchema>) {
    // Do something with the form values.
    // This will be type-safe and validated.
    alert(JSON.stringify(values, null, 2));
  }

  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(onSubmit)}
        className="flex flex-col gap-3 container py-6"
      >
        <FormInput
          control={form.control}
          name={"username"}
          label={"Username"}
          type="string"
        />

        <FormInput
          control={form.control}
          name={"email"}
          label={"Email"}
          type="email"
        />

        <FormInput
          control={form.control}
          name={"age"}
          label={"Age"}
          type="number"
        />

        <FormInput
          control={form.control}
          name={"password"}
          label={"Password"}
          type="password"
        />

        <FormInput
          control={form.control}
          name={"passwordConfirm"}
          label={"Confirm Password"}
          type="password"
        />

        <Button type="submit">Submit</Button>
      </form>
    </Form>
  );
};

export default Formulaire;