feat: add docs and examples for react-hook-form and tanstack form (#8412)

* feat: add next forms docs

* feat

* docs: rhf and tsf

* docs: forms

* feat: update react-hook-form docs

* feat: update docs for both lib

* docs: update tanstack docs

* docs: update

* fix

* fix

* fix

* add forms link in sidebar
This commit is contained in:
shadcn
2025-10-10 21:29:30 +04:00
committed by GitHub
parent f494411953
commit c67e630521
96 changed files with 10179 additions and 5902 deletions

View File

@@ -9,12 +9,14 @@ export function ComponentPreviewTabs({
className,
align = "center",
hideCode = false,
chromeLessOnMobile = false,
component,
source,
...props
}: React.ComponentProps<"div"> & {
align?: "center" | "start" | "end"
hideCode?: boolean
chromeLessOnMobile?: boolean
component: React.ReactNode
source: React.ReactNode
}) {
@@ -51,7 +53,8 @@ export function ComponentPreviewTabs({
</Tabs>
<div
data-tab={tab}
className="data-[tab=code]:border-code relative rounded-lg border md:-mx-1"
data-chrome-less-on-mobile={chromeLessOnMobile}
className="data-[tab=code]:border-code relative rounded-lg border data-[chrome-less-on-mobile=true]:border-0 sm:data-[chrome-less-on-mobile=true]:border md:-mx-1"
>
<div
data-slot="preview"
@@ -61,7 +64,8 @@ export function ComponentPreviewTabs({
<div
data-align={align}
className={cn(
"preview flex h-[450px] w-full justify-center p-10 data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start"
"preview flex w-full justify-center data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start",
chromeLessOnMobile ? "sm:p-10" : "h-[450px] p-10"
)}
>
{component}

View File

@@ -10,6 +10,7 @@ export function ComponentPreview({
className,
align = "center",
hideCode = false,
chromeLessOnMobile = false,
...props
}: React.ComponentProps<"div"> & {
name: string
@@ -17,12 +18,13 @@ export function ComponentPreview({
description?: string
hideCode?: boolean
type?: "block" | "component" | "example"
chromeLessOnMobile?: boolean
}) {
const Component = Index[name]?.component
if (!Component) {
return (
<p className="text-muted-foreground text-sm">
<p className="text-muted-foreground mt-6 text-sm">
Component{" "}
<code className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm">
{name}
@@ -63,6 +65,7 @@ export function ComponentPreview({
hideCode={hideCode}
component={<Component />}
source={<ComponentSource name={name} collapsible={false} />}
chromeLessOnMobile={chromeLessOnMobile}
{...props}
/>
)

View File

@@ -43,6 +43,14 @@ export async function ComponentSource({
return null
}
// Fix imports.
// Replace @/registry/new-york-v4/ with @/components/.
code = code.replaceAll("@/registry/new-york-v4/", "@/components/")
// Replace export default with export.
code = code.replaceAll("export default", "export")
code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "")
const lang = language ?? title?.split(".").pop() ?? "tsx"
const highlightedCode = await highlightCode(code, lang)

View File

@@ -31,6 +31,10 @@ const TOP_LEVEL_SECTIONS = [
name: "MCP Server",
href: "/docs/mcp",
},
{
name: "Forms",
href: "/docs/forms",
},
{
name: "Changelog",
href: "/docs/changelog",

View File

@@ -29,6 +29,10 @@ const TOP_LEVEL_SECTIONS = [
name: "MCP Server",
href: "/docs/mcp",
},
{
name: "Forms",
href: "/docs/forms",
},
{
name: "Changelog",
href: "/docs/changelog",

View File

@@ -56,9 +56,3 @@ import { Checkbox } from "@/components/ui/checkbox"
```tsx
<Checkbox />
```
## Examples
### Form
<ComponentPreview name="checkbox-form-multiple" />

View File

@@ -143,7 +143,3 @@ export function ExampleCombobox() {
You can create a responsive combobox by using the `<Popover />` on desktop and the `<Drawer />` components on mobile.
<ComponentPreview name="combobox-responsive" />
### Form
<ComponentPreview name="combobox-form" />

View File

@@ -94,7 +94,3 @@ This component uses the `chrono-node` library to parse natural language dates.
title="Natural Language Picker"
description="A calendar with natural language picker."
/>
### Form
<ComponentPreview name="date-picker-form" />

View File

@@ -98,6 +98,10 @@ The `Field` family is designed for composing accessible forms. A typical field i
- `FieldContent` is a flex column that groups label and description. Not required if you have no description.
- Wrap related fields with `FieldGroup`, and use `FieldSet` with `FieldLegend` for semantic grouping.
## Form
See the [Form](/docs/forms) documentation for building forms with the `Field` component and [React Hook Form](/docs/forms/react-hook-form) or [Tanstack Form](/docs/forms/tanstack-form).
## Examples
### Input

View File

@@ -1,10 +1,18 @@
---
title: React Hook Form
title: Form
description: Building forms with React Hook Form and Zod.
links:
doc: https://react-hook-form.com
---
import { InfoIcon } from "lucide-react"
<Callout icon={<InfoIcon />} title="We are not actively developing this component anymore.">
The Form component is an abstraction over the `react-hook-form` library. Going forward, we recommend using the [`<Field />`](/docs/components/field) component to build forms. See the [Form](/docs/form) documentation for more information.
</Callout>
Forms are tricky. They are one of the most common things you'll build in a web application, but also one of the most complex.
Well-designed HTML forms are:
@@ -119,8 +127,6 @@ npm install @radix-ui/react-label @radix-ui/react-slot react-hook-form @hookform
## Usage
<Steps>
### Create a form schema
Define the shape of your form using a Zod schema. You can read more about using Zod in the [Zod documentation](https://zod.dev).
@@ -233,23 +239,3 @@ export function ProfileForm() {
### Done
That's it. You now have a fully accessible form that is type-safe with client-side validation.
<ComponentPreview
name="input-form"
className="[&_[role=tablist]]:hidden [&>div>div:first-child]:hidden"
/>
</Steps>
## Examples
See the following links for more examples on how to use the `<Form />` component with other components:
- [Checkbox](/docs/components/checkbox#form)
- [Date Picker](/docs/components/date-picker#form)
- [Input](/docs/components/input#form)
- [Radio Group](/docs/components/radio-group#form)
- [Select](/docs/components/select#form)
- [Switch](/docs/components/switch#form)
- [Textarea](/docs/components/textarea#form)
- [Combobox](/docs/components/combobox#form)

View File

@@ -94,10 +94,6 @@ import { Input } from "@/components/ui/input"
description="An input component with a button."
/>
### Form
<ComponentPreview name="input-form" />
## Changelog
### 2025-09-18 Remove `flex` class

View File

@@ -69,9 +69,3 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
</div>
</RadioGroup>
```
## Examples
### Form
<ComponentPreview name="radio-group-form" />

View File

@@ -84,7 +84,3 @@ import {
name="select-scrollable"
description="A select component with a scrollable list of options."
/>
### Form
<ComponentPreview name="select-form" />

View File

@@ -56,9 +56,3 @@ import { Switch } from "@/components/ui/switch"
```tsx
<Switch />
```
## Examples
### Form
<ComponentPreview name="switch-form" />

View File

@@ -79,7 +79,3 @@ import { Textarea } from "@/components/ui/textarea"
name="textarea-with-button"
description="A textarea with a button"
/>
### Form
<ComponentPreview name="textarea-form" />

View File

@@ -0,0 +1,45 @@
---
title: Forms
description: Build forms with React and shadcn/ui.
---
import { ClipboardListIcon, InfoIcon } from "lucide-react"
## Pick Your Framework
Start by selecting your framework. Then follow the instructions to learn how to build forms with shadcn/ui and the form library of your choice.
<div className="mt-8 grid gap-4 sm:grid-cols-2 sm:gap-6">
<LinkedCard href="/docs/forms/react-hook-form">
<ClipboardListIcon className="size-10" />
<p className="mt-2 font-medium">React Hook Form</p>
</LinkedCard>
<LinkedCard href="/docs/forms/tanstack-form">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className="size-10"
fill="currentColor"
>
<path d="M6.93 13.688a.343.343 0 0 1 .468.132l.063.106c.48.851.98 1.66 1.5 2.426a35.65 35.65 0 0 0 2.074 2.742.345.345 0 0 1-.039.484l-.074.066c-2.543 2.223-4.191 2.665-4.953 1.333-.746-1.305-.477-3.672.808-7.11a.344.344 0 0 1 .153-.18ZM17.75 16.3a.34.34 0 0 1 .395.27l.02.1c.628 3.286.187 4.93-1.325 4.93-1.48 0-3.36-1.402-5.649-4.203a.327.327 0 0 1-.074-.222c0-.188.156-.34.344-.34h.121a32.984 32.984 0 0 0 2.809-.098c1.07-.086 2.191-.23 3.359-.437zm.871-6.977a.353.353 0 0 1 .445-.21l.102.034c3.262 1.11 4.504 2.332 3.719 3.664-.766 1.305-2.993 2.254-6.684 2.848a.362.362 0 0 1-.238-.047.343.343 0 0 1-.125-.476l.062-.106a34.07 34.07 0 0 0 1.367-2.523c.477-.989.93-2.051 1.352-3.184zM7.797 8.34a.362.362 0 0 1 .238.047.343.343 0 0 1 .125.476l-.062.106a34.088 34.088 0 0 0-1.367 2.523c-.477.988-.93 2.051-1.352 3.184a.353.353 0 0 1-.445.21l-.102-.034C1.57 13.742.328 12.52 1.113 11.188 1.88 9.883 4.106 8.934 7.797 8.34Zm5.281-3.984c2.543-2.223 4.192-2.664 4.953-1.332.746 1.304.477 3.671-.808 7.109a.344.344 0 0 1-.153.18.343.343 0 0 1-.468-.133l-.063-.106a34.64 34.64 0 0 0-1.5-2.426 35.65 35.65 0 0 0-2.074-2.742.345.345 0 0 1 .039-.484ZM7.285 2.274c1.48 0 3.364 1.402 5.649 4.203a.349.349 0 0 1 .078.218.348.348 0 0 1-.348.344l-.117-.004a34.584 34.584 0 0 0-2.809.102 35.54 35.54 0 0 0-3.363.437.343.343 0 0 1-.394-.273l-.02-.098c-.629-3.285-.188-4.93 1.324-4.93Zm2.871 5.812h3.688a.638.638 0 0 1 .55.316l1.848 3.22a.644.644 0 0 1 0 .628l-1.847 3.223a.638.638 0 0 1-.551.316h-3.688a.627.627 0 0 1-.547-.316L7.758 12.25a.644.644 0 0 1 0-.629L9.61 8.402a.627.627 0 0 1 .546-.316Zm3.23.793a.638.638 0 0 1 .552.316l1.39 2.426a.644.644 0 0 1 0 .629l-1.39 2.43a.638.638 0 0 1-.551.316h-2.774a.627.627 0 0 1-.546-.316l-1.395-2.43a.644.644 0 0 1 0-.629l1.395-2.426a.627.627 0 0 1 .546-.316Zm-.491.867h-1.79a.624.624 0 0 0-.546.316l-.899 1.56a.644.644 0 0 0 0 .628l.899 1.563a.632.632 0 0 0 .547.316h1.789a.632.632 0 0 0 .547-.316l.898-1.563a.644.644 0 0 0 0-.629l-.898-1.558a.624.624 0 0 0-.547-.317Zm-.477.828c.227 0 .438.121.547.317l.422.73a.625.625 0 0 1 0 .629l-.422.734a.627.627 0 0 1-.547.317h-.836a.632.632 0 0 1-.547-.317l-.422-.734a.625.625 0 0 1 0-.629l.422-.73a.632.632 0 0 1 .547-.317zm-.418.817a.548.548 0 0 0-.473.273.547.547 0 0 0 0 .547.544.544 0 0 0 .473.27.544.544 0 0 0 .473-.27.547.547 0 0 0 0-.547.548.548 0 0 0-.473-.273Zm-4.422.546h.98M18.98 7.75c.391-1.895.477-3.344.223-4.398-.148-.63-.422-1.137-.84-1.508-.441-.39-1-.582-1.625-.582-1.035 0-2.12.472-3.281 1.367a14.9 14.9 0 0 0-1.473 1.316 1.206 1.206 0 0 0-.136-.144c-1.446-1.285-2.66-2.082-3.7-2.39-.617-.184-1.195-.2-1.722-.024-.559.187-1.004.574-1.317 1.117-.515.894-.652 2.074-.46 3.527.078.59.214 1.235.402 1.934a1.119 1.119 0 0 0-.215.047C3.008 8.62 1.71 9.269.926 10.015c-.465.442-.77.938-.883 1.481-.113.578 0 1.156.312 1.7.516.894 1.465 1.597 2.817 2.155.543.223 1.156.426 1.844.61a1.023 1.023 0 0 0-.07.226c-.391 1.891-.477 3.344-.223 4.395.148.629.425 1.14.84 1.508.44.39 1 .582 1.625.582 1.035 0 2.12-.473 3.28-1.364.477-.37.973-.816 1.489-1.336a1.2 1.2 0 0 0 .195.227c1.446 1.285 2.66 2.082 3.7 2.39.617.184 1.195.2 1.722.024.559-.187 1.004-.574 1.317-1.117.515-.894.652-2.074.46-3.527a14.941 14.941 0 0 0-.425-2.012 1.225 1.225 0 0 0 .238-.047c1.828-.61 3.125-1.258 3.91-2.004.465-.441.77-.937.883-1.48.113-.578 0-1.157-.313-1.7-.515-.894-1.464-1.597-2.816-2.156a14.576 14.576 0 0 0-1.906-.625.865.865 0 0 0 .059-.195z" />
</svg>
<p className="mt-2 font-medium">TanStack Form</p>
</LinkedCard>
<LinkedCard href="#" className="border border-dashed bg-transparent">
<svg
role="img"
viewBox="0 0 24 24"
className="size-10"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<title>React</title>
<path
d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z"
/>
</svg>
<p className="mt-2 font-medium">useActionState</p>
<p className="text-muted-foreground mt-1 text-xs">(Coming Soon)</p>
</LinkedCard>
</div>

View File

@@ -0,0 +1,3 @@
{
"pages": ["react-hook-form", "tanstack-form"]
}

View File

@@ -0,0 +1,397 @@
---
title: Next.js
description: Build forms in React using useActionState and Server Actions.
---
import { InfoIcon } from "lucide-react"
In this guide, we will take a look at building forms with Next.js using `useActionState` and Server Actions. We'll cover building forms, validation, pending states, accessibility, and more.
## Demo
We are going to build the following form with a simple text input and a textarea. On submit, we'll use a server action to validate the form data and update the form state.
<ComponentPreview
name="form-next-demo"
className="[&_.preview]:h-[700px] [&_pre]:!h-[700px]"
/>
<Callout icon={<InfoIcon />}>
**Note:** The examples on this page intentionally disable browser validation
to show how schema validation and form errors work in server actions.
</Callout>
## Approach
This form leverages Next.js and React's built-in capabilities for form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
- Uses Next.js `<Form />` component for navigation and progressive enhancement.
- `<Field />` components for building accessible forms.
- `useActionState` for managing form state and errors.
- Handles loading states with pending prop.
- Server Actions for handling form submissions.
- Server-side validation using Zod.
## Anatomy
Here's a basic example of a form using the `<Field />` component.
```tsx showLineNumbers
<Form action={formAction}>
<FieldGroup>
<Field data-invalid={!!formState.errors?.title?.length}>
<FieldLabel htmlFor="title">Bug Title</FieldLabel>
<Input
id="title"
name="title"
defaultValue={formState.values.title}
disabled={pending}
aria-invalid={!!formState.errors?.title?.length}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
{formState.errors?.title && (
<FieldError>{formState.errors.title[0]}</FieldError>
)}
</Field>
</FieldGroup>
<Button type="submit">Submit</Button>
</Form>
```
## Usage
### Create a form schema
We'll start by defining the shape of our form using a Zod schema in a `schema.ts` file.
<Callout icon={<InfoIcon />}>
**Note:** This example uses `zod v3` for schema validation, but you can
replace it with any other schema validation library. Make sure your schema
library conforms to the Standard Schema specification.
</Callout>
```tsx showLineNumbers title="schema.ts"
import { z } from "zod"
export const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
```
### Define the form state type
Next, we'll create a type for our form state that includes values, errors, and success status. This will be used to type the form state on the client and server.
```tsx showLineNumbers title="schema.ts"
import { z } from "zod"
export type FormState = {
values?: z.infer<typeof formSchema>
errors: null | Partial<Record<keyof z.infer<typeof formSchema>, string[]>>
success: boolean
}
```
**Important:** We define the schema and the `FormState` type in a separate file so we can import them into both the client and server components.
### Create the Server Action
A server action is a function that runs on the server and can be called from the client. We'll use it to validate the form data and update the form state.
<ComponentSource
src="/registry/new-york-v4/examples/form-next-demo-action.ts"
title="actions.ts"
/>
**Note:** We're returning `values` for error cases. This is because we want to keep the user submitted values in the form state. For success cases, we're returning empty values to reset the form.
### Build the form
We can now build the form using the `<Field />` component. We'll use the `useActionState` hook to manage the form state, server action, and pending state.
<ComponentSource
src="/registry/new-york-v4/examples/form-next-demo.tsx"
title="form.tsx"
/>
### Done
That's it. You now have a fully accessible form with client and server-side validation.
When you submit the form, the `formAction` function will be called on the server. The server action will validate the form data and update the form state.
If the form data is invalid, the server action will return the errors to the client. If the form data is valid, the server action will return the success status and update the form state.
## Pending States
Use the `pending` prop from `useActionState` to show loading indicators and disable form inputs.
```tsx showLineNumbers {11,26-34}
"use client"
import * as React from "react"
import Form from "next/form"
import { Spinner } from "@/components/ui/spinner"
import { bugReportFormAction } from "./actions"
export function BugReportForm() {
const [formState, formAction, pending] = React.useActionState(
bugReportFormAction,
{
errors: null,
success: false,
}
)
return (
<Form action={formAction}>
<FieldGroup>
<Field data-disabled={pending}>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input id="name" name="name" disabled={pending} />
</Field>
<Field>
<Button type="submit" disabled={pending}>
{pending && <Spinner />} Submit
</Button>
</Field>
</FieldGroup>
</Form>
)
}
```
## Disabled States
### Submit Button
To disable the submit button, use the `pending` prop on the button's `disabled` prop.
```tsx showLineNumbers
<Button type="submit" disabled={pending}>
{pending && <Spinner />} Submit
</Button>
```
### Field
To apply a disabled state and styling to a `<Field />` component, use the `data-disabled` prop on the `<Field />` component.
```tsx showLineNumbers
<Field data-disabled={pending}>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input id="name" name="name" disabled={pending} />
</Field>
```
## Validation
### Server-side Validation
Use `safeParse()` on your schema in your server action to validate the form data.
```tsx showLineNumbers title="actions.ts" {12-20}
"use server"
export async function bugReportFormAction(
_prevState: FormState,
formData: FormData
) {
const values = {
title: formData.get("title") as string,
description: formData.get("description") as string,
}
const result = formSchema.safeParse(values)
if (!result.success) {
return {
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
return {
errors: null,
success: true,
}
}
```
### Business Logic Validation
You can add additional custom validation logic in your server action.
Make sure to return the values on validation errors. This is to ensure that the form state maintains the user's input.
```tsx showLineNumbers title="actions.ts" {22-35}
"use server"
export async function bugReportFormAction(
_prevState: FormState,
formData: FormData
) {
const values = {
title: formData.get("title") as string,
description: formData.get("description") as string,
}
const result = formSchema.safeParse(values)
if (!result.success) {
return {
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
// Check if email already exists in database.
const existingUser = await db.user.findUnique({
where: { email: result.data.email },
})
if (existingUser) {
return {
values,
success: false,
errors: {
email: ["This email is already registered"],
},
}
}
return {
errors: null,
success: true,
}
}
```
## Displaying Errors
Display errors next to the field using `<FieldError />`. Make sure to add the `data-invalid` prop to the `<Field />` component and `aria-invalid` prop to the input.
```tsx showLineNumbers
<Field data-invalid={!!formState.errors?.email?.length}>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
name="email"
type="email"
aria-invalid={!!formState.errors?.email?.length}
/>
{formState.errors?.email && (
<FieldError>{formState.errors.email[0]}</FieldError>
)}
</Field>
```
## Resetting the Form
When you submit a form with a server action, React will automatically reset the form state to the initial values.
### Reset on Success
To reset the form on success, you can omit the `values` from the server action and React will automatically reset the form state to the initial values. This is standard React behavior.
```tsx showLineNumbers title="actions.ts" {22-26}
export async function demoFormAction(
_prevState: FormState,
formData: FormData
) {
const values = {
title: formData.get("title") as string,
description: formData.get("description") as string,
}
// Validation.
if (!result.success) {
return {
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
// Business logic.
callYourDatabaseOrAPI(values)
// Omit the values on success to reset the form state.
return {
errors: null,
success: true,
}
}
```
### Preserve on Validation Errors
To prevent the form from being reset on failure, you can return the values in the server action. This is to ensure that the form state maintains the user's input.
```tsx showLineNumbers title="actions.ts" {12-17}
export async function demoFormAction(
_prevState: FormState,
formData: FormData
) {
const values = {
title: formData.get("title") as string,
description: formData.get("description") as string,
}
// Validation.
if (!result.success) {
return {
// Return the values on validation errors.
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
}
```
## Complex Forms
Here is an example of a more complex form with multiple fields and validation.
<ComponentPreview
name="form-next-complex"
className="[&_.preview]:h-[1100px] [&_pre]:!h-[1100px]"
hideCode
/>
### Schema
<ComponentSource
src="/registry/new-york-v4/examples/form-next-complex-schema.ts"
title="schema.ts"
/>
### Form
<ComponentSource
src="/registry/new-york-v4/examples/form-next-complex.tsx"
title="form.tsx"
/>
### Server Action
<ComponentSource
src="/registry/new-york-v4/examples/form-next-complex-action.ts"
title="actions.ts"
/>

View File

@@ -0,0 +1,629 @@
---
title: React Hook Form
description: Build forms in React using React Hook Form and Zod.
links:
doc: https://react-hook-form.com
---
import { InfoIcon } from "lucide-react"
In this guide, we will take a look at building forms with React Hook Form. We'll cover building forms with the `<Field />` component, adding schema validation using Zod, error handling, accessibility, and more.
## Demo
We are going to build the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
<Callout icon={<InfoIcon />}>
**Note:** For the purpose of this demo, we have intentionally disabled browser
validation to show how schema validation and form errors work in React Hook
Form. It is recommended to add basic browser validation in your production
code.
</Callout>
<ComponentPreview
name="form-rhf-demo"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
## Approach
This form leverages React Hook Form for performant, flexible form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
- Uses React Hook Form's `useForm` hook for form state management.
- `<Controller />` component for controlled inputs.
- `<Field />` components for building accessible forms.
- Client-side validation using Zod with `zodResolver`.
## Anatomy
Here's a basic example of a form using the `<Controller />` component from React Hook Form and the `<Field />` component.
```tsx showLineNumbers {5-18}
<Controller
name="title"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
<Input
{...field}
id={field.name}
aria-invalid={fieldState.invalid}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
```
## Form
### Create a form schema
We'll start by defining the shape of our form using a Zod schema
<Callout icon={<InfoIcon />}>
**Note:** This example uses `zod v3` for schema validation, but you can
replace it with any other Standard Schema validation library supported by
React Hook Form.
</Callout>
```tsx showLineNumbers title="form.tsx"
import * as z from "zod"
const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
```
### Setup the form
Next, we'll use the `useForm` hook from React Hook Form to create our form instance. We'll also add the Zod resolver to validate the form data.
```tsx showLineNumbers title="form.tsx" {17-23}
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
export function BugReportForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
description: "",
},
})
function onSubmit(data: z.infer<typeof formSchema>) {
// Do something with the form values.
console.log(data)
}
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* ... */}
{/* Build the form here */}
{/* ... */}
</form>
)
}
```
### Build the form
We can now build the form using the `<Controller />` component from React Hook Form and the `<Field />` component.
<ComponentSource
src="/registry/new-york-v4/examples/form-rhf-demo.tsx"
title="form.tsx"
/>
### Done
That's it. You now have a fully accessible form with client-side validation.
When you submit the form, the `onSubmit` function will be called with the validated form data. If the form data is invalid, React Hook Form will display the errors next to each field.
## Validation
### Client-side Validation
React Hook Form validates your form data using the Zod schema. Define a schema and pass it to the `resolver` option of the `useForm` hook.
```tsx showLineNumbers title="example-form.tsx" {5-8,12}
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
const formSchema = z.object({
title: z.string(),
description: z.string().optional(),
})
export function ExampleForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
description: "",
},
})
}
```
### Validation Modes
React Hook Form supports different validation modes.
```tsx showLineNumbers title="form.tsx" {3}
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
mode: "onChange",
})
```
| Mode | Description |
| ------------- | -------------------------------------------------------- |
| `"onChange"` | Validation triggers on every change. |
| `"onBlur"` | Validation triggers on blur. |
| `"onSubmit"` | Validation triggers on submit (default). |
| `"onTouched"` | Validation triggers on first blur, then on every change. |
| `"all"` | Validation triggers on blur and change. |
## Displaying Errors
Display errors next to the field using `<FieldError />`. For styling and accessibility:
- Add the `data-invalid` prop to the `<Field />` component.
- Add the `aria-invalid` prop to the form control such as `<Input />`, `<SelectTrigger />`, `<Checkbox />`, etc.
```tsx showLineNumbers title="form.tsx" {5,11,13}
<Controller
name="email"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
<Input
{...field}
id={field.name}
type="email"
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
```
## Working with Different Field Types
### Input
- For input fields, spread the `field` object onto the `<Input />` component.
- To show errors, add the `aria-invalid` prop to the `<Input />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-rhf-input"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
For simple text inputs, spread the `field` object onto the input.
```tsx showLineNumbers title="form.tsx" {5,7,8}
<Controller
name="name"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
<Input {...field} id={field.name} aria-invalid={fieldState.invalid} />
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
```
### Textarea
- For textarea fields, spread the `field` object onto the `<Textarea />` component.
- To show errors, add the `aria-invalid` prop to the `<Textarea />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-rhf-textarea"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
For textarea fields, spread the `field` object onto the textarea.
```tsx showLineNumbers title="form.tsx" {5,10,18}
<Controller
name="about"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-rhf-textarea-about">More about you</FieldLabel>
<Textarea
{...field}
id="form-rhf-textarea-about"
aria-invalid={fieldState.invalid}
placeholder="I'm a software engineer..."
className="min-h-[120px]"
/>
<FieldDescription>
Tell us more about yourself. This will be used to help us personalize
your experience.
</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
```
### Select
- For select components, use `field.value` and `field.onChange` on the `<Select />` component.
- To show errors, add the `aria-invalid` prop to the `<SelectTrigger />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-rhf-select"
className="sm:[&_.preview]:h-[500px] sm:[&_pre]:!h-[500px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {5,13,22}
<Controller
name="language"
control={form.control}
render={({ field, fieldState }) => (
<Field orientation="responsive" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldLabel htmlFor="form-rhf-select-language">
Spoken Language
</FieldLabel>
<FieldDescription>
For best results, select the language you speak.
</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldContent>
<Select
name={field.name}
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger
id="form-rhf-select-language"
aria-invalid={fieldState.invalid}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</Field>
)}
/>
```
### Checkbox
- For checkbox arrays, use `field.value` and `field.onChange` with array manipulation.
- To show errors, add the `aria-invalid` prop to the `<Checkbox />` component and the `data-invalid` prop to the `<Field />` component.
- Remember to add `data-slot="checkbox-group"` to the `<FieldGroup />` component for proper styling and spacing.
<ComponentPreview
name="form-rhf-checkbox"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {10,15,20-22,38}
<Controller
name="tasks"
control={form.control}
render={({ field, fieldState }) => (
<FieldSet>
<FieldLegend variant="label">Tasks</FieldLegend>
<FieldDescription>
Get notified when tasks you&apos;ve created have updates.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{tasks.map((task) => (
<Field
key={task.id}
orientation="horizontal"
data-invalid={fieldState.invalid}
>
<Checkbox
id={`form-rhf-checkbox-${task.id}`}
name={field.name}
aria-invalid={fieldState.invalid}
checked={field.value.includes(task.id)}
onCheckedChange={(checked) => {
const newValue = checked
? [...field.value, task.id]
: field.value.filter((value) => value !== task.id)
field.onChange(newValue)
}}
/>
<FieldLabel
htmlFor={`form-rhf-checkbox-${task.id}`}
className="font-normal"
>
{task.label}
</FieldLabel>
</Field>
))}
</FieldGroup>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldSet>
)}
/>
```
### Radio Group
- For radio groups, use `field.value` and `field.onChange` on the `<RadioGroup />` component.
- To show errors, add the `aria-invalid` prop to the `<RadioGroupItem />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-rhf-radiogroup"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {12-13,17,25,31}
<Controller
name="plan"
control={form.control}
render={({ field, fieldState }) => (
<FieldSet>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup
name={field.name}
value={field.value}
onValueChange={field.onChange}
>
{plans.map((plan) => (
<FieldLabel key={plan.id} htmlFor={`form-rhf-radiogroup-${plan.id}`}>
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>{plan.description}</FieldDescription>
</FieldContent>
<RadioGroupItem
value={plan.id}
id={`form-rhf-radiogroup-${plan.id}`}
aria-invalid={fieldState.invalid}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldSet>
)}
/>
```
### Switch
- For switches, use `field.value` and `field.onChange` on the `<Switch />` component.
- To show errors, add the `aria-invalid` prop to the `<Switch />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-rhf-switch"
className="sm:[&_.preview]:h-[500px] sm:[&_pre]:!h-[500px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {5,13,18-19}
<Controller
name="twoFactor"
control={form.control}
render={({ field, fieldState }) => (
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldLabel htmlFor="form-rhf-switch-twoFactor">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldContent>
<Switch
id="form-rhf-switch-twoFactor"
name={field.name}
checked={field.value}
onCheckedChange={field.onChange}
aria-invalid={fieldState.invalid}
/>
</Field>
)}
/>
```
### Complex Forms
Here is an example of a more complex form with multiple fields and validation.
<ComponentPreview
name="form-rhf-complex"
className="sm:[&_.preview]:h-[1300px] sm:[&_pre]:!h-[1300px]"
chromeLessOnMobile
/>
## Resetting the Form
Use `form.reset()` to reset the form to its default values.
```tsx showLineNumbers
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
```
## Array Fields
React Hook Form provides a `useFieldArray` hook for managing dynamic array fields. This is useful when you need to add or remove fields dynamically.
<ComponentPreview
name="form-rhf-array"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
### Using useFieldArray
Use the `useFieldArray` hook to manage array fields. It provides `fields`, `append`, and `remove` methods.
```tsx showLineNumbers title="form.tsx" {8-11}
import { useFieldArray, useForm } from "react-hook-form"
export function ExampleForm() {
const form = useForm({
// ... form config
})
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "emails",
})
}
```
### Array Field Structure
Wrap your array fields in a `<FieldSet />` with a `<FieldLegend />` and `<FieldDescription />`.
```tsx showLineNumbers title="form.tsx"
<FieldSet className="gap-4">
<FieldLegend variant="label">Email Addresses</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup className="gap-4">{/* Array items go here */}</FieldGroup>
</FieldSet>
```
### Controller Pattern for Array Items
Map over the `fields` array and use `<Controller />` for each item. **Make sure to use `field.id` as the key**.
```tsx showLineNumbers title="form.tsx"
{
fields.map((field, index) => (
<Controller
key={field.id}
name={`emails.${index}.address`}
control={form.control}
render={({ field: controllerField, fieldState }) => (
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
<FieldContent>
<InputGroup>
<InputGroupInput
{...controllerField}
id={`form-rhf-array-email-${index}`}
aria-invalid={fieldState.invalid}
placeholder="name@example.com"
type="email"
autoComplete="email"
/>
{/* Remove button */}
</InputGroup>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldContent>
</Field>
)}
/>
))
}
```
### Adding Items
Use the `append` method to add new items to the array.
```tsx showLineNumbers title="form.tsx"
<Button
type="button"
variant="outline"
size="sm"
onClick={() => append({ address: "" })}
disabled={fields.length >= 5}
>
Add Email Address
</Button>
```
### Removing Items
Use the `remove` method to remove items from the array. Add the remove button conditionally.
```tsx showLineNumbers title="form.tsx"
{
fields.length > 1 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
onClick={() => remove(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
)
}
```
### Array Validation
Use Zod's `array` method to validate array fields.
```tsx showLineNumbers title="form.tsx"
const formSchema = z.object({
emails: z
.array(
z.object({
address: z.string().email("Enter a valid email address."),
})
)
.min(1, "Add at least one email address.")
.max(5, "You can add up to 5 email addresses."),
})
```

View File

@@ -0,0 +1,698 @@
---
title: TanStack Form
description: Build forms in React using TanStack Form and Zod.
links:
doc: https://tanstack.com/form
---
import { InfoIcon } from "lucide-react"
This guide explores how to build forms using TanStack Form. You'll learn to create forms with the `<Field />` component, implement schema validation with Zod, handle errors, and ensure accessibility.
## Demo
We'll start by building the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
<Callout icon={<InfoIcon />}>
**Note:** For the purpose of this demo, we have intentionally disabled browser
validation to show how schema validation and form errors work in TanStack
Form. It is recommended to add basic browser validation in your production
code.
</Callout>
<ComponentPreview
name="form-tanstack-demo"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
## Approach
This form leverages TanStack Form for powerful, headless form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
- Uses TanStack Form's `useForm` hook for form state management.
- `form.Field` component with render prop pattern for controlled inputs.
- `<Field />` components for building accessible forms.
- Client-side validation using Zod.
- Real-time validation feedback.
## Anatomy
Here's a basic example of a form using TanStack Form with the `<Field />` component.
```tsx showLineNumbers {15-31}
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<form.Field
name="title"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
</FieldGroup>
<Button type="submit">Submit</Button>
</form>
```
## Form
### Create a schema
We'll start by defining the shape of our form using a Zod schema.
<Callout icon={<InfoIcon />}>
**Note:** This example uses `zod v3` for schema validation. TanStack Form
integrates seamlessly with Zod and other Standard Schema validation libraries
through its validators API.
</Callout>
```tsx showLineNumbers title="form.tsx"
import * as z from "zod"
const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
```
### Setup the form
Use the `useForm` hook from TanStack Form to create your form instance with Zod validation.
```tsx showLineNumbers title="form.tsx" {10-21}
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
const formSchema = z.object({
// ...
})
export function BugReportForm() {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast.success("Form submitted successfully")
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
{/* ... */}
</form>
)
}
```
We are using `onSubmit` to validate the form data here. TanStack Form supports other validation modes, which you can read about in the [documentation](https://tanstack.com/form/latest/docs/framework/react/guides/dynamic-validation).
### Build the form
We can now build the form using the `form.Field` component from TanStack Form and the `<Field />` component.
<ComponentSource
src="/registry/new-york-v4/examples/form-tanstack-demo.tsx"
title="form.tsx"
/>
### Done
That's it. You now have a fully accessible form with client-side validation.
When you submit the form, the `onSubmit` function will be called with the validated form data. If the form data is invalid, TanStack Form will display the errors next to each field.
## Validation
### Client-side Validation
TanStack Form validates your form data using the Zod schema. Validation happens in real-time as the user types.
```tsx showLineNumbers title="form.tsx" {13-15}
import { useForm } from "@tanstack/react-form"
const formSchema = z.object({
// ...
})
export function BugReportForm() {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
return <form onSubmit={/* ... */}>{/* ... */}</form>
}
```
### Validation Modes
TanStack Form supports different validation strategies through the `validators` option:
| Mode | Description |
| ------------ | ------------------------------------ |
| `"onChange"` | Validation triggers on every change. |
| `"onBlur"` | Validation triggers on blur. |
| `"onSubmit"` | Validation triggers on submit. |
```tsx showLineNumbers title="form.tsx" {6-9}
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
onChange: formSchema,
onBlur: formSchema,
},
})
```
## Displaying Errors
Display errors next to the field using `<FieldError />`. For styling and accessibility:
- Add the `data-invalid` prop to the `<Field />` component.
- Add the `aria-invalid` prop to the form control such as `<Input />`, `<SelectTrigger />`, `<Checkbox />`, etc.
```tsx showLineNumbers title="form.tsx" {4,18}
<form.Field
name="email"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
type="email"
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
```
## Working with Different Field Types
### Input
- For input fields, use `field.state.value` and `field.handleChange` on the `<Input />` component.
- To show errors, add the `aria-invalid` prop to the `<Input />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-tanstack-input"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {6,11-14,22}
<form.Field
name="username"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor="form-tanstack-input-username">Username</FieldLabel>
<Input
id="form-tanstack-input-username"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="shadcn"
autoComplete="username"
/>
<FieldDescription>
This is your public display name. Must be between 3 and 10 characters.
Must only contain letters, numbers, and underscores.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
```
### Textarea
- For textarea fields, use `field.state.value` and `field.handleChange` on the `<Textarea />` component.
- To show errors, add the `aria-invalid` prop to the `<Textarea />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-tanstack-textarea"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {6,13-16,24}
<form.Field
name="about"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor="form-tanstack-textarea-about">
More about you
</FieldLabel>
<Textarea
id="form-tanstack-textarea-about"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="I'm a software engineer..."
className="min-h-[120px]"
/>
<FieldDescription>
Tell us more about yourself. This will be used to help us personalize
your experience.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
```
### Select
- For select components, use `field.state.value` and `field.handleChange` on the `<Select />` component.
- To show errors, add the `aria-invalid` prop to the `<SelectTrigger />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-tanstack-select"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {6,18-19,23}
<form.Field
name="language"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="responsive" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-select-language">
Spoken Language
</FieldLabel>
<FieldDescription>
For best results, select the language you speak.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldContent>
<Select
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
<SelectTrigger
id="form-tanstack-select-language"
aria-invalid={isInvalid}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</Field>
)
}}
/>
```
### Checkbox
- For checkbox, use `field.state.value` and `field.handleChange` on the `<Checkbox />` component.
- To show errors, add the `aria-invalid` prop to the `<Checkbox />` component and the `data-invalid` prop to the `<Field />` component.
- For checkbox arrays, use `mode="array"` on the `<form.Field />` component and TanStack Form's array helpers.
- Remember to add `data-slot="checkbox-group"` to the `<FieldGroup />` component for proper styling and spacing.
<ComponentPreview
name="form-tanstack-checkbox"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {12,17,22-24,44}
<form.Field
name="tasks"
mode="array"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet>
<FieldLegend variant="label">Tasks</FieldLegend>
<FieldDescription>
Get notified when tasks you&apos;ve created have updates.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{tasks.map((task) => (
<Field
key={task.id}
orientation="horizontal"
data-invalid={isInvalid}
>
<Checkbox
id={`form-tanstack-checkbox-${task.id}`}
name={field.name}
aria-invalid={isInvalid}
checked={field.state.value.includes(task.id)}
onCheckedChange={(checked) => {
if (checked) {
field.pushValue(task.id)
} else {
const index = field.state.value.indexOf(task.id)
if (index > -1) {
field.removeValue(index)
}
}
}}
/>
<FieldLabel
htmlFor={`form-tanstack-checkbox-${task.id}`}
className="font-normal"
>
{task.label}
</FieldLabel>
</Field>
))}
</FieldGroup>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldSet>
)
}}
/>
```
### Radio Group
- For radio groups, use `field.state.value` and `field.handleChange` on the `<RadioGroup />` component.
- To show errors, add the `aria-invalid` prop to the `<RadioGroupItem />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-tanstack-radiogroup"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {21,29,35}
<form.Field
name="plan"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
{plans.map((plan) => (
<FieldLabel
key={plan.id}
htmlFor={`form-tanstack-radiogroup-${plan.id}`}
>
<Field orientation="horizontal" data-invalid={isInvalid}>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>{plan.description}</FieldDescription>
</FieldContent>
<RadioGroupItem
value={plan.id}
id={`form-tanstack-radiogroup-${plan.id}`}
aria-invalid={isInvalid}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldSet>
)
}}
/>
```
### Switch
- For switches, use `field.state.value` and `field.handleChange` on the `<Switch />` component.
- To show errors, add the `aria-invalid` prop to the `<Switch />` component and the `data-invalid` prop to the `<Field />` component.
<ComponentPreview
name="form-tanstack-switch"
className="sm:[&_.preview]:h-[500px] sm:[&_pre]:!h-[500px]"
chromeLessOnMobile
/>
```tsx showLineNumbers title="form.tsx" {6,14,19-21}
<form.Field
name="twoFactor"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="horizontal" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-switch-twoFactor">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldContent>
<Switch
id="form-tanstack-switch-twoFactor"
name={field.name}
checked={field.state.value}
onCheckedChange={field.handleChange}
aria-invalid={isInvalid}
/>
</Field>
)
}}
/>
```
### Complex Forms
Here is an example of a more complex form with multiple fields and validation.
<ComponentPreview
name="form-tanstack-complex"
className="sm:[&_.preview]:h-[1100px] sm:[&_pre]:!h-[1100px]"
chromeLessOnMobile
/>
## Resetting the Form
Use `form.reset()` to reset the form to its default values.
```tsx showLineNumbers
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
```
## Array Fields
TanStack Form provides powerful array field management with `mode="array"`. This allows you to dynamically add, remove, and update array items with full validation support.
<ComponentPreview
name="form-tanstack-array"
className="sm:[&_.preview]:h-[700px] sm:[&_pre]:!h-[700px]"
chromeLessOnMobile
/>
This example demonstrates managing multiple email addresses with array fields. Users can add up to 5 email addresses, remove individual addresses, and each address is validated independently.
### Array Field Structure
Use `mode="array"` on the parent field to enable array field management.
```tsx showLineNumbers title="form.tsx" {3,12-14}
<form.Field
name="emails"
mode="array"
children={(field) => {
return (
<FieldSet>
<FieldLegend variant="label">Email Addresses</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup>
{field.state.value.map((_, index) => (
// Nested field for each array item
))}
</FieldGroup>
</FieldSet>
)
}}
/>
```
### Nested Fields
Access individual array items using bracket notation: `fieldName[index].propertyName`. This example uses `InputGroup` to display the remove button inline with the input.
```tsx showLineNumbers title="form.tsx"
<form.Field
name={`emails[${index}].address`}
children={(subField) => {
const isSubFieldInvalid =
subField.state.meta.isTouched && !subField.state.meta.isValid
return (
<Field orientation="horizontal" data-invalid={isSubFieldInvalid}>
<FieldContent>
<InputGroup>
<InputGroupInput
id={`form-tanstack-array-email-${index}`}
name={subField.name}
value={subField.state.value}
onBlur={subField.handleBlur}
onChange={(e) => subField.handleChange(e.target.value)}
aria-invalid={isSubFieldInvalid}
placeholder="name@example.com"
type="email"
/>
{field.state.value.length > 1 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
onClick={() => field.removeValue(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
{isSubFieldInvalid && (
<FieldError errors={subField.state.meta.errors} />
)}
</FieldContent>
</Field>
)
}}
/>
```
### Adding Items
Use `field.pushValue(item)` to add items to an array field. You can disable the button when the array reaches its maximum length.
```tsx showLineNumbers title="form.tsx"
<Button
type="button"
variant="outline"
size="sm"
onClick={() => field.pushValue({ address: "" })}
disabled={field.state.value.length >= 5}
>
Add Email Address
</Button>
```
### Removing Items
Use `field.removeValue(index)` to remove items from an array field. You can conditionally show the remove button only when there's more than one item.
```tsx showLineNumbers title="form.tsx"
{
field.state.value.length > 1 && (
<InputGroupButton
onClick={() => field.removeValue(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
)
}
```
### Array Validation
Validate array fields using Zod's array methods.
```tsx showLineNumbers title="form.tsx"
const formSchema = z.object({
emails: z
.array(
z.object({
address: z.string().email("Enter a valid email address."),
})
)
.min(1, "Add at least one email address.")
.max(5, "You can add up to 5 email addresses."),
})
```

View File

@@ -4,6 +4,7 @@
"(root)",
"changelog",
"components",
"forms",
"installation",
"dark-mode",
"registry"

View File

@@ -42,16 +42,6 @@ const nextConfig = {
destination: "/docs/figma",
permanent: true,
},
{
source: "/docs/forms",
destination: "/docs/components/form",
permanent: false,
},
{
source: "/docs/forms/react-hook-form",
destination: "/docs/components/form",
permanent: false,
},
{
source: "/sidebar",
destination: "/docs/components/sidebar",

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-next-demo",
"type": "registry:example",
"registryDependencies": [
"field",
"input",
"textarea",
"button",
"card",
"spinner"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-next-demo.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport Form from \"next/form\"\nimport { toast } from \"sonner\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport {\n InputGroup,\n InputGroupAddon,\n InputGroupText,\n InputGroupTextarea,\n} from \"@/registry/new-york-v4/ui/input-group\"\nimport { Spinner } from \"@/registry/new-york-v4/ui/spinner\"\n\nimport { demoFormAction } from \"./form-next-demo-action\"\nimport { type FormState } from \"./form-next-demo-schema\"\n\nexport default function FormNextDemo() {\n const [formState, formAction, pending] = React.useActionState<\n FormState,\n FormData\n >(demoFormAction, {\n values: {\n title: \"\",\n description: \"\",\n },\n errors: null,\n success: false,\n })\n const [descriptionLength, setDescriptionLength] = React.useState(0)\n\n React.useEffect(() => {\n if (formState.success) {\n toast(\"Thank you for your feedback\", {\n description: \"We'll review your report and get back to you soon.\",\n })\n }\n }, [formState.success])\n\n React.useEffect(() => {\n setDescriptionLength(formState.values.description.length)\n }, [formState.values.description])\n\n return (\n <Card className=\"w-full max-w-md\">\n <CardHeader>\n <CardTitle>Bug Report</CardTitle>\n <CardDescription>\n Help us improve by reporting bugs you encounter.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <Form action={formAction} id=\"bug-report-form\">\n <FieldGroup>\n <Field data-invalid={!!formState.errors?.title?.length}>\n <FieldLabel htmlFor=\"title\">Bug Title</FieldLabel>\n <Input\n id=\"title\"\n name=\"title\"\n defaultValue={formState.values.title}\n disabled={pending}\n aria-invalid={!!formState.errors?.title?.length}\n placeholder=\"Login button not working on mobile\"\n autoComplete=\"off\"\n />\n {formState.errors?.title && (\n <FieldError>{formState.errors.title[0]}</FieldError>\n )}\n </Field>\n <Field data-invalid={!!formState.errors?.description?.length}>\n <FieldLabel htmlFor=\"description\">Description</FieldLabel>\n <InputGroup>\n <InputGroupTextarea\n id=\"description\"\n name=\"description\"\n defaultValue={formState.values.description}\n placeholder=\"I'm having an issue with the login button on mobile.\"\n rows={6}\n className=\"min-h-24 resize-none\"\n disabled={pending}\n aria-invalid={!!formState.errors?.description?.length}\n onChange={(e) => setDescriptionLength(e.target.value.length)}\n />\n <InputGroupAddon align=\"block-end\">\n <InputGroupText className=\"tabular-nums\">\n {descriptionLength}/100 characters\n </InputGroupText>\n </InputGroupAddon>\n </InputGroup>\n <FieldDescription>\n Include steps to reproduce, expected behavior, and what actually\n happened.\n </FieldDescription>\n {formState.errors?.description && (\n <FieldError>{formState.errors.description[0]}</FieldError>\n )}\n </Field>\n </FieldGroup>\n </Form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"submit\" disabled={pending} form=\"bug-report-form\">\n {pending && <Spinner />}\n Submit\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-rhf-demo",
"type": "registry:example",
"dependencies": [
"react-hook-form",
"@hookform/resolvers",
"zod"
],
"registryDependencies": [
"field",
"input",
"input-group",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-rhf-demo.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport {\n InputGroup,\n InputGroupAddon,\n InputGroupText,\n InputGroupTextarea,\n} from \"@/registry/new-york-v4/ui/input-group\"\n\nconst formSchema = z.object({\n title: z\n .string()\n .min(5, \"Bug title must be at least 5 characters.\")\n .max(32, \"Bug title must be at most 32 characters.\"),\n description: z\n .string()\n .min(20, \"Description must be at least 20 characters.\")\n .max(100, \"Description must be at most 100 characters.\"),\n})\n\nexport default function BugReportForm() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n title: \"\",\n description: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Bug Report</CardTitle>\n <CardDescription>\n Help us improve by reporting bugs you encounter.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-demo\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"title\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field data-invalid={fieldState.invalid}>\n <FieldLabel htmlFor=\"form-rhf-demo-title\">\n Bug Title\n </FieldLabel>\n <Input\n {...field}\n id=\"form-rhf-demo-title\"\n aria-invalid={fieldState.invalid}\n placeholder=\"Login button not working on mobile\"\n autoComplete=\"off\"\n />\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </Field>\n )}\n />\n <Controller\n name=\"description\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field data-invalid={fieldState.invalid}>\n <FieldLabel htmlFor=\"form-rhf-demo-description\">\n Description\n </FieldLabel>\n <InputGroup>\n <InputGroupTextarea\n {...field}\n id=\"form-rhf-demo-description\"\n placeholder=\"I'm having an issue with the login button on mobile.\"\n rows={6}\n className=\"min-h-24 resize-none\"\n aria-invalid={fieldState.invalid}\n />\n <InputGroupAddon align=\"block-end\">\n <InputGroupText className=\"tabular-nums\">\n {field.value.length}/100 characters\n </InputGroupText>\n </InputGroupAddon>\n </InputGroup>\n <FieldDescription>\n Include steps to reproduce, expected behavior, and what\n actually happened.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-demo\">\n Submit\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-rhf-input",
"type": "registry:example",
"dependencies": [
"react-hook-form",
"@hookform/resolvers",
"zod"
],
"registryDependencies": [
"field",
"input",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-rhf-input.tsx",
"content": "\"use client\"\n\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\n\nconst formSchema = z.object({\n username: z\n .string()\n .min(3, \"Username must be at least 3 characters.\")\n .max(10, \"Username must be at most 10 characters.\")\n .regex(\n /^[a-zA-Z0-9_]+$/,\n \"Username can only contain letters, numbers, and underscores.\"\n ),\n})\n\nexport default function FormRhfInput() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n username: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Profile Settings</CardTitle>\n <CardDescription>\n Update your profile information below.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-input\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"username\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field data-invalid={fieldState.invalid}>\n <FieldLabel htmlFor=\"form-rhf-input-username\">\n Username\n </FieldLabel>\n <Input\n {...field}\n id=\"form-rhf-input-username\"\n aria-invalid={fieldState.invalid}\n placeholder=\"shadcn\"\n autoComplete=\"username\"\n />\n <FieldDescription>\n This is your public display name. Must be between 3 and 10\n characters. Must only contain letters, numbers, and\n underscores.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-input\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-rhf-radiogroup",
"type": "registry:example",
"dependencies": [
"react-hook-form",
"@hookform/resolvers",
"zod"
],
"registryDependencies": [
"field",
"radio-group",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-rhf-radiogroup.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSet,\n FieldTitle,\n} from \"@/registry/new-york-v4/ui/field\"\nimport {\n RadioGroup,\n RadioGroupItem,\n} from \"@/registry/new-york-v4/ui/radio-group\"\n\nconst plans = [\n {\n id: \"starter\",\n title: \"Starter (100K tokens/month)\",\n description: \"For everyday use with basic features.\",\n },\n {\n id: \"pro\",\n title: \"Pro (1M tokens/month)\",\n description: \"For advanced AI usage with more features.\",\n },\n {\n id: \"enterprise\",\n title: \"Enterprise (Unlimited tokens)\",\n description: \"For large teams and heavy usage.\",\n },\n] as const\n\nconst formSchema = z.object({\n plan: z.string().min(1, \"You must select a subscription plan to continue.\"),\n})\n\nexport default function FormRhfRadioGroup() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n plan: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Subscription Plan</CardTitle>\n <CardDescription>\n See pricing and features for each plan.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-radiogroup\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"plan\"\n control={form.control}\n render={({ field, fieldState }) => (\n <FieldSet data-invalid={fieldState.invalid}>\n <FieldLegend>Plan</FieldLegend>\n <FieldDescription>\n You can upgrade or downgrade your plan at any time.\n </FieldDescription>\n <RadioGroup\n name={field.name}\n value={field.value}\n onValueChange={field.onChange}\n aria-invalid={fieldState.invalid}\n >\n {plans.map((plan) => (\n <FieldLabel\n key={plan.id}\n htmlFor={`form-rhf-radiogroup-${plan.id}`}\n >\n <Field\n orientation=\"horizontal\"\n data-invalid={fieldState.invalid}\n >\n <FieldContent>\n <FieldTitle>{plan.title}</FieldTitle>\n <FieldDescription>\n {plan.description}\n </FieldDescription>\n </FieldContent>\n <RadioGroupItem\n value={plan.id}\n id={`form-rhf-radiogroup-${plan.id}`}\n aria-invalid={fieldState.invalid}\n />\n </Field>\n </FieldLabel>\n ))}\n </RadioGroup>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </FieldSet>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-radiogroup\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-rhf-select",
"type": "registry:example",
"dependencies": [
"react-hook-form",
"@hookform/resolvers",
"zod"
],
"registryDependencies": [
"field",
"select",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-rhf-select.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/new-york-v4/ui/select\"\n\nconst spokenLanguages = [\n { label: \"English\", value: \"en\" },\n { label: \"Spanish\", value: \"es\" },\n { label: \"French\", value: \"fr\" },\n { label: \"German\", value: \"de\" },\n { label: \"Italian\", value: \"it\" },\n { label: \"Chinese\", value: \"zh\" },\n { label: \"Japanese\", value: \"ja\" },\n] as const\n\nconst formSchema = z.object({\n language: z\n .string()\n .min(1, \"Please select your spoken language.\")\n .refine((val) => val !== \"auto\", {\n message:\n \"Auto-detection is not allowed. Please select a specific language.\",\n }),\n})\n\nexport default function FormRhfSelect() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n language: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-lg\">\n <CardHeader>\n <CardTitle>Language Preferences</CardTitle>\n <CardDescription>\n Select your preferred spoken language.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-select\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"language\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field\n orientation=\"responsive\"\n data-invalid={fieldState.invalid}\n >\n <FieldContent>\n <FieldLabel htmlFor=\"form-rhf-select-language\">\n Spoken Language\n </FieldLabel>\n <FieldDescription>\n For best results, select the language you speak.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </FieldContent>\n <Select\n name={field.name}\n value={field.value}\n onValueChange={field.onChange}\n >\n <SelectTrigger\n id=\"form-rhf-select-language\"\n aria-invalid={fieldState.invalid}\n className=\"min-w-[120px]\"\n >\n <SelectValue placeholder=\"Select\" />\n </SelectTrigger>\n <SelectContent position=\"item-aligned\">\n <SelectItem value=\"auto\">Auto</SelectItem>\n <SelectSeparator />\n {spokenLanguages.map((language) => (\n <SelectItem key={language.value} value={language.value}>\n {language.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-select\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-rhf-switch",
"type": "registry:example",
"dependencies": [
"react-hook-form",
"@hookform/resolvers",
"zod"
],
"registryDependencies": [
"field",
"switch",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-rhf-switch.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Switch } from \"@/registry/new-york-v4/ui/switch\"\n\nconst formSchema = z.object({\n twoFactor: z.boolean().refine((val) => val === true, {\n message: \"It is highly recommended to enable two-factor authentication.\",\n }),\n})\n\nexport default function FormRhfSwitch() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n twoFactor: false,\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Security Settings</CardTitle>\n <CardDescription>\n Manage your account security preferences.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-switch\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"twoFactor\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field\n orientation=\"horizontal\"\n data-invalid={fieldState.invalid}\n >\n <FieldContent>\n <FieldLabel htmlFor=\"form-rhf-switch-twoFactor\">\n Multi-factor authentication\n </FieldLabel>\n <FieldDescription>\n Enable multi-factor authentication to secure your account.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </FieldContent>\n <Switch\n id=\"form-rhf-switch-twoFactor\"\n name={field.name}\n checked={field.value}\n onCheckedChange={field.onChange}\n aria-invalid={fieldState.invalid}\n />\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-switch\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-rhf-textarea",
"type": "registry:example",
"dependencies": [
"react-hook-form",
"@hookform/resolvers",
"zod"
],
"registryDependencies": [
"field",
"textarea",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-rhf-textarea.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Textarea } from \"@/registry/new-york-v4/ui/textarea\"\n\nconst formSchema = z.object({\n about: z\n .string()\n .min(10, \"Please provide at least 10 characters.\")\n .max(200, \"Please keep it under 200 characters.\"),\n})\n\nexport default function FormRhfTextarea() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n about: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Personalization</CardTitle>\n <CardDescription>\n Customize your experience by telling us more about yourself.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-textarea\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"about\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field data-invalid={fieldState.invalid}>\n <FieldLabel htmlFor=\"form-rhf-textarea-about\">\n More about you\n </FieldLabel>\n <Textarea\n {...field}\n id=\"form-rhf-textarea-about\"\n aria-invalid={fieldState.invalid}\n placeholder=\"I'm a software engineer...\"\n className=\"min-h-[120px]\"\n />\n <FieldDescription>\n Tell us more about yourself. This will be used to help us\n personalize your experience.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-textarea\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-tanstack-input",
"type": "registry:example",
"dependencies": [
"@tanstack/react-form",
"zod"
],
"registryDependencies": [
"field",
"input",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-tanstack-input.tsx",
"content": "/* eslint-disable react/no-children-prop */\n\"use client\"\n\nimport { useForm } from \"@tanstack/react-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\n\nconst formSchema = z.object({\n username: z\n .string()\n .min(3, \"Username must be at least 3 characters.\")\n .max(10, \"Username must be at most 10 characters.\")\n .regex(\n /^[a-zA-Z0-9_]+$/,\n \"Username can only contain letters, numbers, and underscores.\"\n ),\n})\n\nexport default function FormTanstackInput() {\n const form = useForm({\n defaultValues: {\n username: \"\",\n },\n validators: {\n onSubmit: formSchema,\n },\n onSubmit: async ({ value }) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(value, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n },\n })\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Profile Settings</CardTitle>\n <CardDescription>\n Update your profile information below.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form\n id=\"form-tanstack-input\"\n onSubmit={(e) => {\n e.preventDefault()\n form.handleSubmit()\n }}\n >\n <FieldGroup>\n <form.Field\n name=\"username\"\n children={(field) => {\n const isInvalid =\n field.state.meta.isTouched && !field.state.meta.isValid\n return (\n <Field data-invalid={isInvalid}>\n <FieldLabel htmlFor=\"form-tanstack-input-username\">\n Username\n </FieldLabel>\n <Input\n id=\"form-tanstack-input-username\"\n name={field.name}\n value={field.state.value}\n onBlur={field.handleBlur}\n onChange={(e) => field.handleChange(e.target.value)}\n aria-invalid={isInvalid}\n placeholder=\"shadcn\"\n autoComplete=\"username\"\n />\n <FieldDescription>\n This is your public display name. Must be between 3 and 10\n characters. Must only contain letters, numbers, and\n underscores.\n </FieldDescription>\n {isInvalid && (\n <FieldError errors={field.state.meta.errors} />\n )}\n </Field>\n )\n }}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-tanstack-input\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

View File

@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-tanstack-radiogroup",
"type": "registry:example",
"dependencies": [
"@tanstack/react-form",
"zod"
],
"registryDependencies": [
"field",
"radio-group",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-tanstack-radiogroup.tsx",
"content": "/* eslint-disable react/no-children-prop */\n\"use client\"\n\nimport { useForm } from \"@tanstack/react-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSet,\n FieldTitle,\n} from \"@/registry/new-york-v4/ui/field\"\nimport {\n RadioGroup,\n RadioGroupItem,\n} from \"@/registry/new-york-v4/ui/radio-group\"\n\nconst plans = [\n {\n id: \"starter\",\n title: \"Starter (100K tokens/month)\",\n description: \"For everyday use with basic features.\",\n },\n {\n id: \"pro\",\n title: \"Pro (1M tokens/month)\",\n description: \"For advanced AI usage with more features.\",\n },\n {\n id: \"enterprise\",\n title: \"Enterprise (Unlimited tokens)\",\n description: \"For large teams and heavy usage.\",\n },\n] as const\n\nconst formSchema = z.object({\n plan: z.string().min(1, \"You must select a subscription plan to continue.\"),\n})\n\nexport default function FormTanstackRadioGroup() {\n const form = useForm({\n defaultValues: {\n plan: \"\",\n },\n validators: {\n onSubmit: formSchema,\n },\n onSubmit: async ({ value }) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(value, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n },\n })\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Subscription Plan</CardTitle>\n <CardDescription>\n See pricing and features for each plan.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form\n id=\"form-tanstack-radiogroup\"\n onSubmit={(e) => {\n e.preventDefault()\n form.handleSubmit()\n }}\n >\n <FieldGroup>\n <form.Field\n name=\"plan\"\n children={(field) => {\n const isInvalid =\n field.state.meta.isTouched && !field.state.meta.isValid\n return (\n <FieldSet>\n <FieldLegend>Plan</FieldLegend>\n <FieldDescription>\n You can upgrade or downgrade your plan at any time.\n </FieldDescription>\n <RadioGroup\n name={field.name}\n value={field.state.value}\n onValueChange={field.handleChange}\n >\n {plans.map((plan) => (\n <FieldLabel\n key={plan.id}\n htmlFor={`form-tanstack-radiogroup-${plan.id}`}\n >\n <Field\n orientation=\"horizontal\"\n data-invalid={isInvalid}\n >\n <FieldContent>\n <FieldTitle>{plan.title}</FieldTitle>\n <FieldDescription>\n {plan.description}\n </FieldDescription>\n </FieldContent>\n <RadioGroupItem\n value={plan.id}\n id={`form-tanstack-radiogroup-${plan.id}`}\n aria-invalid={isInvalid}\n />\n </Field>\n </FieldLabel>\n ))}\n </RadioGroup>\n {isInvalid && (\n <FieldError errors={field.state.meta.errors} />\n )}\n </FieldSet>\n )\n }}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-tanstack-radiogroup\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-tanstack-switch",
"type": "registry:example",
"dependencies": [
"@tanstack/react-form",
"zod"
],
"registryDependencies": [
"field",
"switch",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-tanstack-switch.tsx",
"content": "/* eslint-disable react/no-children-prop */\n\"use client\"\n\nimport { useForm } from \"@tanstack/react-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Switch } from \"@/registry/new-york-v4/ui/switch\"\n\nconst formSchema = z.object({\n twoFactor: z.boolean().refine((val) => val === true, {\n message: \"It is highly recommended to enable two-factor authentication.\",\n }),\n})\n\nexport default function FormTanstackSwitch() {\n const form = useForm({\n defaultValues: {\n twoFactor: false,\n },\n validators: {\n onSubmit: formSchema,\n },\n onSubmit: async ({ value }) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(value, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n },\n })\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Security Settings</CardTitle>\n <CardDescription>\n Manage your account security preferences.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form\n id=\"form-tanstack-switch\"\n onSubmit={(e) => {\n e.preventDefault()\n form.handleSubmit()\n }}\n >\n <FieldGroup>\n <form.Field\n name=\"twoFactor\"\n children={(field) => {\n const isInvalid =\n field.state.meta.isTouched && !field.state.meta.isValid\n return (\n <Field orientation=\"horizontal\" data-invalid={isInvalid}>\n <FieldContent>\n <FieldLabel htmlFor=\"form-tanstack-switch-twoFactor\">\n Multi-factor authentication\n </FieldLabel>\n <FieldDescription>\n Enable multi-factor authentication to secure your\n account.\n </FieldDescription>\n {isInvalid && (\n <FieldError errors={field.state.meta.errors} />\n )}\n </FieldContent>\n <Switch\n id=\"form-tanstack-switch-twoFactor\"\n name={field.name}\n checked={field.state.value}\n onCheckedChange={field.handleChange}\n aria-invalid={isInvalid}\n />\n </Field>\n )\n }}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-tanstack-switch\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

View File

@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-tanstack-textarea",
"type": "registry:example",
"dependencies": [
"@tanstack/react-form",
"zod"
],
"registryDependencies": [
"field",
"textarea",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-tanstack-textarea.tsx",
"content": "/* eslint-disable react/no-children-prop */\n\"use client\"\n\nimport { useForm } from \"@tanstack/react-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Textarea } from \"@/registry/new-york-v4/ui/textarea\"\n\nconst formSchema = z.object({\n about: z\n .string()\n .min(10, \"Please provide at least 10 characters.\")\n .max(200, \"Please keep it under 200 characters.\"),\n})\n\nexport default function FormTanstackTextarea() {\n const form = useForm({\n defaultValues: {\n about: \"\",\n },\n validators: {\n onSubmit: formSchema,\n },\n onSubmit: async ({ value }) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(value, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n },\n })\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Personalization</CardTitle>\n <CardDescription>\n Customize your experience by telling us more about yourself.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form\n id=\"form-tanstack-textarea\"\n onSubmit={(e) => {\n e.preventDefault()\n form.handleSubmit()\n }}\n >\n <FieldGroup>\n <form.Field\n name=\"about\"\n children={(field) => {\n const isInvalid =\n field.state.meta.isTouched && !field.state.meta.isValid\n return (\n <Field data-invalid={isInvalid}>\n <FieldLabel htmlFor=\"form-tanstack-textarea-about\">\n More about you\n </FieldLabel>\n <Textarea\n id=\"form-tanstack-textarea-about\"\n name={field.name}\n value={field.state.value}\n onBlur={field.handleBlur}\n onChange={(e) => field.handleChange(e.target.value)}\n aria-invalid={isInvalid}\n placeholder=\"I'm a software engineer...\"\n className=\"min-h-[120px]\"\n />\n <FieldDescription>\n Tell us more about yourself. This will be used to help us\n personalize your experience.\n </FieldDescription>\n {isInvalid && (\n <FieldError errors={field.state.meta.errors} />\n )}\n </Field>\n )\n }}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-tanstack-textarea\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5520,6 +5520,348 @@ export const Index: Record<string, any> = {
categories: undefined,
meta: undefined,
},
"form-rhf-demo": {
name: "form-rhf-demo",
description: "",
type: "registry:example",
registryDependencies: ["field","input","input-group","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-rhf-demo.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-rhf-demo.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-rhf-input": {
name: "form-rhf-input",
description: "",
type: "registry:example",
registryDependencies: ["field","input","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-rhf-input.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-rhf-input.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-rhf-select": {
name: "form-rhf-select",
description: "",
type: "registry:example",
registryDependencies: ["field","select","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-rhf-select.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-rhf-select.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-rhf-checkbox": {
name: "form-rhf-checkbox",
description: "",
type: "registry:example",
registryDependencies: ["field","checkbox","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-rhf-checkbox.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-rhf-checkbox.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-rhf-switch": {
name: "form-rhf-switch",
description: "",
type: "registry:example",
registryDependencies: ["field","switch","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-rhf-switch.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-rhf-switch.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-rhf-textarea": {
name: "form-rhf-textarea",
description: "",
type: "registry:example",
registryDependencies: ["field","textarea","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-rhf-textarea.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-rhf-textarea.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-rhf-radiogroup": {
name: "form-rhf-radiogroup",
description: "",
type: "registry:example",
registryDependencies: ["field","radio-group","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-rhf-radiogroup.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-rhf-radiogroup.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-rhf-array": {
name: "form-rhf-array",
description: "",
type: "registry:example",
registryDependencies: ["field","input","input-group","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-rhf-array.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-rhf-array.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-rhf-complex": {
name: "form-rhf-complex",
description: "",
type: "registry:example",
registryDependencies: ["field","button","card","checkbox","radio-group","select","switch"],
files: [{
path: "registry/new-york-v4/examples/form-rhf-complex.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-rhf-complex.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-rhf-password": {
name: "form-rhf-password",
description: "",
type: "registry:example",
registryDependencies: ["field","input-group","progress","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-rhf-password.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-rhf-password.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-tanstack-demo": {
name: "form-tanstack-demo",
description: "",
type: "registry:example",
registryDependencies: ["field","input","input-group","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-tanstack-demo.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-demo.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-tanstack-input": {
name: "form-tanstack-input",
description: "",
type: "registry:example",
registryDependencies: ["field","input","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-tanstack-input.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-input.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-tanstack-textarea": {
name: "form-tanstack-textarea",
description: "",
type: "registry:example",
registryDependencies: ["field","textarea","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-tanstack-textarea.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-textarea.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-tanstack-select": {
name: "form-tanstack-select",
description: "",
type: "registry:example",
registryDependencies: ["field","select","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-tanstack-select.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-select.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-tanstack-checkbox": {
name: "form-tanstack-checkbox",
description: "",
type: "registry:example",
registryDependencies: ["field","checkbox","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-tanstack-checkbox.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-checkbox.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-tanstack-switch": {
name: "form-tanstack-switch",
description: "",
type: "registry:example",
registryDependencies: ["field","switch","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-tanstack-switch.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-switch.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-tanstack-radiogroup": {
name: "form-tanstack-radiogroup",
description: "",
type: "registry:example",
registryDependencies: ["field","radio-group","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-tanstack-radiogroup.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-radiogroup.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-tanstack-array": {
name: "form-tanstack-array",
description: "",
type: "registry:example",
registryDependencies: ["field","input","input-group","button","card"],
files: [{
path: "registry/new-york-v4/examples/form-tanstack-array.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-array.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"form-tanstack-complex": {
name: "form-tanstack-complex",
description: "",
type: "registry:example",
registryDependencies: ["field","button","card","checkbox","radio-group","select","switch"],
files: [{
path: "registry/new-york-v4/examples/form-tanstack-complex.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/form-tanstack-complex.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"drawer-dialog": {
name: "drawer-dialog",
description: "",

View File

@@ -0,0 +1,37 @@
"use server"
import { formSchema, type FormState } from "./form-next-complex-schema"
export async function complexFormAction(
_prevState: FormState,
formData: FormData
) {
// Sleep for 1 second
await new Promise((resolve) => setTimeout(resolve, 1000))
const values = {
plan: formData.get("plan") as FormState["values"]["plan"],
billingPeriod: formData.get("billingPeriod") as string,
addons: formData.getAll("addons") as string[],
emailNotifications: formData.get("emailNotifications") === "on",
}
const result = formSchema.safeParse(values)
if (!result.success) {
return {
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
// Do something with the values.
// Call your database or API here.
return {
values,
errors: null,
success: true,
}
}

View File

@@ -0,0 +1,52 @@
import { z } from "zod"
export const formSchema = z.object({
plan: z
.string({
required_error: "Please select a subscription plan",
})
.min(1, "Please select a subscription plan")
.refine((value) => value === "basic" || value === "pro", {
message: "Invalid plan selection. Please choose Basic or Pro",
}),
billingPeriod: z
.string({
required_error: "Please select a billing period",
})
.min(1, "Please select a billing period"),
addons: z
.array(z.string())
.min(1, "Please select at least one add-on")
.max(3, "You can select up to 3 add-ons")
.refine(
(value) => value.every((addon) => addons.some((a) => a.id === addon)),
{
message: "You selected an invalid add-on",
}
),
emailNotifications: z.boolean(),
})
export type FormState = {
values: z.infer<typeof formSchema>
errors: null | Partial<Record<keyof z.infer<typeof formSchema>, string[]>>
success: boolean
}
export const addons = [
{
id: "analytics",
title: "Analytics",
description: "Advanced analytics and reporting",
},
{
id: "backup",
title: "Backup",
description: "Automated daily backups",
},
{
id: "support",
title: "Priority Support",
description: "24/7 premium customer support",
},
] as const

View File

@@ -0,0 +1,194 @@
"use client"
import * as React from "react"
import Form from "next/form"
import { toast } from "sonner"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Card, CardContent, CardFooter } from "@/registry/new-york-v4/ui/card"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
import { Switch } from "@/registry/new-york-v4/ui/switch"
import { complexFormAction } from "./form-next-complex-action"
import { addons, type FormState } from "./form-next-complex-schema"
export default function FormNextComplex() {
const [formState, formAction, pending] = React.useActionState<
FormState,
FormData
>(complexFormAction, {
values: {
plan: "basic",
billingPeriod: "monthly",
addons: [],
emailNotifications: false,
},
errors: null,
success: false,
})
React.useEffect(() => {
if (formState.success) {
toast.success("Preferences saved", {
description: "Your subscription plan has been updated.",
})
}
}, [formState.success])
return (
<Card className="w-full max-w-sm">
<CardContent>
<Form action={formAction} id="subscription-form">
<FieldGroup>
<FieldSet data-invalid={!!formState.errors?.plan?.length}>
<FieldLegend>Subscription Plan</FieldLegend>
<FieldDescription>
Choose your subscription plan.
</FieldDescription>
<RadioGroup
name="plan"
defaultValue={formState.values.plan}
disabled={pending}
aria-invalid={!!formState.errors?.plan?.length}
>
<FieldLabel htmlFor="basic">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Basic</FieldTitle>
<FieldDescription>
For individuals and small teams
</FieldDescription>
</FieldContent>
<RadioGroupItem value="basic" id="basic" />
</Field>
</FieldLabel>
<FieldLabel htmlFor="pro">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Pro</FieldTitle>
<FieldDescription>
For businesses with higher demands
</FieldDescription>
</FieldContent>
<RadioGroupItem value="pro" id="pro" />
</Field>
</FieldLabel>
</RadioGroup>
{formState.errors?.plan && (
<FieldError>{formState.errors.plan[0]}</FieldError>
)}
</FieldSet>
<FieldSeparator />
<Field data-invalid={!!formState.errors?.billingPeriod?.length}>
<FieldLabel htmlFor="billingPeriod">Billing Period</FieldLabel>
<Select
name="billingPeriod"
defaultValue={formState.values.billingPeriod}
disabled={pending}
aria-invalid={!!formState.errors?.billingPeriod?.length}
>
<SelectTrigger id="billingPeriod">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="monthly">Monthly</SelectItem>
<SelectItem value="yearly">Yearly</SelectItem>
</SelectContent>
</Select>
<FieldDescription>
Choose how often you want to be billed.
</FieldDescription>
{formState.errors?.billingPeriod && (
<FieldError>{formState.errors.billingPeriod[0]}</FieldError>
)}
</Field>
<FieldSeparator />
<FieldSet>
<FieldLegend>Add-ons</FieldLegend>
<FieldDescription>
Select additional features you&apos;d like to include.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{addons.map((addon) => (
<Field
key={addon.id}
orientation="horizontal"
data-invalid={!!formState.errors?.addons?.length}
>
<Checkbox
id={addon.id}
name="addons"
value={addon.id}
defaultChecked={formState.values.addons.includes(
addon.id
)}
disabled={pending}
aria-invalid={!!formState.errors?.addons?.length}
/>
<FieldContent>
<FieldLabel htmlFor={addon.id}>{addon.title}</FieldLabel>
<FieldDescription>{addon.description}</FieldDescription>
</FieldContent>
</Field>
))}
</FieldGroup>
{formState.errors?.addons && (
<FieldError>{formState.errors.addons[0]}</FieldError>
)}
</FieldSet>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="emailNotifications">
Email Notifications
</FieldLabel>
<FieldDescription>
Receive email updates about your subscription
</FieldDescription>
</FieldContent>
<Switch
id="emailNotifications"
name="emailNotifications"
defaultChecked={formState.values.emailNotifications}
disabled={pending}
aria-invalid={!!formState.errors?.emailNotifications?.length}
/>
</Field>
</FieldGroup>
</Form>
</CardContent>
<CardFooter>
<Field orientation="horizontal" className="justify-end">
<Button type="submit" disabled={pending} form="subscription-form">
{pending && <Spinner />}
Save Preferences
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,35 @@
"use server"
import { formSchema, type FormState } from "./form-next-demo-schema"
export async function demoFormAction(
_prevState: FormState,
formData: FormData
) {
const values = {
title: formData.get("title") as string,
description: formData.get("description") as string,
}
const result = formSchema.safeParse(values)
if (!result.success) {
return {
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
// Do something with the values.
// Call your database or API here.
return {
values: {
title: "",
description: "",
},
errors: null,
success: true,
}
}

View File

@@ -0,0 +1,18 @@
import { z } from "zod"
export const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
export type FormState = {
values: z.infer<typeof formSchema>
errors: null | Partial<Record<keyof z.infer<typeof formSchema>, string[]>>
success: boolean
}

View File

@@ -0,0 +1,128 @@
"use client"
import * as React from "react"
import Form from "next/form"
import { toast } from "sonner"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
InputGroup,
InputGroupAddon,
InputGroupText,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
import { demoFormAction } from "./form-next-demo-action"
import { type FormState } from "./form-next-demo-schema"
export default function FormNextDemo() {
const [formState, formAction, pending] = React.useActionState<
FormState,
FormData
>(demoFormAction, {
values: {
title: "",
description: "",
},
errors: null,
success: false,
})
const [descriptionLength, setDescriptionLength] = React.useState(0)
React.useEffect(() => {
if (formState.success) {
toast("Thank you for your feedback", {
description: "We'll review your report and get back to you soon.",
})
}
}, [formState.success])
React.useEffect(() => {
setDescriptionLength(formState.values.description.length)
}, [formState.values.description])
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Bug Report</CardTitle>
<CardDescription>
Help us improve by reporting bugs you encounter.
</CardDescription>
</CardHeader>
<CardContent>
<Form action={formAction} id="bug-report-form">
<FieldGroup>
<Field data-invalid={!!formState.errors?.title?.length}>
<FieldLabel htmlFor="title">Bug Title</FieldLabel>
<Input
id="title"
name="title"
defaultValue={formState.values.title}
disabled={pending}
aria-invalid={!!formState.errors?.title?.length}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
{formState.errors?.title && (
<FieldError>{formState.errors.title[0]}</FieldError>
)}
</Field>
<Field data-invalid={!!formState.errors?.description?.length}>
<FieldLabel htmlFor="description">Description</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="description"
name="description"
defaultValue={formState.values.description}
placeholder="I'm having an issue with the login button on mobile."
rows={6}
className="min-h-24 resize-none"
disabled={pending}
aria-invalid={!!formState.errors?.description?.length}
onChange={(e) => setDescriptionLength(e.target.value.length)}
/>
<InputGroupAddon align="block-end">
<InputGroupText className="tabular-nums">
{descriptionLength}/100 characters
</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Include steps to reproduce, expected behavior, and what actually
happened.
</FieldDescription>
{formState.errors?.description && (
<FieldError>{formState.errors.description[0]}</FieldError>
)}
</Field>
</FieldGroup>
</Form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="submit" disabled={pending} form="bug-report-form">
{pending && <Spinner />}
Submit
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { XIcon } from "lucide-react"
import { Controller, useFieldArray, useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSet,
} from "@/registry/new-york-v4/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
const formSchema = z.object({
emails: z
.array(
z.object({
address: z.string().email("Enter a valid email address."),
})
)
.min(1, "Add at least one email address.")
.max(5, "You can add up to 5 email addresses."),
})
export default function FormRhfArray() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
emails: [{ address: "" }, { address: "" }],
},
})
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "emails",
})
function onSubmit(data: z.infer<typeof formSchema>) {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
}
return (
<Card className="w-full sm:max-w-md">
<CardHeader className="border-b">
<CardTitle>Contact Emails</CardTitle>
<CardDescription>Manage your contact email addresses.</CardDescription>
</CardHeader>
<CardContent>
<form id="form-rhf-array" onSubmit={form.handleSubmit(onSubmit)}>
<FieldSet className="gap-4">
<FieldLegend variant="label">Email Addresses</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup className="gap-4">
{fields.map((field, index) => (
<Controller
key={field.id}
name={`emails.${index}.address`}
control={form.control}
render={({ field: controllerField, fieldState }) => (
<Field
orientation="horizontal"
data-invalid={fieldState.invalid}
>
<FieldContent>
<InputGroup>
<InputGroupInput
{...controllerField}
id={`form-rhf-array-email-${index}`}
aria-invalid={fieldState.invalid}
placeholder="name@example.com"
type="email"
autoComplete="email"
/>
{fields.length > 1 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
onClick={() => remove(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</FieldContent>
</Field>
)}
/>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => append({ address: "" })}
disabled={fields.length >= 5}
>
Add Email Address
</Button>
</FieldGroup>
{form.formState.errors.emails?.root && (
<FieldError errors={[form.formState.errors.emails.root]} />
)}
</FieldSet>
</form>
</CardContent>
<CardFooter className="border-t">
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-rhf-array">
Save
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,181 @@
"use client"
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from "@/registry/new-york-v4/ui/field"
const tasks = [
{
id: "push",
label: "Push notifications",
},
{
id: "email",
label: "Email notifications",
},
] as const
const formSchema = z.object({
responses: z.boolean(),
tasks: z
.array(z.string())
.min(1, "Please select at least one notification type.")
.refine(
(value) => value.every((task) => tasks.some((t) => t.id === task)),
{
message: "Invalid notification type selected.",
}
),
})
export default function FormRhfCheckbox() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
responses: true,
tasks: [],
},
})
function onSubmit(data: z.infer<typeof formSchema>) {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
}
return (
<Card className="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>Manage your notification preferences.</CardDescription>
</CardHeader>
<CardContent>
<form id="form-rhf-checkbox" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="responses"
control={form.control}
render={({ field, fieldState }) => (
<FieldSet data-invalid={fieldState.invalid}>
<FieldLegend variant="label">Responses</FieldLegend>
<FieldDescription>
Get notified for requests that take time, like research or
image generation.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
<Field orientation="horizontal">
<Checkbox
id="form-rhf-checkbox-responses"
name={field.name}
checked={field.value}
onCheckedChange={field.onChange}
disabled
/>
<FieldLabel
htmlFor="form-rhf-checkbox-responses"
className="font-normal"
>
Push notifications
</FieldLabel>
</Field>
</FieldGroup>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</FieldSet>
)}
/>
<FieldSeparator />
<Controller
name="tasks"
control={form.control}
render={({ field, fieldState }) => (
<FieldSet data-invalid={fieldState.invalid}>
<FieldLegend variant="label">Tasks</FieldLegend>
<FieldDescription>
Get notified when tasks you&apos;ve created have updates.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{tasks.map((task) => (
<Field
key={task.id}
orientation="horizontal"
data-invalid={fieldState.invalid}
>
<Checkbox
id={`form-rhf-checkbox-${task.id}`}
name={field.name}
aria-invalid={fieldState.invalid}
checked={field.value.includes(task.id)}
onCheckedChange={(checked) => {
const newValue = checked
? [...field.value, task.id]
: field.value.filter((value) => value !== task.id)
field.onChange(newValue)
}}
/>
<FieldLabel
htmlFor={`form-rhf-checkbox-${task.id}`}
className="font-normal"
>
{task.label}
</FieldLabel>
</Field>
))}
</FieldGroup>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</FieldSet>
)}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-rhf-checkbox">
Save
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,307 @@
"use client"
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Switch } from "@/registry/new-york-v4/ui/switch"
const addons = [
{
id: "analytics",
title: "Analytics",
description: "Advanced analytics and reporting",
},
{
id: "backup",
title: "Backup",
description: "Automated daily backups",
},
{
id: "support",
title: "Priority Support",
description: "24/7 premium customer support",
},
] as const
const formSchema = z.object({
plan: z
.string({
required_error: "Please select a subscription plan",
})
.min(1, "Please select a subscription plan")
.refine((value) => value === "basic" || value === "pro", {
message: "Invalid plan selection. Please choose Basic or Pro",
}),
billingPeriod: z
.string({
required_error: "Please select a billing period",
})
.min(1, "Please select a billing period"),
addons: z
.array(z.string())
.min(1, "Please select at least one add-on")
.max(3, "You can select up to 3 add-ons")
.refine(
(value) => value.every((addon) => addons.some((a) => a.id === addon)),
{
message: "You selected an invalid add-on",
}
),
emailNotifications: z.boolean(),
})
export default function FormRhfComplex() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
plan: "basic",
billingPeriod: "",
addons: [],
emailNotifications: false,
},
})
function onSubmit(data: z.infer<typeof formSchema>) {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
}
return (
<Card className="w-full max-w-sm">
<CardHeader className="border-b">
<CardTitle>You&apos;re almost there!</CardTitle>
<CardDescription>
Choose your subscription plan and billing period.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-rhf-complex" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="plan"
control={form.control}
render={({ field, fieldState }) => {
const isInvalid = fieldState.invalid
return (
<FieldSet data-invalid={isInvalid}>
<FieldLegend variant="label">Subscription Plan</FieldLegend>
<FieldDescription>
Choose your subscription plan.
</FieldDescription>
<RadioGroup
name={field.name}
value={field.value}
onValueChange={field.onChange}
aria-invalid={isInvalid}
>
<FieldLabel htmlFor="form-rhf-complex-basic">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Basic</FieldTitle>
<FieldDescription>
For individuals and small teams
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="basic"
id="form-rhf-complex-basic"
/>
</Field>
</FieldLabel>
<FieldLabel htmlFor="form-rhf-complex-pro">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Pro</FieldTitle>
<FieldDescription>
For businesses with higher demands
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="pro"
id="form-rhf-complex-pro"
/>
</Field>
</FieldLabel>
</RadioGroup>
{isInvalid && <FieldError errors={[fieldState.error]} />}
</FieldSet>
)
}}
/>
<FieldSeparator />
<Controller
name="billingPeriod"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-rhf-complex-billingPeriod">
Billing Period
</FieldLabel>
<Select
name={field.name}
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger
id="form-rhf-complex-billingPeriod"
aria-invalid={fieldState.invalid}
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="monthly">Monthly</SelectItem>
<SelectItem value="yearly">Yearly</SelectItem>
</SelectContent>
</Select>
<FieldDescription>
Choose how often you want to be billed.
</FieldDescription>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<FieldSeparator />
<Controller
name="addons"
control={form.control}
render={({ field, fieldState }) => (
<FieldSet>
<FieldLegend>Add-ons</FieldLegend>
<FieldDescription>
Select additional features you&apos;d like to include.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{addons.map((addon) => (
<Field
key={addon.id}
orientation="horizontal"
data-invalid={fieldState.invalid}
>
<Checkbox
id={`form-rhf-complex-${addon.id}`}
name={field.name}
aria-invalid={fieldState.invalid}
checked={field.value.includes(addon.id)}
onCheckedChange={(checked) => {
const newValue = checked
? [...field.value, addon.id]
: field.value.filter(
(value) => value !== addon.id
)
field.onChange(newValue)
field.onBlur()
}}
/>
<FieldContent>
<FieldLabel htmlFor={`form-rhf-complex-${addon.id}`}>
{addon.title}
</FieldLabel>
<FieldDescription>
{addon.description}
</FieldDescription>
</FieldContent>
</Field>
))}
</FieldGroup>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</FieldSet>
)}
/>
<FieldSeparator />
<Controller
name="emailNotifications"
control={form.control}
render={({ field, fieldState }) => (
<Field
orientation="horizontal"
data-invalid={fieldState.invalid}
>
<FieldContent>
<FieldLabel htmlFor="form-rhf-complex-emailNotifications">
Email Notifications
</FieldLabel>
<FieldDescription>
Receive email updates about your subscription
</FieldDescription>
</FieldContent>
<Switch
id="form-rhf-complex-emailNotifications"
name={field.name}
checked={field.value}
onCheckedChange={field.onChange}
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter className="border-t">
<Field>
<Button type="submit" form="form-rhf-complex">
Save Preferences
</Button>
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,150 @@
"use client"
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
InputGroup,
InputGroupAddon,
InputGroupText,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
export default function BugReportForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
description: "",
},
})
function onSubmit(data: z.infer<typeof formSchema>) {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
}
return (
<Card className="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Bug Report</CardTitle>
<CardDescription>
Help us improve by reporting bugs you encounter.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-rhf-demo" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="title"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-rhf-demo-title">
Bug Title
</FieldLabel>
<Input
{...field}
id="form-rhf-demo-title"
aria-invalid={fieldState.invalid}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<Controller
name="description"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-rhf-demo-description">
Description
</FieldLabel>
<InputGroup>
<InputGroupTextarea
{...field}
id="form-rhf-demo-description"
placeholder="I'm having an issue with the login button on mobile."
rows={6}
className="min-h-24 resize-none"
aria-invalid={fieldState.invalid}
/>
<InputGroupAddon align="block-end">
<InputGroupText className="tabular-nums">
{field.value.length}/100 characters
</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Include steps to reproduce, expected behavior, and what
actually happened.
</FieldDescription>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-rhf-demo">
Submit
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,114 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
const formSchema = z.object({
username: z
.string()
.min(3, "Username must be at least 3 characters.")
.max(10, "Username must be at most 10 characters.")
.regex(
/^[a-zA-Z0-9_]+$/,
"Username can only contain letters, numbers, and underscores."
),
})
export default function FormRhfInput() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
},
})
function onSubmit(data: z.infer<typeof formSchema>) {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
}
return (
<Card className="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Profile Settings</CardTitle>
<CardDescription>
Update your profile information below.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-rhf-input" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="username"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-rhf-input-username">
Username
</FieldLabel>
<Input
{...field}
id="form-rhf-input-username"
aria-invalid={fieldState.invalid}
placeholder="shadcn"
autoComplete="username"
/>
<FieldDescription>
This is your public display name. Must be between 3 and 10
characters. Must only contain letters, numbers, and
underscores.
</FieldDescription>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-rhf-input">
Save
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,221 @@
"use client"
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { CheckIcon } from "lucide-react"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import { Progress } from "@/registry/new-york-v4/ui/progress"
const passwordRequirements = [
{
id: "length",
label: "At least 8 characters",
test: (val: string) => val.length >= 8,
},
{
id: "lowercase",
label: "One lowercase letter",
test: (val: string) => /[a-z]/.test(val),
},
{
id: "uppercase",
label: "One uppercase letter",
test: (val: string) => /[A-Z]/.test(val),
},
{ id: "number", label: "One number", test: (val: string) => /\d/.test(val) },
{
id: "special",
label: "One special character",
test: (val: string) => /[!@#$%^&*(),.?":{}|<>]/.test(val),
},
]
const formSchema = z.object({
password: z
.string()
.min(8, "Password must be at least 8 characters")
.refine(
(val) => /[a-z]/.test(val),
"Password must contain at least one lowercase letter"
)
.refine(
(val) => /[A-Z]/.test(val),
"Password must contain at least one uppercase letter"
)
.refine(
(val) => /\d/.test(val),
"Password must contain at least one number"
)
.refine(
(val) => /[!@#$%^&*(),.?":{}|<>]/.test(val),
"Password must contain at least one special character"
),
})
export default function FormRhfPassword() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
password: "",
},
})
const password = form.watch("password")
// Calculate password strength.
const metRequirements = passwordRequirements.filter((req) =>
req.test(password || "")
)
const strengthPercentage =
(metRequirements.length / passwordRequirements.length) * 100
// Determine strength level and color.
const getStrengthColor = () => {
if (strengthPercentage === 0) return "bg-neutral-200"
if (strengthPercentage <= 40) return "bg-red-500"
if (strengthPercentage <= 80) return "bg-yellow-500"
return "bg-green-500"
}
const allRequirementsMet =
metRequirements.length === passwordRequirements.length
function onSubmit(data: z.infer<typeof formSchema>) {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
}
return (
<Card className="w-full sm:max-w-md">
<CardHeader className="border-b">
<CardTitle>Create Password</CardTitle>
<CardDescription>
Choose a strong password to secure your account.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-rhf-password" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="password"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-rhf-password-input">
Password
</FieldLabel>
<InputGroup>
<InputGroupInput
{...field}
id="form-rhf-password-input"
type="password"
placeholder="Enter your password"
aria-invalid={fieldState.invalid}
autoComplete="new-password"
/>
<InputGroupAddon align="inline-end">
<CheckIcon
className={
allRequirementsMet
? "text-green-500"
: "text-muted-foreground"
}
/>
</InputGroupAddon>
</InputGroup>
{/* Password strength meter. */}
<div className="space-y-2">
<Progress
value={strengthPercentage}
className={getStrengthColor()}
/>
{/* Requirements list. */}
<div className="space-y-1.5">
{passwordRequirements.map((requirement) => {
const isMet = requirement.test(password || "")
return (
<div
key={requirement.id}
className="flex items-center gap-2 text-sm"
>
<CheckIcon
className={
isMet
? "size-4 text-green-500"
: "text-muted-foreground size-4"
}
/>
<span
className={
isMet
? "text-foreground"
: "text-muted-foreground"
}
>
{requirement.label}
</span>
</div>
)
})}
</div>
</div>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter className="border-t">
<Field>
<Button type="submit" form="form-rhf-password">
Create Password
</Button>
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,152 @@
"use client"
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
const plans = [
{
id: "starter",
title: "Starter (100K tokens/month)",
description: "For everyday use with basic features.",
},
{
id: "pro",
title: "Pro (1M tokens/month)",
description: "For advanced AI usage with more features.",
},
{
id: "enterprise",
title: "Enterprise (Unlimited tokens)",
description: "For large teams and heavy usage.",
},
] as const
const formSchema = z.object({
plan: z.string().min(1, "You must select a subscription plan to continue."),
})
export default function FormRhfRadioGroup() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
plan: "",
},
})
function onSubmit(data: z.infer<typeof formSchema>) {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
}
return (
<Card className="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Subscription Plan</CardTitle>
<CardDescription>
See pricing and features for each plan.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-rhf-radiogroup" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="plan"
control={form.control}
render={({ field, fieldState }) => (
<FieldSet data-invalid={fieldState.invalid}>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup
name={field.name}
value={field.value}
onValueChange={field.onChange}
aria-invalid={fieldState.invalid}
>
{plans.map((plan) => (
<FieldLabel
key={plan.id}
htmlFor={`form-rhf-radiogroup-${plan.id}`}
>
<Field
orientation="horizontal"
data-invalid={fieldState.invalid}
>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>
{plan.description}
</FieldDescription>
</FieldContent>
<RadioGroupItem
value={plan.id}
id={`form-rhf-radiogroup-${plan.id}`}
aria-invalid={fieldState.invalid}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</FieldSet>
)}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-rhf-radiogroup">
Save
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,150 @@
"use client"
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import {
Select,
SelectContent,
SelectItem,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
const spokenLanguages = [
{ label: "English", value: "en" },
{ label: "Spanish", value: "es" },
{ label: "French", value: "fr" },
{ label: "German", value: "de" },
{ label: "Italian", value: "it" },
{ label: "Chinese", value: "zh" },
{ label: "Japanese", value: "ja" },
] as const
const formSchema = z.object({
language: z
.string()
.min(1, "Please select your spoken language.")
.refine((val) => val !== "auto", {
message:
"Auto-detection is not allowed. Please select a specific language.",
}),
})
export default function FormRhfSelect() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
language: "",
},
})
function onSubmit(data: z.infer<typeof formSchema>) {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
}
return (
<Card className="w-full sm:max-w-lg">
<CardHeader>
<CardTitle>Language Preferences</CardTitle>
<CardDescription>
Select your preferred spoken language.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-rhf-select" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="language"
control={form.control}
render={({ field, fieldState }) => (
<Field
orientation="responsive"
data-invalid={fieldState.invalid}
>
<FieldContent>
<FieldLabel htmlFor="form-rhf-select-language">
Spoken Language
</FieldLabel>
<FieldDescription>
For best results, select the language you speak.
</FieldDescription>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</FieldContent>
<Select
name={field.name}
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger
id="form-rhf-select-language"
aria-invalid={fieldState.invalid}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="auto">Auto</SelectItem>
<SelectSeparator />
{spokenLanguages.map((language) => (
<SelectItem key={language.value} value={language.value}>
{language.label}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
)}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-rhf-select">
Save
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,114 @@
"use client"
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import { Switch } from "@/registry/new-york-v4/ui/switch"
const formSchema = z.object({
twoFactor: z.boolean().refine((val) => val === true, {
message: "It is highly recommended to enable two-factor authentication.",
}),
})
export default function FormRhfSwitch() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
twoFactor: false,
},
})
function onSubmit(data: z.infer<typeof formSchema>) {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
}
return (
<Card className="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Security Settings</CardTitle>
<CardDescription>
Manage your account security preferences.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-rhf-switch" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="twoFactor"
control={form.control}
render={({ field, fieldState }) => (
<Field
orientation="horizontal"
data-invalid={fieldState.invalid}
>
<FieldContent>
<FieldLabel htmlFor="form-rhf-switch-twoFactor">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</FieldContent>
<Switch
id="form-rhf-switch-twoFactor"
name={field.name}
checked={field.value}
onCheckedChange={field.onChange}
aria-invalid={fieldState.invalid}
/>
</Field>
)}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-rhf-switch">
Save
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,110 @@
"use client"
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
const formSchema = z.object({
about: z
.string()
.min(10, "Please provide at least 10 characters.")
.max(200, "Please keep it under 200 characters."),
})
export default function FormRhfTextarea() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
about: "",
},
})
function onSubmit(data: z.infer<typeof formSchema>) {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
}
return (
<Card className="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Personalization</CardTitle>
<CardDescription>
Customize your experience by telling us more about yourself.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-rhf-textarea" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="about"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-rhf-textarea-about">
More about you
</FieldLabel>
<Textarea
{...field}
id="form-rhf-textarea-about"
aria-invalid={fieldState.invalid}
placeholder="I'm a software engineer..."
className="min-h-[120px]"
/>
<FieldDescription>
Tell us more about yourself. This will be used to help us
personalize your experience.
</FieldDescription>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-rhf-textarea">
Save
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,179 @@
/* eslint-disable react/no-children-prop */
"use client"
import * as React from "react"
import { useForm } from "@tanstack/react-form"
import { XIcon } from "lucide-react"
import { toast } from "sonner"
import { z } from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSet,
} from "@/registry/new-york-v4/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
const formSchema = z.object({
emails: z
.array(
z.object({
address: z.string().email("Enter a valid email address."),
})
)
.min(1, "Add at least one email address.")
.max(5, "You can add up to 5 email addresses."),
})
export default function FormTanstackArray() {
const form = useForm({
defaultValues: {
emails: [{ address: "" }],
},
validators: {
onBlur: formSchema,
},
onSubmit: async ({ value }) => {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(value, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
},
})
return (
<Card className="w-full sm:max-w-md">
<CardHeader className="border-b">
<CardTitle>Contact Emails</CardTitle>
<CardDescription>Manage your contact email addresses.</CardDescription>
</CardHeader>
<CardContent>
<form
id="form-tanstack-array"
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<form.Field name="emails" mode="array">
{(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet className="gap-4">
<FieldLegend variant="label">Email Addresses</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup className="gap-4">
{field.state.value.map((_, index) => (
<form.Field
key={index}
name={`emails[${index}].address`}
children={(subField) => {
const isSubFieldInvalid =
subField.state.meta.isTouched &&
!subField.state.meta.isValid
return (
<Field
orientation="horizontal"
data-invalid={isSubFieldInvalid}
>
<FieldContent>
<InputGroup>
<InputGroupInput
id={`form-tanstack-array-email-${index}`}
name={subField.name}
value={subField.state.value}
onBlur={subField.handleBlur}
onChange={(e) =>
subField.handleChange(e.target.value)
}
aria-invalid={isSubFieldInvalid}
placeholder="name@example.com"
type="email"
autoComplete="email"
/>
{field.state.value.length > 1 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
onClick={() => field.removeValue(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
{isSubFieldInvalid && (
<FieldError
errors={subField.state.meta.errors}
/>
)}
</FieldContent>
</Field>
)
}}
/>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => field.pushValue({ address: "" })}
disabled={field.state.value.length >= 5}
>
Add Email Address
</Button>
</FieldGroup>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldSet>
)
}}
</form.Field>
</form>
</CardContent>
<CardFooter className="border-t">
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-tanstack-array">
Save
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,200 @@
/* eslint-disable react/no-children-prop */
"use client"
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from "@/registry/new-york-v4/ui/field"
const tasks = [
{
id: "push",
label: "Push notifications",
},
{
id: "email",
label: "Email notifications",
},
] as const
const formSchema = z.object({
responses: z.boolean(),
tasks: z
.array(z.string())
.min(1, "Please select at least one notification type.")
.refine(
(value) => value.every((task) => tasks.some((t) => t.id === task)),
{
message: "Invalid notification type selected.",
}
),
})
export default function FormTanstackCheckbox() {
const form = useForm({
defaultValues: {
responses: true,
tasks: [] as string[],
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(value, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
},
})
return (
<Card className="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>Manage your notification preferences.</CardDescription>
</CardHeader>
<CardContent>
<form
id="form-tanstack-checkbox"
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<form.Field
name="responses"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet>
<FieldLegend variant="label">Responses</FieldLegend>
<FieldDescription>
Get notified for requests that take time, like research or
image generation.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
<Field orientation="horizontal" data-invalid={isInvalid}>
<Checkbox
id="form-tanstack-checkbox-responses"
name={field.name}
checked={field.state.value}
onCheckedChange={(checked) =>
field.handleChange(checked === true)
}
disabled
/>
<FieldLabel
htmlFor="form-tanstack-checkbox-responses"
className="font-normal"
>
Push notifications
</FieldLabel>
</Field>
</FieldGroup>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldSet>
)
}}
/>
<FieldSeparator />
<form.Field
name="tasks"
mode="array"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet>
<FieldLegend variant="label">Tasks</FieldLegend>
<FieldDescription>
Get notified when tasks you&apos;ve created have updates.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{tasks.map((task) => (
<Field
key={task.id}
orientation="horizontal"
data-invalid={isInvalid}
>
<Checkbox
id={`form-tanstack-checkbox-${task.id}`}
name={field.name}
aria-invalid={isInvalid}
checked={field.state.value.includes(task.id)}
onCheckedChange={(checked) => {
if (checked) {
field.pushValue(task.id)
} else {
const index = field.state.value.indexOf(task.id)
if (index > -1) {
field.removeValue(index)
}
}
}}
/>
<FieldLabel
htmlFor={`form-tanstack-checkbox-${task.id}`}
className="font-normal"
>
{task.label}
</FieldLabel>
</Field>
))}
</FieldGroup>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldSet>
)
}}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-tanstack-checkbox">
Save
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,313 @@
/* eslint-disable react/no-children-prop */
"use client"
import * as React from "react"
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Card, CardContent, CardFooter } from "@/registry/new-york-v4/ui/card"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Switch } from "@/registry/new-york-v4/ui/switch"
const addons = [
{
id: "analytics",
title: "Analytics",
description: "Advanced analytics and reporting",
},
{
id: "backup",
title: "Backup",
description: "Automated daily backups",
},
{
id: "support",
title: "Priority Support",
description: "24/7 premium customer support",
},
] as const
const formSchema = z.object({
plan: z
.string({
required_error: "Please select a subscription plan",
})
.min(1, "Please select a subscription plan")
.refine((value) => value === "basic" || value === "pro", {
message: "Invalid plan selection. Please choose Basic or Pro",
}),
billingPeriod: z
.string({
required_error: "Please select a billing period",
})
.min(1, "Please select a billing period"),
addons: z
.array(z.string())
.min(1, "Please select at least one add-on")
.max(3, "You can select up to 3 add-ons")
.refine(
(value) => value.every((addon) => addons.some((a) => a.id === addon)),
{
message: "You selected an invalid add-on",
}
),
emailNotifications: z.boolean(),
})
export default function FormTanstackComplex() {
const form = useForm({
defaultValues: {
plan: "basic",
billingPeriod: "monthly",
addons: [] as string[],
emailNotifications: false,
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(value, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
},
})
return (
<Card className="w-full max-w-sm">
<CardContent>
<form
id="subscription-form"
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<form.Field
name="plan"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet>
<FieldLegend>Subscription Plan</FieldLegend>
<FieldDescription>
Choose your subscription plan.
</FieldDescription>
<RadioGroup
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
<FieldLabel htmlFor="basic">
<Field
orientation="horizontal"
data-invalid={isInvalid}
>
<FieldContent>
<FieldTitle>Basic</FieldTitle>
<FieldDescription>
For individuals and small teams
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="basic"
id="basic"
aria-invalid={isInvalid}
/>
</Field>
</FieldLabel>
<FieldLabel htmlFor="pro">
<Field
orientation="horizontal"
data-invalid={isInvalid}
>
<FieldContent>
<FieldTitle>Pro</FieldTitle>
<FieldDescription>
For businesses with higher demands
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="pro"
id="pro"
aria-invalid={isInvalid}
/>
</Field>
</FieldLabel>
</RadioGroup>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldSet>
)
}}
/>
<FieldSeparator />
<form.Field
name="billingPeriod"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Billing Period</FieldLabel>
<Select
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
aria-invalid={isInvalid}
>
<SelectTrigger id={field.name}>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="monthly">Monthly</SelectItem>
<SelectItem value="yearly">Yearly</SelectItem>
</SelectContent>
</Select>
<FieldDescription>
Choose how often you want to be billed.
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
<FieldSeparator />
<form.Field
name="addons"
mode="array"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet>
<FieldLegend>Add-ons</FieldLegend>
<FieldDescription>
Select additional features you&apos;d like to include.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{addons.map((addon) => (
<Field
key={addon.id}
orientation="horizontal"
data-invalid={isInvalid}
>
<Checkbox
id={addon.id}
name={field.name}
aria-invalid={isInvalid}
checked={field.state.value.includes(addon.id)}
onCheckedChange={(checked) => {
if (checked) {
field.pushValue(addon.id)
} else {
const index = field.state.value.indexOf(
addon.id
)
if (index > -1) {
field.removeValue(index)
}
}
}}
/>
<FieldContent>
<FieldLabel htmlFor={addon.id}>
{addon.title}
</FieldLabel>
<FieldDescription>
{addon.description}
</FieldDescription>
</FieldContent>
</Field>
))}
</FieldGroup>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldSet>
)
}}
/>
<FieldSeparator />
<form.Field
name="emailNotifications"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="horizontal" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor={field.name}>
Email Notifications
</FieldLabel>
<FieldDescription>
Receive email updates about your subscription
</FieldDescription>
</FieldContent>
<Switch
id={field.name}
name={field.name}
checked={field.state.value}
onCheckedChange={field.handleChange}
aria-invalid={isInvalid}
/>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal" className="justify-end">
<Button type="submit" form="subscription-form">
Save Preferences
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,165 @@
/* eslint-disable react/no-children-prop */
"use client"
import * as React from "react"
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
InputGroup,
InputGroupAddon,
InputGroupText,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})
export default function BugReportForm() {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(value, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
},
})
return (
<Card className="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Bug Report</CardTitle>
<CardDescription>
Help us improve by reporting bugs you encounter.
</CardDescription>
</CardHeader>
<CardContent>
<form
id="bug-report-form"
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<form.Field
name="title"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
<form.Field
name="description"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Description</FieldLabel>
<InputGroup>
<InputGroupTextarea
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="I'm having an issue with the login button on mobile."
rows={6}
className="min-h-24 resize-none"
aria-invalid={isInvalid}
/>
<InputGroupAddon align="block-end">
<InputGroupText className="tabular-nums">
{field.state.value.length}/100 characters
</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Include steps to reproduce, expected behavior, and what
actually happened.
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="bug-report-form">
Submit
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,127 @@
/* eslint-disable react/no-children-prop */
"use client"
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
const formSchema = z.object({
username: z
.string()
.min(3, "Username must be at least 3 characters.")
.max(10, "Username must be at most 10 characters.")
.regex(
/^[a-zA-Z0-9_]+$/,
"Username can only contain letters, numbers, and underscores."
),
})
export default function FormTanstackInput() {
const form = useForm({
defaultValues: {
username: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(value, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
},
})
return (
<Card className="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Profile Settings</CardTitle>
<CardDescription>
Update your profile information below.
</CardDescription>
</CardHeader>
<CardContent>
<form
id="form-tanstack-input"
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<form.Field
name="username"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor="form-tanstack-input-username">
Username
</FieldLabel>
<Input
id="form-tanstack-input-username"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="shadcn"
autoComplete="username"
/>
<FieldDescription>
This is your public display name. Must be between 3 and 10
characters. Must only contain letters, numbers, and
underscores.
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-tanstack-input">
Save
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,160 @@
/* eslint-disable react/no-children-prop */
"use client"
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
const plans = [
{
id: "starter",
title: "Starter (100K tokens/month)",
description: "For everyday use with basic features.",
},
{
id: "pro",
title: "Pro (1M tokens/month)",
description: "For advanced AI usage with more features.",
},
{
id: "enterprise",
title: "Enterprise (Unlimited tokens)",
description: "For large teams and heavy usage.",
},
] as const
const formSchema = z.object({
plan: z.string().min(1, "You must select a subscription plan to continue."),
})
export default function FormTanstackRadioGroup() {
const form = useForm({
defaultValues: {
plan: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(value, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
},
})
return (
<Card className="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Subscription Plan</CardTitle>
<CardDescription>
See pricing and features for each plan.
</CardDescription>
</CardHeader>
<CardContent>
<form
id="form-tanstack-radiogroup"
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<form.Field
name="plan"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
{plans.map((plan) => (
<FieldLabel
key={plan.id}
htmlFor={`form-tanstack-radiogroup-${plan.id}`}
>
<Field
orientation="horizontal"
data-invalid={isInvalid}
>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>
{plan.description}
</FieldDescription>
</FieldContent>
<RadioGroupItem
value={plan.id}
id={`form-tanstack-radiogroup-${plan.id}`}
aria-invalid={isInvalid}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldSet>
)
}}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-tanstack-radiogroup">
Save
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,159 @@
/* eslint-disable react/no-children-prop */
"use client"
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import {
Select,
SelectContent,
SelectItem,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
const spokenLanguages = [
{ label: "English", value: "en" },
{ label: "Spanish", value: "es" },
{ label: "French", value: "fr" },
{ label: "German", value: "de" },
{ label: "Italian", value: "it" },
{ label: "Chinese", value: "zh" },
{ label: "Japanese", value: "ja" },
] as const
const formSchema = z.object({
language: z
.string()
.min(1, "Please select your spoken language.")
.refine((val) => val !== "auto", {
message:
"Auto-detection is not allowed. Please select a specific language.",
}),
})
export default function FormTanstackSelect() {
const form = useForm({
defaultValues: {
language: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(value, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
},
})
return (
<Card className="w-full sm:max-w-lg">
<CardHeader>
<CardTitle>Language Preferences</CardTitle>
<CardDescription>
Select your preferred spoken language.
</CardDescription>
</CardHeader>
<CardContent>
<form
id="form-tanstack-select"
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<form.Field
name="language"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="responsive" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-select-language">
Spoken Language
</FieldLabel>
<FieldDescription>
For best results, select the language you speak.
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldContent>
<Select
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
<SelectTrigger
id="form-tanstack-select-language"
aria-invalid={isInvalid}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="auto">Auto</SelectItem>
<SelectSeparator />
{spokenLanguages.map((language) => (
<SelectItem
key={language.value}
value={language.value}
>
{language.label}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
)
}}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-tanstack-select">
Save
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,121 @@
/* eslint-disable react/no-children-prop */
"use client"
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import { Switch } from "@/registry/new-york-v4/ui/switch"
const formSchema = z.object({
twoFactor: z.boolean().refine((val) => val === true, {
message: "It is highly recommended to enable two-factor authentication.",
}),
})
export default function FormTanstackSwitch() {
const form = useForm({
defaultValues: {
twoFactor: false,
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(value, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
},
})
return (
<Card className="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Security Settings</CardTitle>
<CardDescription>
Manage your account security preferences.
</CardDescription>
</CardHeader>
<CardContent>
<form
id="form-tanstack-switch"
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<form.Field
name="twoFactor"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="horizontal" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-switch-twoFactor">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your
account.
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldContent>
<Switch
id="form-tanstack-switch-twoFactor"
name={field.name}
checked={field.state.value}
onCheckedChange={field.handleChange}
aria-invalid={isInvalid}
/>
</Field>
)
}}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-tanstack-switch">
Save
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,122 @@
/* eslint-disable react/no-children-prop */
"use client"
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
const formSchema = z.object({
about: z
.string()
.min(10, "Please provide at least 10 characters.")
.max(200, "Please keep it under 200 characters."),
})
export default function FormTanstackTextarea() {
const form = useForm({
defaultValues: {
about: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast("You submitted the following values:", {
description: (
<pre className="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
<code>{JSON.stringify(value, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
},
})
return (
<Card className="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Personalization</CardTitle>
<CardDescription>
Customize your experience by telling us more about yourself.
</CardDescription>
</CardHeader>
<CardContent>
<form
id="form-tanstack-textarea"
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<form.Field
name="about"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor="form-tanstack-textarea-about">
More about you
</FieldLabel>
<Textarea
id="form-tanstack-textarea-about"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="I'm a software engineer..."
className="min-h-[120px]"
/>
<FieldDescription>
Tell us more about yourself. This will be used to help us
personalize your experience.
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-tanstack-textarea">
Save
</Button>
</Field>
</CardFooter>
</Card>
)
}

View File

@@ -1006,6 +1006,256 @@ export const examples: Registry["items"] = [
},
],
},
{
name: "form-rhf-demo",
type: "registry:example",
registryDependencies: ["field", "input", "input-group", "button", "card"],
files: [
{
path: "examples/form-rhf-demo.tsx",
type: "registry:example",
},
],
dependencies: ["react-hook-form", "@hookform/resolvers", "zod"],
},
{
name: "form-rhf-input",
type: "registry:example",
registryDependencies: ["field", "input", "button", "card"],
files: [
{
path: "examples/form-rhf-input.tsx",
type: "registry:example",
},
],
dependencies: ["react-hook-form", "@hookform/resolvers", "zod"],
},
{
name: "form-rhf-select",
type: "registry:example",
registryDependencies: ["field", "select", "button", "card"],
files: [
{
path: "examples/form-rhf-select.tsx",
type: "registry:example",
},
],
dependencies: ["react-hook-form", "@hookform/resolvers", "zod"],
},
{
name: "form-rhf-checkbox",
type: "registry:example",
registryDependencies: ["field", "checkbox", "button", "card"],
files: [
{
path: "examples/form-rhf-checkbox.tsx",
type: "registry:example",
},
],
dependencies: ["react-hook-form", "@hookform/resolvers", "zod"],
},
{
name: "form-rhf-switch",
type: "registry:example",
registryDependencies: ["field", "switch", "button", "card"],
files: [
{
path: "examples/form-rhf-switch.tsx",
type: "registry:example",
},
],
dependencies: ["react-hook-form", "@hookform/resolvers", "zod"],
},
{
name: "form-rhf-textarea",
type: "registry:example",
registryDependencies: ["field", "textarea", "button", "card"],
files: [
{
path: "examples/form-rhf-textarea.tsx",
type: "registry:example",
},
],
dependencies: ["react-hook-form", "@hookform/resolvers", "zod"],
},
{
name: "form-rhf-radiogroup",
type: "registry:example",
registryDependencies: ["field", "radio-group", "button", "card"],
files: [
{
path: "examples/form-rhf-radiogroup.tsx",
type: "registry:example",
},
],
dependencies: ["react-hook-form", "@hookform/resolvers", "zod"],
},
{
name: "form-rhf-array",
type: "registry:example",
registryDependencies: ["field", "input", "input-group", "button", "card"],
files: [
{
path: "examples/form-rhf-array.tsx",
type: "registry:example",
},
],
dependencies: ["react-hook-form", "@hookform/resolvers", "zod"],
},
{
name: "form-rhf-complex",
type: "registry:example",
registryDependencies: [
"field",
"button",
"card",
"checkbox",
"radio-group",
"select",
"switch",
],
files: [
{
path: "examples/form-rhf-complex.tsx",
type: "registry:example",
},
],
dependencies: ["react-hook-form", "@hookform/resolvers", "zod"],
},
{
name: "form-rhf-password",
type: "registry:example",
registryDependencies: [
"field",
"input-group",
"progress",
"button",
"card",
],
files: [
{
path: "examples/form-rhf-password.tsx",
type: "registry:example",
},
],
dependencies: ["react-hook-form", "@hookform/resolvers", "zod"],
},
{
name: "form-tanstack-demo",
type: "registry:example",
registryDependencies: ["field", "input", "input-group", "button", "card"],
files: [
{
path: "examples/form-tanstack-demo.tsx",
type: "registry:example",
},
],
dependencies: ["@tanstack/react-form", "zod"],
},
{
name: "form-tanstack-input",
type: "registry:example",
registryDependencies: ["field", "input", "button", "card"],
files: [
{
path: "examples/form-tanstack-input.tsx",
type: "registry:example",
},
],
dependencies: ["@tanstack/react-form", "zod"],
},
{
name: "form-tanstack-textarea",
type: "registry:example",
registryDependencies: ["field", "textarea", "button", "card"],
files: [
{
path: "examples/form-tanstack-textarea.tsx",
type: "registry:example",
},
],
dependencies: ["@tanstack/react-form", "zod"],
},
{
name: "form-tanstack-select",
type: "registry:example",
registryDependencies: ["field", "select", "button", "card"],
files: [
{
path: "examples/form-tanstack-select.tsx",
type: "registry:example",
},
],
dependencies: ["@tanstack/react-form", "zod"],
},
{
name: "form-tanstack-checkbox",
type: "registry:example",
registryDependencies: ["field", "checkbox", "button", "card"],
files: [
{
path: "examples/form-tanstack-checkbox.tsx",
type: "registry:example",
},
],
dependencies: ["@tanstack/react-form", "zod"],
},
{
name: "form-tanstack-switch",
type: "registry:example",
registryDependencies: ["field", "switch", "button", "card"],
files: [
{
path: "examples/form-tanstack-switch.tsx",
type: "registry:example",
},
],
dependencies: ["@tanstack/react-form", "zod"],
},
{
name: "form-tanstack-radiogroup",
type: "registry:example",
registryDependencies: ["field", "radio-group", "button", "card"],
files: [
{
path: "examples/form-tanstack-radiogroup.tsx",
type: "registry:example",
},
],
dependencies: ["@tanstack/react-form", "zod"],
},
{
name: "form-tanstack-array",
type: "registry:example",
registryDependencies: ["field", "input", "input-group", "button", "card"],
files: [
{
path: "examples/form-tanstack-array.tsx",
type: "registry:example",
},
],
dependencies: ["@tanstack/react-form", "zod"],
},
{
name: "form-tanstack-complex",
type: "registry:example",
registryDependencies: [
"field",
"button",
"card",
"checkbox",
"radio-group",
"select",
"switch",
],
files: [
{
path: "examples/form-tanstack-complex.tsx",
type: "registry:example",
},
],
dependencies: ["@tanstack/react-form", "zod"],
},
{
name: "drawer-dialog",
type: "registry:example",

View File

@@ -27,15 +27,21 @@
"@billingsdk": "https://billingsdk.com/r/{name}.json",
"@elements": "https://tryelements.dev/r/{name}.json",
"@nativeui": "https://nativeui.io/registry/{name}.json",
"@scrollxui": "https://www.scrollxui.dev/registry/{name}.json",
"@smoothui": "https://smoothui.dev/r/{name}.json",
"@svgl": "https://svgl.app/r/{name}.json",
"@formcn": "https://formcn.dev/r/{name}.json",
"@limeplay": "https://limeplay.winoffrg.dev/r/{name}.json",
"@skiper-ui": "https://skiper-ui.com/registry/{name}.json",
"@shadcnblocks": "https://shadcnblocks.com/r/{name}.json",
"@shadcn-editor": "https://shadcn-editor.vercel.app/r/{name}.json",
"@shadcn-studio": "https://shadcnstudio.com/r/{name}.json",
"@rigidui": "https://rigidui.com/r/{name}.json",
"@skyr": "https://ui-play.skyroc.me/r/{name}.json",
"@retroui": "https://retroui.dev/r/{name}.json",
"@wds": "https://wds-shadcn-registry.netlify.app/r/{name}.json",
"@97cn": "https://97cn.itzik.co/r/{name}.json"
"@97cn": "https://97cn.itzik.co/r/{name}.json",
"@better-upload": "https://better-upload.com/r/{name}.json",
"@zippystarter": "https://zippystarter.com/r/{name}.json",
"@elevenlabs-ui": "https://ui.elevenlabs.io/r/{name}.json"
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-next-demo",
"type": "registry:example",
"registryDependencies": [
"field",
"input",
"textarea",
"button",
"card",
"spinner"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-next-demo.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport Form from \"next/form\"\nimport { toast } from \"sonner\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport {\n InputGroup,\n InputGroupAddon,\n InputGroupText,\n InputGroupTextarea,\n} from \"@/registry/new-york-v4/ui/input-group\"\nimport { Spinner } from \"@/registry/new-york-v4/ui/spinner\"\n\nimport { demoFormAction } from \"./form-next-demo-action\"\nimport { type FormState } from \"./form-next-demo-schema\"\n\nexport default function FormNextDemo() {\n const [formState, formAction, pending] = React.useActionState<\n FormState,\n FormData\n >(demoFormAction, {\n values: {\n title: \"\",\n description: \"\",\n },\n errors: null,\n success: false,\n })\n const [descriptionLength, setDescriptionLength] = React.useState(0)\n\n React.useEffect(() => {\n if (formState.success) {\n toast(\"Thank you for your feedback\", {\n description: \"We'll review your report and get back to you soon.\",\n })\n }\n }, [formState.success])\n\n React.useEffect(() => {\n setDescriptionLength(formState.values.description.length)\n }, [formState.values.description])\n\n return (\n <Card className=\"w-full max-w-md\">\n <CardHeader>\n <CardTitle>Bug Report</CardTitle>\n <CardDescription>\n Help us improve by reporting bugs you encounter.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <Form action={formAction} id=\"bug-report-form\">\n <FieldGroup>\n <Field data-invalid={!!formState.errors?.title?.length}>\n <FieldLabel htmlFor=\"title\">Bug Title</FieldLabel>\n <Input\n id=\"title\"\n name=\"title\"\n defaultValue={formState.values.title}\n disabled={pending}\n aria-invalid={!!formState.errors?.title?.length}\n placeholder=\"Login button not working on mobile\"\n autoComplete=\"off\"\n />\n {formState.errors?.title && (\n <FieldError>{formState.errors.title[0]}</FieldError>\n )}\n </Field>\n <Field data-invalid={!!formState.errors?.description?.length}>\n <FieldLabel htmlFor=\"description\">Description</FieldLabel>\n <InputGroup>\n <InputGroupTextarea\n id=\"description\"\n name=\"description\"\n defaultValue={formState.values.description}\n placeholder=\"I'm having an issue with the login button on mobile.\"\n rows={6}\n className=\"min-h-24 resize-none\"\n disabled={pending}\n aria-invalid={!!formState.errors?.description?.length}\n onChange={(e) => setDescriptionLength(e.target.value.length)}\n />\n <InputGroupAddon align=\"block-end\">\n <InputGroupText className=\"tabular-nums\">\n {descriptionLength}/100 characters\n </InputGroupText>\n </InputGroupAddon>\n </InputGroup>\n <FieldDescription>\n Include steps to reproduce, expected behavior, and what actually\n happened.\n </FieldDescription>\n {formState.errors?.description && (\n <FieldError>{formState.errors.description[0]}</FieldError>\n )}\n </Field>\n </FieldGroup>\n </Form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"submit\" disabled={pending} form=\"bug-report-form\">\n {pending && <Spinner />}\n Submit\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-rhf-demo",
"type": "registry:example",
"dependencies": [
"react-hook-form",
"@hookform/resolvers",
"zod"
],
"registryDependencies": [
"field",
"input",
"input-group",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-rhf-demo.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport {\n InputGroup,\n InputGroupAddon,\n InputGroupText,\n InputGroupTextarea,\n} from \"@/registry/new-york-v4/ui/input-group\"\n\nconst formSchema = z.object({\n title: z\n .string()\n .min(5, \"Bug title must be at least 5 characters.\")\n .max(32, \"Bug title must be at most 32 characters.\"),\n description: z\n .string()\n .min(20, \"Description must be at least 20 characters.\")\n .max(100, \"Description must be at most 100 characters.\"),\n})\n\nexport default function BugReportForm() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n title: \"\",\n description: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Bug Report</CardTitle>\n <CardDescription>\n Help us improve by reporting bugs you encounter.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-demo\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"title\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field data-invalid={fieldState.invalid}>\n <FieldLabel htmlFor=\"form-rhf-demo-title\">\n Bug Title\n </FieldLabel>\n <Input\n {...field}\n id=\"form-rhf-demo-title\"\n aria-invalid={fieldState.invalid}\n placeholder=\"Login button not working on mobile\"\n autoComplete=\"off\"\n />\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </Field>\n )}\n />\n <Controller\n name=\"description\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field data-invalid={fieldState.invalid}>\n <FieldLabel htmlFor=\"form-rhf-demo-description\">\n Description\n </FieldLabel>\n <InputGroup>\n <InputGroupTextarea\n {...field}\n id=\"form-rhf-demo-description\"\n placeholder=\"I'm having an issue with the login button on mobile.\"\n rows={6}\n className=\"min-h-24 resize-none\"\n aria-invalid={fieldState.invalid}\n />\n <InputGroupAddon align=\"block-end\">\n <InputGroupText className=\"tabular-nums\">\n {field.value.length}/100 characters\n </InputGroupText>\n </InputGroupAddon>\n </InputGroup>\n <FieldDescription>\n Include steps to reproduce, expected behavior, and what\n actually happened.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-demo\">\n Submit\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-rhf-input",
"type": "registry:example",
"dependencies": [
"react-hook-form",
"@hookform/resolvers",
"zod"
],
"registryDependencies": [
"field",
"input",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-rhf-input.tsx",
"content": "\"use client\"\n\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\n\nconst formSchema = z.object({\n username: z\n .string()\n .min(3, \"Username must be at least 3 characters.\")\n .max(10, \"Username must be at most 10 characters.\")\n .regex(\n /^[a-zA-Z0-9_]+$/,\n \"Username can only contain letters, numbers, and underscores.\"\n ),\n})\n\nexport default function FormRhfInput() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n username: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Profile Settings</CardTitle>\n <CardDescription>\n Update your profile information below.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-input\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"username\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field data-invalid={fieldState.invalid}>\n <FieldLabel htmlFor=\"form-rhf-input-username\">\n Username\n </FieldLabel>\n <Input\n {...field}\n id=\"form-rhf-input-username\"\n aria-invalid={fieldState.invalid}\n placeholder=\"shadcn\"\n autoComplete=\"username\"\n />\n <FieldDescription>\n This is your public display name. Must be between 3 and 10\n characters. Must only contain letters, numbers, and\n underscores.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-input\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-rhf-radiogroup",
"type": "registry:example",
"dependencies": [
"react-hook-form",
"@hookform/resolvers",
"zod"
],
"registryDependencies": [
"field",
"radio-group",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-rhf-radiogroup.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSet,\n FieldTitle,\n} from \"@/registry/new-york-v4/ui/field\"\nimport {\n RadioGroup,\n RadioGroupItem,\n} from \"@/registry/new-york-v4/ui/radio-group\"\n\nconst plans = [\n {\n id: \"starter\",\n title: \"Starter (100K tokens/month)\",\n description: \"For everyday use with basic features.\",\n },\n {\n id: \"pro\",\n title: \"Pro (1M tokens/month)\",\n description: \"For advanced AI usage with more features.\",\n },\n {\n id: \"enterprise\",\n title: \"Enterprise (Unlimited tokens)\",\n description: \"For large teams and heavy usage.\",\n },\n] as const\n\nconst formSchema = z.object({\n plan: z.string().min(1, \"You must select a subscription plan to continue.\"),\n})\n\nexport default function FormRhfRadioGroup() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n plan: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Subscription Plan</CardTitle>\n <CardDescription>\n See pricing and features for each plan.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-radiogroup\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"plan\"\n control={form.control}\n render={({ field, fieldState }) => (\n <FieldSet data-invalid={fieldState.invalid}>\n <FieldLegend>Plan</FieldLegend>\n <FieldDescription>\n You can upgrade or downgrade your plan at any time.\n </FieldDescription>\n <RadioGroup\n name={field.name}\n value={field.value}\n onValueChange={field.onChange}\n aria-invalid={fieldState.invalid}\n >\n {plans.map((plan) => (\n <FieldLabel\n key={plan.id}\n htmlFor={`form-rhf-radiogroup-${plan.id}`}\n >\n <Field\n orientation=\"horizontal\"\n data-invalid={fieldState.invalid}\n >\n <FieldContent>\n <FieldTitle>{plan.title}</FieldTitle>\n <FieldDescription>\n {plan.description}\n </FieldDescription>\n </FieldContent>\n <RadioGroupItem\n value={plan.id}\n id={`form-rhf-radiogroup-${plan.id}`}\n aria-invalid={fieldState.invalid}\n />\n </Field>\n </FieldLabel>\n ))}\n </RadioGroup>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </FieldSet>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-radiogroup\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-rhf-select",
"type": "registry:example",
"dependencies": [
"react-hook-form",
"@hookform/resolvers",
"zod"
],
"registryDependencies": [
"field",
"select",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-rhf-select.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/new-york-v4/ui/select\"\n\nconst spokenLanguages = [\n { label: \"English\", value: \"en\" },\n { label: \"Spanish\", value: \"es\" },\n { label: \"French\", value: \"fr\" },\n { label: \"German\", value: \"de\" },\n { label: \"Italian\", value: \"it\" },\n { label: \"Chinese\", value: \"zh\" },\n { label: \"Japanese\", value: \"ja\" },\n] as const\n\nconst formSchema = z.object({\n language: z\n .string()\n .min(1, \"Please select your spoken language.\")\n .refine((val) => val !== \"auto\", {\n message:\n \"Auto-detection is not allowed. Please select a specific language.\",\n }),\n})\n\nexport default function FormRhfSelect() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n language: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-lg\">\n <CardHeader>\n <CardTitle>Language Preferences</CardTitle>\n <CardDescription>\n Select your preferred spoken language.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-select\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"language\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field\n orientation=\"responsive\"\n data-invalid={fieldState.invalid}\n >\n <FieldContent>\n <FieldLabel htmlFor=\"form-rhf-select-language\">\n Spoken Language\n </FieldLabel>\n <FieldDescription>\n For best results, select the language you speak.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </FieldContent>\n <Select\n name={field.name}\n value={field.value}\n onValueChange={field.onChange}\n >\n <SelectTrigger\n id=\"form-rhf-select-language\"\n aria-invalid={fieldState.invalid}\n className=\"min-w-[120px]\"\n >\n <SelectValue placeholder=\"Select\" />\n </SelectTrigger>\n <SelectContent position=\"item-aligned\">\n <SelectItem value=\"auto\">Auto</SelectItem>\n <SelectSeparator />\n {spokenLanguages.map((language) => (\n <SelectItem key={language.value} value={language.value}>\n {language.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-select\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-rhf-switch",
"type": "registry:example",
"dependencies": [
"react-hook-form",
"@hookform/resolvers",
"zod"
],
"registryDependencies": [
"field",
"switch",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-rhf-switch.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Switch } from \"@/registry/new-york-v4/ui/switch\"\n\nconst formSchema = z.object({\n twoFactor: z.boolean().refine((val) => val === true, {\n message: \"It is highly recommended to enable two-factor authentication.\",\n }),\n})\n\nexport default function FormRhfSwitch() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n twoFactor: false,\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Security Settings</CardTitle>\n <CardDescription>\n Manage your account security preferences.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-switch\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"twoFactor\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field\n orientation=\"horizontal\"\n data-invalid={fieldState.invalid}\n >\n <FieldContent>\n <FieldLabel htmlFor=\"form-rhf-switch-twoFactor\">\n Multi-factor authentication\n </FieldLabel>\n <FieldDescription>\n Enable multi-factor authentication to secure your account.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </FieldContent>\n <Switch\n id=\"form-rhf-switch-twoFactor\"\n name={field.name}\n checked={field.value}\n onCheckedChange={field.onChange}\n aria-invalid={fieldState.invalid}\n />\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-switch\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-rhf-textarea",
"type": "registry:example",
"dependencies": [
"react-hook-form",
"@hookform/resolvers",
"zod"
],
"registryDependencies": [
"field",
"textarea",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-rhf-textarea.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { zodResolver } from \"@hookform/resolvers/zod\"\nimport { Controller, useForm } from \"react-hook-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Textarea } from \"@/registry/new-york-v4/ui/textarea\"\n\nconst formSchema = z.object({\n about: z\n .string()\n .min(10, \"Please provide at least 10 characters.\")\n .max(200, \"Please keep it under 200 characters.\"),\n})\n\nexport default function FormRhfTextarea() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n defaultValues: {\n about: \"\",\n },\n })\n\n function onSubmit(data: z.infer<typeof formSchema>) {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(data, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Personalization</CardTitle>\n <CardDescription>\n Customize your experience by telling us more about yourself.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form id=\"form-rhf-textarea\" onSubmit={form.handleSubmit(onSubmit)}>\n <FieldGroup>\n <Controller\n name=\"about\"\n control={form.control}\n render={({ field, fieldState }) => (\n <Field data-invalid={fieldState.invalid}>\n <FieldLabel htmlFor=\"form-rhf-textarea-about\">\n More about you\n </FieldLabel>\n <Textarea\n {...field}\n id=\"form-rhf-textarea-about\"\n aria-invalid={fieldState.invalid}\n placeholder=\"I'm a software engineer...\"\n className=\"min-h-[120px]\"\n />\n <FieldDescription>\n Tell us more about yourself. This will be used to help us\n personalize your experience.\n </FieldDescription>\n {fieldState.invalid && (\n <FieldError errors={[fieldState.error]} />\n )}\n </Field>\n )}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-rhf-textarea\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-tanstack-input",
"type": "registry:example",
"dependencies": [
"@tanstack/react-form",
"zod"
],
"registryDependencies": [
"field",
"input",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-tanstack-input.tsx",
"content": "/* eslint-disable react/no-children-prop */\n\"use client\"\n\nimport { useForm } from \"@tanstack/react-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\n\nconst formSchema = z.object({\n username: z\n .string()\n .min(3, \"Username must be at least 3 characters.\")\n .max(10, \"Username must be at most 10 characters.\")\n .regex(\n /^[a-zA-Z0-9_]+$/,\n \"Username can only contain letters, numbers, and underscores.\"\n ),\n})\n\nexport default function FormTanstackInput() {\n const form = useForm({\n defaultValues: {\n username: \"\",\n },\n validators: {\n onSubmit: formSchema,\n },\n onSubmit: async ({ value }) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(value, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n },\n })\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Profile Settings</CardTitle>\n <CardDescription>\n Update your profile information below.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form\n id=\"form-tanstack-input\"\n onSubmit={(e) => {\n e.preventDefault()\n form.handleSubmit()\n }}\n >\n <FieldGroup>\n <form.Field\n name=\"username\"\n children={(field) => {\n const isInvalid =\n field.state.meta.isTouched && !field.state.meta.isValid\n return (\n <Field data-invalid={isInvalid}>\n <FieldLabel htmlFor=\"form-tanstack-input-username\">\n Username\n </FieldLabel>\n <Input\n id=\"form-tanstack-input-username\"\n name={field.name}\n value={field.state.value}\n onBlur={field.handleBlur}\n onChange={(e) => field.handleChange(e.target.value)}\n aria-invalid={isInvalid}\n placeholder=\"shadcn\"\n autoComplete=\"username\"\n />\n <FieldDescription>\n This is your public display name. Must be between 3 and 10\n characters. Must only contain letters, numbers, and\n underscores.\n </FieldDescription>\n {isInvalid && (\n <FieldError errors={field.state.meta.errors} />\n )}\n </Field>\n )\n }}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-tanstack-input\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

View File

@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-tanstack-radiogroup",
"type": "registry:example",
"dependencies": [
"@tanstack/react-form",
"zod"
],
"registryDependencies": [
"field",
"radio-group",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-tanstack-radiogroup.tsx",
"content": "/* eslint-disable react/no-children-prop */\n\"use client\"\n\nimport { useForm } from \"@tanstack/react-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSet,\n FieldTitle,\n} from \"@/registry/new-york-v4/ui/field\"\nimport {\n RadioGroup,\n RadioGroupItem,\n} from \"@/registry/new-york-v4/ui/radio-group\"\n\nconst plans = [\n {\n id: \"starter\",\n title: \"Starter (100K tokens/month)\",\n description: \"For everyday use with basic features.\",\n },\n {\n id: \"pro\",\n title: \"Pro (1M tokens/month)\",\n description: \"For advanced AI usage with more features.\",\n },\n {\n id: \"enterprise\",\n title: \"Enterprise (Unlimited tokens)\",\n description: \"For large teams and heavy usage.\",\n },\n] as const\n\nconst formSchema = z.object({\n plan: z.string().min(1, \"You must select a subscription plan to continue.\"),\n})\n\nexport default function FormTanstackRadioGroup() {\n const form = useForm({\n defaultValues: {\n plan: \"\",\n },\n validators: {\n onSubmit: formSchema,\n },\n onSubmit: async ({ value }) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(value, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n },\n })\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Subscription Plan</CardTitle>\n <CardDescription>\n See pricing and features for each plan.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form\n id=\"form-tanstack-radiogroup\"\n onSubmit={(e) => {\n e.preventDefault()\n form.handleSubmit()\n }}\n >\n <FieldGroup>\n <form.Field\n name=\"plan\"\n children={(field) => {\n const isInvalid =\n field.state.meta.isTouched && !field.state.meta.isValid\n return (\n <FieldSet>\n <FieldLegend>Plan</FieldLegend>\n <FieldDescription>\n You can upgrade or downgrade your plan at any time.\n </FieldDescription>\n <RadioGroup\n name={field.name}\n value={field.state.value}\n onValueChange={field.handleChange}\n >\n {plans.map((plan) => (\n <FieldLabel\n key={plan.id}\n htmlFor={`form-tanstack-radiogroup-${plan.id}`}\n >\n <Field\n orientation=\"horizontal\"\n data-invalid={isInvalid}\n >\n <FieldContent>\n <FieldTitle>{plan.title}</FieldTitle>\n <FieldDescription>\n {plan.description}\n </FieldDescription>\n </FieldContent>\n <RadioGroupItem\n value={plan.id}\n id={`form-tanstack-radiogroup-${plan.id}`}\n aria-invalid={isInvalid}\n />\n </Field>\n </FieldLabel>\n ))}\n </RadioGroup>\n {isInvalid && (\n <FieldError errors={field.state.meta.errors} />\n )}\n </FieldSet>\n )\n }}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-tanstack-radiogroup\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-tanstack-switch",
"type": "registry:example",
"dependencies": [
"@tanstack/react-form",
"zod"
],
"registryDependencies": [
"field",
"switch",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-tanstack-switch.tsx",
"content": "/* eslint-disable react/no-children-prop */\n\"use client\"\n\nimport { useForm } from \"@tanstack/react-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Switch } from \"@/registry/new-york-v4/ui/switch\"\n\nconst formSchema = z.object({\n twoFactor: z.boolean().refine((val) => val === true, {\n message: \"It is highly recommended to enable two-factor authentication.\",\n }),\n})\n\nexport default function FormTanstackSwitch() {\n const form = useForm({\n defaultValues: {\n twoFactor: false,\n },\n validators: {\n onSubmit: formSchema,\n },\n onSubmit: async ({ value }) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(value, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n },\n })\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Security Settings</CardTitle>\n <CardDescription>\n Manage your account security preferences.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form\n id=\"form-tanstack-switch\"\n onSubmit={(e) => {\n e.preventDefault()\n form.handleSubmit()\n }}\n >\n <FieldGroup>\n <form.Field\n name=\"twoFactor\"\n children={(field) => {\n const isInvalid =\n field.state.meta.isTouched && !field.state.meta.isValid\n return (\n <Field orientation=\"horizontal\" data-invalid={isInvalid}>\n <FieldContent>\n <FieldLabel htmlFor=\"form-tanstack-switch-twoFactor\">\n Multi-factor authentication\n </FieldLabel>\n <FieldDescription>\n Enable multi-factor authentication to secure your\n account.\n </FieldDescription>\n {isInvalid && (\n <FieldError errors={field.state.meta.errors} />\n )}\n </FieldContent>\n <Switch\n id=\"form-tanstack-switch-twoFactor\"\n name={field.name}\n checked={field.state.value}\n onCheckedChange={field.handleChange}\n aria-invalid={isInvalid}\n />\n </Field>\n )\n }}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-tanstack-switch\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

View File

@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "form-tanstack-textarea",
"type": "registry:example",
"dependencies": [
"@tanstack/react-form",
"zod"
],
"registryDependencies": [
"field",
"textarea",
"button",
"card"
],
"files": [
{
"path": "registry/new-york-v4/examples/form-tanstack-textarea.tsx",
"content": "/* eslint-disable react/no-children-prop */\n\"use client\"\n\nimport { useForm } from \"@tanstack/react-form\"\nimport { toast } from \"sonner\"\nimport * as z from \"zod\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Textarea } from \"@/registry/new-york-v4/ui/textarea\"\n\nconst formSchema = z.object({\n about: z\n .string()\n .min(10, \"Please provide at least 10 characters.\")\n .max(200, \"Please keep it under 200 characters.\"),\n})\n\nexport default function FormTanstackTextarea() {\n const form = useForm({\n defaultValues: {\n about: \"\",\n },\n validators: {\n onSubmit: formSchema,\n },\n onSubmit: async ({ value }) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4\">\n <code>{JSON.stringify(value, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n },\n })\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Personalization</CardTitle>\n <CardDescription>\n Customize your experience by telling us more about yourself.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form\n id=\"form-tanstack-textarea\"\n onSubmit={(e) => {\n e.preventDefault()\n form.handleSubmit()\n }}\n >\n <FieldGroup>\n <form.Field\n name=\"about\"\n children={(field) => {\n const isInvalid =\n field.state.meta.isTouched && !field.state.meta.isValid\n return (\n <Field data-invalid={isInvalid}>\n <FieldLabel htmlFor=\"form-tanstack-textarea-about\">\n More about you\n </FieldLabel>\n <Textarea\n id=\"form-tanstack-textarea-about\"\n name={field.name}\n value={field.state.value}\n onBlur={field.handleBlur}\n onChange={(e) => field.handleChange(e.target.value)}\n aria-invalid={isInvalid}\n placeholder=\"I'm a software engineer...\"\n className=\"min-h-[120px]\"\n />\n <FieldDescription>\n Tell us more about yourself. This will be used to help us\n personalize your experience.\n </FieldDescription>\n {isInvalid && (\n <FieldError errors={field.state.meta.errors} />\n )}\n </Field>\n )\n }}\n />\n </FieldGroup>\n </form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => form.reset()}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-tanstack-textarea\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
"type": "registry:example"
}
]
}

File diff suppressed because it is too large Load Diff

892
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff