stepper-validation-demo

PreviousNext
Docs
diceuiexample

Preview

Loading preview…
examples/stepper-validation-demo.tsx
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import * as React from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
  Stepper,
  StepperContent,
  StepperDescription,
  StepperIndicator,
  StepperItem,
  StepperList,
  StepperNext,
  StepperPrev,
  type StepperProps,
  StepperSeparator,
  StepperTitle,
  StepperTrigger,
} from "@/registry/default/ui/stepper";

const formSchema = z.object({
  username: z
    .string()
    .min(3, "Username must be at least 3 characters")
    .regex(
      /^[a-zA-Z0-9_]+$/,
      "Username can only contain letters, numbers, and underscores",
    ),
  email: z.email("Please enter a valid email address"),
  firstName: z.string().min(1, "First name is required"),
  lastName: z.string().min(1, "Last name is required"),
  bio: z.string().min(10, "Bio must be at least 10 characters"),
});

type FormSchema = z.infer<typeof formSchema>;

const steps = [
  {
    value: "account",
    title: "Account Setup",
    description: "Create your account",
    fields: ["username", "email"] as const,
  },
  {
    value: "profile",
    title: "Profile Info",
    description: "Complete your profile",
    fields: ["firstName", "lastName", "bio"] as const,
  },
  {
    value: "review",
    title: "Review",
    description: "Review your information",
    fields: [] as const,
  },
];

export default function StepperValidationDemo() {
  const [step, setStep] = React.useState("account");

  const form = useForm<FormSchema>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      email: "",
      firstName: "",
      lastName: "",
      bio: "",
    },
  });

  const stepIndex = React.useMemo(
    () => steps.findIndex((s) => s.value === step),
    [step],
  );

  const onValidate: NonNullable<StepperProps["onValidate"]> = React.useCallback(
    async (_value, direction) => {
      if (direction === "prev") return true;

      const stepData = steps.find((s) => s.value === step);
      if (!stepData) return true;

      const isValid = await form.trigger(stepData.fields);

      if (!isValid) {
        toast.info("Please complete all required fields to continue", {
          description: "Fix the validation errors and try again.",
        });
      }

      return isValid;
    },
    [form, step],
  );

  const onSubmit = React.useCallback((input: FormSchema) => {
    toast.success(
      <pre className="w-full">{JSON.stringify(input, null, 2)}</pre>,
    );
  }, []);

  return (
    <Form {...form}>
      <form className="w-full" onSubmit={form.handleSubmit(onSubmit)}>
        <Stepper value={step} onValueChange={setStep} onValidate={onValidate}>
          <StepperList>
            {steps.map((step) => (
              <StepperItem key={step.value} value={step.value}>
                <StepperTrigger>
                  <StepperIndicator />
                  <div className="flex flex-col gap-1">
                    <StepperTitle>{step.title}</StepperTitle>
                    <StepperDescription>{step.description}</StepperDescription>
                  </div>
                </StepperTrigger>
                <StepperSeparator className="mx-4" />
              </StepperItem>
            ))}
          </StepperList>
          <StepperContent
            value="account"
            className="flex flex-col gap-4 rounded-md border bg-card p-4 text-card-foreground"
          >
            <FormField
              control={form.control}
              name="username"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Username</FormLabel>
                  <FormControl>
                    <Input placeholder="Enter username" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <FormField
              control={form.control}
              name="email"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Email</FormLabel>
                  <FormControl>
                    <Input type="email" placeholder="Enter email" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
          </StepperContent>
          <StepperContent
            value="profile"
            className="flex flex-col gap-4 rounded-md border bg-card p-4 text-card-foreground"
          >
            <div className="grid grid-cols-2 gap-4">
              <FormField
                control={form.control}
                name="firstName"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>First Name</FormLabel>
                    <FormControl>
                      <Input placeholder="Enter first name" {...field} />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />
              <FormField
                control={form.control}
                name="lastName"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Last Name</FormLabel>
                    <FormControl>
                      <Input placeholder="Enter last name" {...field} />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />
            </div>
            <FormField
              control={form.control}
              name="bio"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Bio</FormLabel>
                  <FormControl>
                    <Textarea
                      placeholder="Tell us about yourself..."
                      className="min-h-[80px]"
                      {...field}
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
          </StepperContent>
          <StepperContent
            value="review"
            className="grid grid-cols-2 gap-4 rounded-md border bg-card p-4 text-card-foreground lg:grid-cols-3"
          >
            <div className="flex flex-col gap-1 rounded-md border p-2">
              <span className="font-medium text-sm">Username</span>
              <p className="text-sm">
                {form.watch("username") ?? "Not provided"}
              </p>
            </div>
            <div className="flex flex-col gap-1 rounded-md border p-2">
              <span className="font-medium text-sm">Email</span>
              <p className="text-sm">{form.watch("email") ?? "Not provided"}</p>
            </div>
            <div className="flex flex-col gap-1 rounded-md border p-2">
              <span className="font-medium text-sm">First Name</span>
              <p className="text-sm">
                {form.watch("firstName") ?? "Not provided"}
              </p>
            </div>
            <div className="flex flex-col gap-1 rounded-md border p-2">
              <span className="font-medium text-sm">Last Name</span>
              <p className="text-sm">
                {form.watch("lastName") ?? "Not provided"}
              </p>
            </div>
            <div className="flex flex-col gap-1 rounded-md border p-2">
              <span className="font-medium text-sm">Bio</span>
              <p className="text-sm">{form.watch("bio") ?? "Not provided"}</p>
            </div>
          </StepperContent>
          <div className="flex justify-between">
            <StepperPrev asChild>
              <Button type="button" variant="outline">
                Previous
              </Button>
            </StepperPrev>
            <div className="text-muted-foreground text-sm">
              Step {stepIndex + 1} of {steps.length}
            </div>
            {stepIndex === steps.length - 1 ? (
              <Button type="submit">Complete Setup</Button>
            ) : (
              <StepperNext asChild>
                <Button>Next</Button>
              </StepperNext>
            )}
          </div>
        </Stepper>
      </form>
    </Form>
  );
}

Installation

npx shadcn@latest add @diceui/stepper-validation-demo

Usage

import { StepperValidationDemo } from "@/components/stepper-validation-demo"
<StepperValidationDemo />