Make useForm compatible with shadcn Form component out of the box #2046
Replies: 4 comments 12 replies
-
Would be fantastic, currently I'm struggling with the same |
Beta Was this translation helpful? Give feedback.
-
Use import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/Components/ui/form';
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form"
import { z } from "zod"
import { router } from '@inertiajs/react'
const formSchema = z.object({
title: z.string().min(5).max(50),
description: z.string().min(5).max(500),
})
export function YourForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
description: "",
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
router.post('/posts', values);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} >
// your shadcn FormFields
</form>
</Form>
);
} you can also pass the edit: add formater typescript to the block code, mb |
Beta Was this translation helpful? Give feedback.
-
i made this adaptation in shadcn form to work with inertiajs forms, i think is the best to not repeat me self and have to manually put id,htmlFor,handle form erros, handle form changes in every field, change class and etc: "use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input"
interface TForm extends Record<string, unknown> {}
export type FormContextType = {
data: TForm
setData: <K extends keyof TForm>(key: K, value: TForm[K]) => void
errors: Partial<Record<keyof TForm, string>>
}
// Define como unknown para permitir atribuição genérica
const FormContext = React.createContext<FormContextType>({} as FormContextType)
const Form = ({
children,
form,
}: {
children: React.ReactNode
form: FormContextType
}) => (
<FormContext.Provider value={form as FormContextType}>
{children}
</FormContext.Provider>
)
type FormFieldContextValue<TFieldName extends string> = {
name: TFieldName
}
const FormFieldContext = React.createContext<FormFieldContextValue<string> | null>(null)
const FormField = <TFieldName extends string>({
name,
children,
}: {
name: TFieldName
children: React.ReactNode
}) => (
<FormFieldContext.Provider value={{ name }}>
{children}
</FormFieldContext.Provider>
)
type FormItemContextValue = { id: string }
const FormItemContext = React.createContext<FormItemContextValue | null>(null)
const FormItem = ({ className, ...props }: React.ComponentProps<'div'>) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const form = React.useContext(FormContext)
if (!fieldContext || !itemContext) {
throw new Error("useFormField must be used within <FormField> and <FormItem>")
}
if (!form) {
throw new Error("useFormField must be used within <Form>")
}
const { name } = fieldContext
const { id } = itemContext
const value = form.data[name as keyof TForm]
const setData = form.setData
const error = form.errors[name as keyof TForm]
return {
id,
name,
value,
setData,
error,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
}
}
const FormLabel = ({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) => {
const { formItemId, error } = useFormField()
return (
<Label
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
const FormInput = (props: React.ComponentProps<'input'>) => {
const inputRef = React.useRef<HTMLInputElement>(null)
const { formItemId, formDescriptionId, formMessageId, name, setData, value, error } = useFormField()
React.useEffect(() => {
if(error) inputRef.current?.focus()
},[error])
return (
<Input
id={formItemId}
aria-describedby={
error ? `${formDescriptionId} ${formMessageId}` : formDescriptionId
}
aria-invalid={!!error}
value={value as string}
onChange={(e) => setData(name, e.target.value)}
{...props}
ref={inputRef}
/>
)
}
const FormDescription = ({ className, ...props }: React.ComponentProps<'p'>) => {
const { formDescriptionId } = useFormField()
return (
<p
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
)
}
const FormMessage = ({ className, children, ...props }: React.ComponentProps<'p'>) => {
const { error, formMessageId } = useFormField()
const body = error ?? children
if (!body) return null
return (
<p
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
}
export {
Form,
FormField,
FormItem,
FormLabel,
FormInput,
FormDescription,
FormMessage,
useFormField,
} this is the example usage code: import InputError from '@/components/input-error';
import AppLayout from '@/layouts/app-layout';
import SettingsLayout from '@/layouts/settings/layout';
import { type BreadcrumbItem } from '@/types';
import { Transition } from '@headlessui/react';
import { Head, useForm } from '@inertiajs/react';
import { FormEventHandler, useRef } from 'react';
import HeadingSmall from '@/components/heading-small';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Form, FormField, FormInput, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Password settings',
href: '/settings/password',
},
];
export default function Password() {
const passwordInput = useRef<HTMLInputElement>(null);
const currentPasswordInput = useRef<HTMLInputElement>(null);
const form = useForm({
current_password: '',
password: '',
password_confirmation: '',
});
const updatePassword: FormEventHandler = (e) => {
e.preventDefault();
form.put(route('password.update'), {
preserveScroll: true,
onSuccess: () => form.reset(),
onError: (errors) => {
if (errors.password) {
form.reset('password', 'password_confirmation');
passwordInput.current?.focus();
}
if (errors.current_password) {
form.reset('current_password');
currentPasswordInput.current?.focus();
}
},
});
};
return (
<AppLayout breadcrumbs={breadcrumbs}>
<Head title="Profile settings" />
<SettingsLayout>
<div className="space-y-6">
<HeadingSmall title="Update password" description="Ensure your account is using a long, random password to stay secure" />
<Form form={form}>
<form onSubmit={updatePassword} className="space-y-6">
<div className="grid gap-2">
<FormField name="current_password">
<FormItem>
<FormLabel>Current password</FormLabel>
<FormInput
type="password"
className="mt-1 block w-full"
autoComplete="current-password"
placeholder="Current password"
/>
<FormMessage />
</FormItem>
</FormField>
</div>
<div className="grid gap-2">
<FormField name="password">
<FormItem>
<FormLabel>New password</FormLabel>
<FormInput
type="password"
className="mt-1 block w-full"
autoComplete="new-password"
placeholder="New password"
/>
<FormMessage />
</FormItem>
</FormField>
</div>
<div className="grid gap-2">
<FormField name="password_confirmation">
<FormItem>
<FormLabel>Confirm password</FormLabel>
<FormInput
type="password"
className="mt-1 block w-full"
autoComplete="new-password"
placeholder="Confirm password"
/>
<FormMessage />
</FormItem>
</FormField>
</div>
<div className="flex items-center gap-4">
<Button disabled={form.processing}>Save password</Button>
<Transition
show={form.recentlySuccessful}
enter="transition ease-in-out"
enterFrom="opacity-0"
leave="transition ease-in-out"
leaveTo="opacity-0"
>
<p className="text-sm text-neutral-600">Saved</p>
</Transition>
</div>
</form>
</Form>
</div>
</SettingsLayout>
</AppLayout>
);
} |
Beta Was this translation helpful? Give feedback.
-
I solved it for AutoForm like this:
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
shadcn/ui
is the most popular UI components collection for React. Its counterpart for Vue and Svelte are:shadcn-vue
shadcn-svelte
They all have the
Form
component to build well-designed HTML forms. Typically, they are wrappers around these libraries to provide composable components for building forms with validation using libraries suchZod
:They expect
useForm
to return some controls and attributes.For more details, here are the direct links to the documentation:
It would be nice to enhance
useForm
to useshadcn
forms out of the box. Thank you 🙏Beta Was this translation helpful? Give feedback.
All reactions