Skip to main content

useForm

Form elements are useful when you need to accept user data entries. This library provides a useForm hook that integrates with Zod to create forms as fast as possible.

Package@g4rcez/componentsSectionForm Controls

Examples

Dynamic Form with Zod Validation

Demonstrates a comprehensive form using `useForm` hook with Zod schema validation, including various input types, dynamic fields, and submission handling.

tsx
"use client";
import { FormEvent, useState } from "react";
import { z } from "zod";
import {
  Autocomplete,
  AutocompleteItemProps,
  Button,
  Checkbox,
  DatePicker,
  Input,
  MultiSelect,
  Select,
  Switch,
  useForm,
  UseOnSubmitArgs,
} from "@g4rcez/components";
import { add } from "date-fns";

const languages: AutocompleteItemProps[] = [
  { label: "Elixir", value: "ex", Render: () => <span>šŸ’§ Elixir</span> },
  { label: "Javascript", value: "js" },
  { label: "Kotlin", value: "kt" },
  { label: "Typescript", value: "typescript" },
  { label: "Java", value: "java" },
  { label: "Cobol", value: "cobol" },
  { label: "C", value: "c" },
  { label: "C++", value: "cpp" },
  { label: "C#", value: "csharp" },
  { label: "F#", value: "fsharp" },
];

const clamp = (min: number, x: number, max: number) =>
  Math.min(Math.max(x, min), max);

const oabMask = (str: string) => {
  const numbers = str.replace(/[^0-9]/g, "");
  const len = clamp(4, numbers.length, 6);
  if (numbers.length === 6)
    return [/[0-9]/, /[0-9]/, /[0-9]/, /[0-9]/, /[0-9]/, /[0-9]/, "/", /[A-Za-z]/, /[A-Za-z]/];
  return Array.from({ length: len })
    .map((): string | RegExp => /[0-9]/)
    .concat(["/", /[A-Za-z]/, /[A-Za-z]/]);
};

const schema = z.object({
  agree: z.literal(true),
  number: z.number(),
  boolean: z.literal(true),
  languages: z.array(z.string()),
  date: z.string().datetime(),
  datetime: z.string().datetime(),
  name: z.string().min(1),
  document: z.string().min(1),
  surname: z.string().min(1),
  main: z.enum(languages.map((x) => x.value) as any),
  secondary: z.enum(languages.map((x) => x.value) as any),
  items: z.array(
    z.object({ document: z.string().min(1), name: z.string().min(1) }),
  ),
  oab: z.array(
    z.object({
      document: z
        .string()
        .regex(new RegExp("[[0-9]{4,6}/[A-Z]"), "Invalid OAB document")
        .transform((x) => x.toUpperCase()),
    }),
    { message: "At least one OAB" },
  ),
});

type Data = z.infer<typeof schema>;
type Args = UseOnSubmitArgs<Data>;

function DatePickerControlled() {
  const [date, setDate] = useState(new Date());
  useEffect(() => {
    const interval = setInterval(() => {
      setDate((prev) => new Date(add(prev, { days: 10, years: 1 })));
    }, 2000);
    return () => clearInterval(interval);
  }, []);
  return <DatePicker title="UseDate" date={date} />;
};

function DynamicForm() {
  const form = useForm(schema as any, "form");
  const [state, setState] = useState({});
  const [items, setItems] = useState<number[]>([0]);

  const onSubmit = (event: FormEvent<HTMLFormElement>, args: Args) => {
    console.log(args);
    event.preventDefault();
    setState(args.data);
  };

  return (
    <Card>
      <form
        name="form"
        id="form"
        onSubmit={form.onSubmit(onSubmit)}
        onInvalid={form.onInvalid((e) => console.log(e))}
      />
      <div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
        <Input
          {...form.input("name", { title: "Name", placeholder: "Fulano" })}
        />
        <Input
          {...form.input("number", {
            title: "Number",
            placeholder: "000",
          })}
        />
        <Input
          {...form.input("surname", {
            title: "Surname",
            placeholder: "Silva",
          })}
        />
        <Input
          {...form.input("document", {
            title: "Document - CPF",
            placeholder: "000.000.000-00",
          })}
          mask="cpf"
        />
        <Select
          {...form.select("main", {
            title: "Language",
            placeholder: "Select a language",
            options: languages,
          })}
        />
        <MultiSelect
          {...form.multiselect("languages", {
            title: "All language",
            options: languages,
            placeholder: "Select your languages",
          })}
        />
        <Autocomplete
          {...form.select("secondary", {
            title: "Second language",
            placeholder: "Select a language",
            onChange: (e: any) => console.log("CUSTOM", e.target.value),
            options: languages,
          })}
        />
        <DatePicker
          {...form.datepicker("date", { title: "Date", clickToClose: true })}
        />
        <DatePicker
          {...form.datepicker("datetime", {
            title: "Date",
            type: "datetime",
            datetimeTitle: "Localtime",
            onChange: (d: any) => console.log(d),
          })}
        />
        <DatePickerControlled />
        <Switch
          {...form.checkbox("agree")}
          container="md:col-span-2 lg:col-span-3"
        >
          Accept the terms - Switch
        </Switch>
        <Checkbox
          {...form.checkbox("boolean")}
          container="md:col-span-2 lg:col-span-3"
        >
          Accept the terms - Checkbox
        </Checkbox>
      </div>
      <ul className="my-4 space-y-4">
        {items.map((item) => (
          <li
            key={item}
            className="flex flex-nowrap gap-8 items-baseline align-baseline"
          >
            <Input
              {...form.input(`items[${item}].name`)}
              title={`Name - ${item}`}
            />
            <Input
              {...form.input(`items[${item}].document`, { mask: "cpf" })}
              title={`Documento - ${item}`}
            />
            <Input
              {...form.input(`oab[${item}].document`, { mask: oabMask })}
              title={`Oab - ${item}`}
            />
          </li>
        ))}
        <li>
          <Button
            onClick={() => setItems((prev) => prev.concat(prev.at(-1)! + 1))}
          >
            Add other item
          </Button>
        </li>
      </ul>
      <div className="flex items-center">
        <Button form={form.name} type="submit">
          Submit
        </Button>
        <Input title="ok" hiddenLabel />
      </div>
    </Card>
  );
}

Form Debug State

Displays the current state of the form object, useful for debugging and understanding form data flow.

Debug - User:
{
  "state": {},
  "errors": null,
  "name": "form",
  "disabled": false
}
tsx
"use client";
import { z } from "zod";
import { useForm } from "@g4rcez/components";

const schema = z.object({
  name: z.string().min(1),
  // ... other schema fields
});

function FormDebug() {
  const form = useForm(schema, "debug-form");
  const [state, setState] = useState({}); // Example state to show submission

  return (
    <Card title={`Debug - User: ${form.get("name")}`}>
      <pre className="mt-10">
        <code>{JSON.stringify({ ...form, state }, null, 2)}</code>
      </pre>
    </Card>
  );
}

When to use useForm

Use when

You need the documented useForm interaction or presentation in a product workflow.

Avoid when

A native HTML element already provides the useForm behavior you need.

Accessibility

Keep the useForm labeled and use its documented keyboard interaction. The component preserves visible focus and communicates state without relying on color alone.

  • Keep keyboard focus visible and follow the documented key commands.
  • Provide an accessible name and connect descriptions or errors to the control.