Getting Started
Blocks
Edit Profile
Update your profile information with automatic validation
"use client"
import { toast } from "sonner"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { ExtendedForm, useZodForm } from "@/components/ui/extended-form"
const profileSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
username: z.string().min(3, "Username must be at least 3 characters"),
bio: z.string().max(160, "Bio must be less than 160 characters"),
})
export function ExtendedFormDemo() {
const form = useZodForm({
schema: profileSchema,
defaultValues: {
name: "Melvyn Malherbe",
username: "melvynx",
bio: "Full-stack developer passionate about building great products",
},
})
const onSubmit = async (data: z.infer<typeof profileSchema>) => {
await new Promise((resolve) => setTimeout(resolve, 1500))
toast.success("Profile updated successfully!")
console.log(data)
}
return (
<Card className="p-6">
<div className="mb-6">
<h3 className="text-lg font-semibold">Edit Profile</h3>
<p className="text-muted-foreground text-sm">
Update your profile information with automatic validation
</p>
</div>
<ExtendedForm form={form} onSubmit={onSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input id="name" {...form.register("name")} />
{form.formState.errors.name && (
<p className="text-destructive text-sm">
{form.formState.errors.name.message}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Input id="username" {...form.register("username")} />
{form.formState.errors.username && (
<p className="text-destructive text-sm">
{form.formState.errors.username.message}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="bio">Bio</Label>
<Input
id="bio"
{...form.register("bio")}
placeholder="Tell us about yourself"
/>
{form.formState.errors.bio && (
<p className="text-destructive text-sm">
{form.formState.errors.bio.message}
</p>
)}
</div>
<div className="flex gap-2">
<Button
type="submit"
disabled={form.formState.isSubmitting}
className="flex-1"
>
{form.formState.isSubmitting ? "Saving..." : "Save Changes"}
</Button>
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
</div>
</ExtendedForm>
</Card>
)
}
Installation
pnpm dlx shadcn@latest add https://ui.nowts.app/r/extended-form.json
Usage
The ExtendedForm
component provides type-safe forms with Zod validation:
import { z } from "zod"
import { ExtendedForm, useZodForm } from "@/components/ui/extended-form"
const schema = z.object({
name: z.string().min(2),
email: z.string().email(),
})
export function MyForm() {
const form = useZodForm({
schema,
defaultValues: {
name: "",
email: "",
},
})
const onSubmit = async (data: z.infer<typeof schema>) => {
console.log(data) // Fully typed!
}
return (
<ExtendedForm form={form} onSubmit={onSubmit}>
<input {...form.register("name")} />
<input {...form.register("email")} />
<button type="submit">Submit</button>
</ExtendedForm>
)
}
Features
Automatic Fieldset Management
The form automatically wraps your inputs in a <fieldset>
that:
- Disables all inputs during form submission
- Can be manually disabled via the
disabled
prop - Prevents duplicate submissions
Type Safety
The useZodForm
hook provides full TypeScript support:
const form = useZodForm({ schema })
// form is fully typed based on your Zod schema
Form State
Access form state from the hook:
const form = useZodForm({ schema })
console.log(form.formState.isSubmitting) // boolean
console.log(form.formState.errors) // typed errors
console.log(form.formState.isDirty) // boolean
With shadcn/ui Form
You can combine with shadcn/ui's Form components:
import { ExtendedForm, useZodForm } from "@/components/ui/extended-form"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
export function MyForm() {
const form = useZodForm({ schema })
return (
<ExtendedForm form={form} onSubmit={onSubmit}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
</FormItem>
)}
/>
</ExtendedForm>
)
}
Props
ExtendedForm
Prop | Type | Required | Description |
---|---|---|---|
form | UseFormReturn | Yes | Form instance from useZodForm |
onSubmit | SubmitHandler | Yes | Form submission handler |
disabled | boolean | No | Manually disable the form |
...formProps | FormHTMLAttributes | No | Standard form attributes |
useZodForm
Prop | Type | Required | Description |
---|---|---|---|
schema | ZodType | Yes | Zod validation schema |
...formProps | UseFormProps | No | react-hook-form options |
Build Your SaaS in Days, Not Months
NOW.TS is the Next.js 15 boilerplate with everything you need to launch your SaaS—auth, payments, database, and AI-ready infrastructure.
Learn more about NOW.TS