mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-08 22:45:47 +00:00
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:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ const TOP_LEVEL_SECTIONS = [
|
||||
name: "MCP Server",
|
||||
href: "/docs/mcp",
|
||||
},
|
||||
{
|
||||
name: "Forms",
|
||||
href: "/docs/forms",
|
||||
},
|
||||
{
|
||||
name: "Changelog",
|
||||
href: "/docs/changelog",
|
||||
|
||||
@@ -29,6 +29,10 @@ const TOP_LEVEL_SECTIONS = [
|
||||
name: "MCP Server",
|
||||
href: "/docs/mcp",
|
||||
},
|
||||
{
|
||||
name: "Forms",
|
||||
href: "/docs/forms",
|
||||
},
|
||||
{
|
||||
name: "Changelog",
|
||||
href: "/docs/changelog",
|
||||
|
||||
@@ -56,9 +56,3 @@ import { Checkbox } from "@/components/ui/checkbox"
|
||||
```tsx
|
||||
<Checkbox />
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentPreview name="checkbox-form-multiple" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -69,9 +69,3 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
</div>
|
||||
</RadioGroup>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentPreview name="radio-group-form" />
|
||||
|
||||
@@ -84,7 +84,3 @@ import {
|
||||
name="select-scrollable"
|
||||
description="A select component with a scrollable list of options."
|
||||
/>
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentPreview name="select-form" />
|
||||
|
||||
@@ -56,9 +56,3 @@ import { Switch } from "@/components/ui/switch"
|
||||
```tsx
|
||||
<Switch />
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Form
|
||||
|
||||
<ComponentPreview name="switch-form" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
45
apps/v4/content/docs/forms/index.mdx
Normal file
45
apps/v4/content/docs/forms/index.mdx
Normal 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>
|
||||
3
apps/v4/content/docs/forms/meta.json
Normal file
3
apps/v4/content/docs/forms/meta.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"pages": ["react-hook-form", "tanstack-form"]
|
||||
}
|
||||
397
apps/v4/content/docs/forms/next.mdx
Normal file
397
apps/v4/content/docs/forms/next.mdx
Normal 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"
|
||||
/>
|
||||
629
apps/v4/content/docs/forms/react-hook-form.mdx
Normal file
629
apps/v4/content/docs/forms/react-hook-form.mdx
Normal 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'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."),
|
||||
})
|
||||
```
|
||||
698
apps/v4/content/docs/forms/tanstack-form.mdx
Normal file
698
apps/v4/content/docs/forms/tanstack-form.mdx
Normal 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'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."),
|
||||
})
|
||||
```
|
||||
@@ -4,6 +4,7 @@
|
||||
"(root)",
|
||||
"changelog",
|
||||
"components",
|
||||
"forms",
|
||||
"installation",
|
||||
"dark-mode",
|
||||
"registry"
|
||||
|
||||
@@ -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",
|
||||
|
||||
35
apps/v4/public/r/styles/new-york-v4/form-next-complex.json
Normal file
35
apps/v4/public/r/styles/new-york-v4/form-next-complex.json
Normal file
File diff suppressed because one or more lines are too long
20
apps/v4/public/r/styles/new-york-v4/form-next-demo.json
Normal file
20
apps/v4/public/r/styles/new-york-v4/form-next-demo.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
apps/v4/public/r/styles/new-york-v4/form-rhf-array.json
Normal file
24
apps/v4/public/r/styles/new-york-v4/form-rhf-array.json
Normal file
File diff suppressed because one or more lines are too long
23
apps/v4/public/r/styles/new-york-v4/form-rhf-checkbox.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-rhf-checkbox.json
Normal file
File diff suppressed because one or more lines are too long
26
apps/v4/public/r/styles/new-york-v4/form-rhf-complex.json
Normal file
26
apps/v4/public/r/styles/new-york-v4/form-rhf-complex.json
Normal file
File diff suppressed because one or more lines are too long
24
apps/v4/public/r/styles/new-york-v4/form-rhf-demo.json
Normal file
24
apps/v4/public/r/styles/new-york-v4/form-rhf-demo.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/v4/public/r/styles/new-york-v4/form-rhf-input.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-rhf-input.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
apps/v4/public/r/styles/new-york-v4/form-rhf-password.json
Normal file
24
apps/v4/public/r/styles/new-york-v4/form-rhf-password.json
Normal file
File diff suppressed because one or more lines are too long
23
apps/v4/public/r/styles/new-york-v4/form-rhf-radiogroup.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-rhf-radiogroup.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/v4/public/r/styles/new-york-v4/form-rhf-select.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-rhf-select.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/v4/public/r/styles/new-york-v4/form-rhf-switch.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-rhf-switch.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/v4/public/r/styles/new-york-v4/form-rhf-textarea.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-rhf-textarea.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/v4/public/r/styles/new-york-v4/form-tanstack-array.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-tanstack-array.json
Normal file
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
23
apps/v4/public/r/styles/new-york-v4/form-tanstack-demo.json
Normal file
23
apps/v4/public/r/styles/new-york-v4/form-tanstack-demo.json
Normal file
File diff suppressed because one or more lines are too long
22
apps/v4/public/r/styles/new-york-v4/form-tanstack-input.json
Normal file
22
apps/v4/public/r/styles/new-york-v4/form-tanstack-input.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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: "",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
194
apps/v4/registry/new-york-v4/examples/form-next-complex.tsx
Normal file
194
apps/v4/registry/new-york-v4/examples/form-next-complex.tsx
Normal 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'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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
128
apps/v4/registry/new-york-v4/examples/form-next-demo.tsx
Normal file
128
apps/v4/registry/new-york-v4/examples/form-next-demo.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
160
apps/v4/registry/new-york-v4/examples/form-rhf-array.tsx
Normal file
160
apps/v4/registry/new-york-v4/examples/form-rhf-array.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
181
apps/v4/registry/new-york-v4/examples/form-rhf-checkbox.tsx
Normal file
181
apps/v4/registry/new-york-v4/examples/form-rhf-checkbox.tsx
Normal 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'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>
|
||||
)
|
||||
}
|
||||
307
apps/v4/registry/new-york-v4/examples/form-rhf-complex.tsx
Normal file
307
apps/v4/registry/new-york-v4/examples/form-rhf-complex.tsx
Normal 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'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'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>
|
||||
)
|
||||
}
|
||||
150
apps/v4/registry/new-york-v4/examples/form-rhf-demo.tsx
Normal file
150
apps/v4/registry/new-york-v4/examples/form-rhf-demo.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
114
apps/v4/registry/new-york-v4/examples/form-rhf-input.tsx
Normal file
114
apps/v4/registry/new-york-v4/examples/form-rhf-input.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
221
apps/v4/registry/new-york-v4/examples/form-rhf-password.tsx
Normal file
221
apps/v4/registry/new-york-v4/examples/form-rhf-password.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
152
apps/v4/registry/new-york-v4/examples/form-rhf-radiogroup.tsx
Normal file
152
apps/v4/registry/new-york-v4/examples/form-rhf-radiogroup.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
150
apps/v4/registry/new-york-v4/examples/form-rhf-select.tsx
Normal file
150
apps/v4/registry/new-york-v4/examples/form-rhf-select.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
114
apps/v4/registry/new-york-v4/examples/form-rhf-switch.tsx
Normal file
114
apps/v4/registry/new-york-v4/examples/form-rhf-switch.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
110
apps/v4/registry/new-york-v4/examples/form-rhf-textarea.tsx
Normal file
110
apps/v4/registry/new-york-v4/examples/form-rhf-textarea.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
179
apps/v4/registry/new-york-v4/examples/form-tanstack-array.tsx
Normal file
179
apps/v4/registry/new-york-v4/examples/form-tanstack-array.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
200
apps/v4/registry/new-york-v4/examples/form-tanstack-checkbox.tsx
Normal file
200
apps/v4/registry/new-york-v4/examples/form-tanstack-checkbox.tsx
Normal 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'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>
|
||||
)
|
||||
}
|
||||
313
apps/v4/registry/new-york-v4/examples/form-tanstack-complex.tsx
Normal file
313
apps/v4/registry/new-york-v4/examples/form-tanstack-complex.tsx
Normal 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'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>
|
||||
)
|
||||
}
|
||||
165
apps/v4/registry/new-york-v4/examples/form-tanstack-demo.tsx
Normal file
165
apps/v4/registry/new-york-v4/examples/form-tanstack-demo.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
127
apps/v4/registry/new-york-v4/examples/form-tanstack-input.tsx
Normal file
127
apps/v4/registry/new-york-v4/examples/form-tanstack-input.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
159
apps/v4/registry/new-york-v4/examples/form-tanstack-select.tsx
Normal file
159
apps/v4/registry/new-york-v4/examples/form-tanstack-select.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
121
apps/v4/registry/new-york-v4/examples/form-tanstack-switch.tsx
Normal file
121
apps/v4/registry/new-york-v4/examples/form-tanstack-switch.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
122
apps/v4/registry/new-york-v4/examples/form-tanstack-textarea.tsx
Normal file
122
apps/v4/registry/new-york-v4/examples/form-tanstack-textarea.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
35
apps/www/public/r/styles/new-york-v4/form-next-complex.json
Normal file
35
apps/www/public/r/styles/new-york-v4/form-next-complex.json
Normal file
File diff suppressed because one or more lines are too long
20
apps/www/public/r/styles/new-york-v4/form-next-demo.json
Normal file
20
apps/www/public/r/styles/new-york-v4/form-next-demo.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
apps/www/public/r/styles/new-york-v4/form-rhf-array.json
Normal file
24
apps/www/public/r/styles/new-york-v4/form-rhf-array.json
Normal file
File diff suppressed because one or more lines are too long
23
apps/www/public/r/styles/new-york-v4/form-rhf-checkbox.json
Normal file
23
apps/www/public/r/styles/new-york-v4/form-rhf-checkbox.json
Normal file
File diff suppressed because one or more lines are too long
26
apps/www/public/r/styles/new-york-v4/form-rhf-complex.json
Normal file
26
apps/www/public/r/styles/new-york-v4/form-rhf-complex.json
Normal file
File diff suppressed because one or more lines are too long
24
apps/www/public/r/styles/new-york-v4/form-rhf-demo.json
Normal file
24
apps/www/public/r/styles/new-york-v4/form-rhf-demo.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/www/public/r/styles/new-york-v4/form-rhf-input.json
Normal file
23
apps/www/public/r/styles/new-york-v4/form-rhf-input.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
apps/www/public/r/styles/new-york-v4/form-rhf-password.json
Normal file
24
apps/www/public/r/styles/new-york-v4/form-rhf-password.json
Normal file
File diff suppressed because one or more lines are too long
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/www/public/r/styles/new-york-v4/form-rhf-select.json
Normal file
23
apps/www/public/r/styles/new-york-v4/form-rhf-select.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/www/public/r/styles/new-york-v4/form-rhf-switch.json
Normal file
23
apps/www/public/r/styles/new-york-v4/form-rhf-switch.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/www/public/r/styles/new-york-v4/form-rhf-textarea.json
Normal file
23
apps/www/public/r/styles/new-york-v4/form-rhf-textarea.json
Normal 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
23
apps/www/public/r/styles/new-york-v4/form-tanstack-demo.json
Normal file
23
apps/www/public/r/styles/new-york-v4/form-tanstack-demo.json
Normal file
File diff suppressed because one or more lines are too long
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
892
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user