2025-03-24 12:29:52 +08:00
|
|
|
'use client'
|
2025-03-14 12:40:51 +08:00
|
|
|
|
2025-03-24 12:29:52 +08:00
|
|
|
import * as LabelPrimitive from '@radix-ui/react-label'
|
|
|
|
|
import {Slot} from '@radix-ui/react-slot'
|
2025-03-14 12:40:51 +08:00
|
|
|
import {
|
|
|
|
|
Controller,
|
|
|
|
|
FormProvider,
|
2025-03-24 12:29:52 +08:00
|
|
|
ControllerProps,
|
|
|
|
|
SubmitHandler,
|
|
|
|
|
FieldValues, useFormContext, FieldPath, UseFormReturn, ControllerRenderProps,
|
2025-04-14 16:00:46 +08:00
|
|
|
ControllerFieldState, UseFormStateReturn, FieldError, FieldErrors, SubmitErrorHandler,
|
2025-03-24 12:29:52 +08:00
|
|
|
} from 'react-hook-form'
|
2025-03-14 12:40:51 +08:00
|
|
|
|
2025-03-24 12:29:52 +08:00
|
|
|
import {merge} from '@/lib/utils'
|
|
|
|
|
import {Label} from '@/components/ui/label'
|
2025-03-14 12:40:51 +08:00
|
|
|
|
2025-04-16 09:43:12 +08:00
|
|
|
import React, {BaseSyntheticEvent, ComponentProps, createContext, ReactNode, useContext, useId} from 'react'
|
2025-03-14 12:40:51 +08:00
|
|
|
|
2025-03-24 12:29:52 +08:00
|
|
|
type FormProps<T extends FieldValues> = {
|
|
|
|
|
form: UseFormReturn<T>
|
2025-04-16 09:43:12 +08:00
|
|
|
onSubmit?: SubmitHandler<T>
|
2025-04-14 16:00:46 +08:00
|
|
|
onError?: SubmitErrorHandler<T>
|
2025-04-16 09:43:12 +08:00
|
|
|
handler?: (e?: React.BaseSyntheticEvent) => Promise<void>
|
2025-04-14 16:00:46 +08:00
|
|
|
} & Omit<ComponentProps<'form'>, 'onSubmit' | 'onError'>
|
2025-03-14 12:40:51 +08:00
|
|
|
|
2025-03-24 12:29:52 +08:00
|
|
|
function Form<T extends FieldValues>(rawProps: FormProps<T>) {
|
|
|
|
|
|
2025-04-16 09:43:12 +08:00
|
|
|
const {children, onSubmit, onError, handler, ...props} = rawProps
|
2025-03-24 12:29:52 +08:00
|
|
|
const form = props.form
|
2025-03-14 12:40:51 +08:00
|
|
|
|
2025-04-16 09:43:12 +08:00
|
|
|
const handle = handler || form.handleSubmit(
|
|
|
|
|
onSubmit || (_ => {}),
|
|
|
|
|
onError,
|
|
|
|
|
)
|
|
|
|
|
|
2025-03-14 12:40:51 +08:00
|
|
|
return (
|
2025-03-24 12:29:52 +08:00
|
|
|
<FormProvider {...form}>
|
2025-04-16 09:43:12 +08:00
|
|
|
<form {...props} onSubmit={async event => {
|
2025-04-08 11:21:58 +08:00
|
|
|
event.preventDefault()
|
|
|
|
|
event.stopPropagation()
|
2025-04-16 09:43:12 +08:00
|
|
|
await handle(event)
|
2025-04-08 11:21:58 +08:00
|
|
|
}}>
|
2025-03-24 12:29:52 +08:00
|
|
|
{children}
|
|
|
|
|
</form>
|
|
|
|
|
</FormProvider>
|
2025-03-14 12:40:51 +08:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-24 12:29:52 +08:00
|
|
|
type FormFieldProps<
|
|
|
|
|
V extends FieldValues = FieldValues,
|
|
|
|
|
N extends FieldPath<V> = FieldPath<V>,
|
|
|
|
|
> = {
|
|
|
|
|
label?: ReactNode
|
|
|
|
|
className?: string
|
|
|
|
|
children: (props: {
|
|
|
|
|
id: string
|
|
|
|
|
field: ControllerRenderProps<V, N>
|
|
|
|
|
fieldState: ControllerFieldState
|
|
|
|
|
formState: UseFormStateReturn<V>
|
|
|
|
|
}) => ReactNode
|
|
|
|
|
} & Omit<ControllerProps<V, N>, 'control' | 'render'>
|
|
|
|
|
|
|
|
|
|
type FormFieldContext = {
|
2025-03-14 12:40:51 +08:00
|
|
|
id: string
|
2025-03-24 12:29:52 +08:00
|
|
|
error?: FieldError
|
2025-03-14 12:40:51 +08:00
|
|
|
}
|
|
|
|
|
|
2025-03-24 12:29:52 +08:00
|
|
|
const FormFieldContext = createContext<FormFieldContext | null>(null)
|
2025-03-14 12:40:51 +08:00
|
|
|
|
2025-03-24 12:29:52 +08:00
|
|
|
function FormField<
|
|
|
|
|
V extends FieldValues = FieldValues,
|
|
|
|
|
N extends FieldPath<V> = FieldPath<V>,
|
|
|
|
|
>(props: FormFieldProps<V, N>) {
|
|
|
|
|
const form = useFormContext<V>()
|
|
|
|
|
const id = useId()
|
2025-03-14 12:40:51 +08:00
|
|
|
return (
|
2025-03-24 12:29:52 +08:00
|
|
|
<Controller<V, N> name={props.name} control={form.control} render={({field, fieldState, formState}) => (
|
|
|
|
|
<div data-slot="form-field" className={merge('grid gap-2', props.className)}>
|
|
|
|
|
<FormFieldContext value={{id: id, error: fieldState.error}}>
|
|
|
|
|
{!!props.label &&
|
|
|
|
|
<Label
|
|
|
|
|
data-slot="form-label"
|
2025-04-12 11:10:51 +08:00
|
|
|
data-fail={!!fieldState.error}
|
|
|
|
|
className={merge('data-[error=true]:text-fail')}
|
2025-03-24 12:29:52 +08:00
|
|
|
htmlFor={id}>
|
|
|
|
|
{props.label}
|
|
|
|
|
</Label>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<Slot
|
|
|
|
|
data-slot="form-control"
|
|
|
|
|
aria-invalid={!!fieldState.error}
|
|
|
|
|
aria-describedby={
|
|
|
|
|
!!fieldState.error
|
|
|
|
|
? `${id}-description`
|
|
|
|
|
: `${id}-description ${id}-message`
|
|
|
|
|
}>
|
|
|
|
|
{props.children({id, field, fieldState, formState})}
|
|
|
|
|
</Slot>
|
|
|
|
|
|
|
|
|
|
{!fieldState.error ? null : (
|
|
|
|
|
<p
|
|
|
|
|
data-slot="form-message"
|
2025-04-12 11:10:51 +08:00
|
|
|
className={merge('text-fail text-sm')}>
|
2025-03-24 12:29:52 +08:00
|
|
|
{fieldState.error?.message}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</FormFieldContext>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
2025-03-14 12:40:51 +08:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-24 12:29:52 +08:00
|
|
|
const useFormField = () => {
|
|
|
|
|
const context = useContext(FormFieldContext)
|
|
|
|
|
if (!context) {
|
|
|
|
|
throw new Error('FormField components must be used within a FormField component')
|
|
|
|
|
}
|
|
|
|
|
return context
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function FormLabel({className, ...props}: ComponentProps<typeof LabelPrimitive.Root>) {
|
|
|
|
|
const {id, error} = useFormField()
|
2025-03-14 12:40:51 +08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Label
|
|
|
|
|
data-slot="form-label"
|
2025-04-12 11:10:51 +08:00
|
|
|
data-fail={!!error}
|
|
|
|
|
className={merge('data-[error=true]:text-fail', className)}
|
2025-03-24 12:29:52 +08:00
|
|
|
htmlFor={id}
|
2025-03-14 12:40:51 +08:00
|
|
|
{...props}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-24 12:29:52 +08:00
|
|
|
function FormDescription({className, ...props}: ComponentProps<'p'>) {
|
|
|
|
|
const {id} = useFormField()
|
2025-03-14 12:40:51 +08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<p
|
|
|
|
|
data-slot="form-description"
|
2025-03-24 12:29:52 +08:00
|
|
|
id={`${id}-description`}
|
|
|
|
|
className={merge('text-muted-foreground text-sm', className)}
|
2025-03-14 12:40:51 +08:00
|
|
|
{...props}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-24 12:29:52 +08:00
|
|
|
function FormMessage({className, ...props}: ComponentProps<'p'>) {
|
|
|
|
|
const {id, error} = useFormField()
|
|
|
|
|
const body = error ? String(error?.message ?? '') : props.children
|
2025-03-14 12:40:51 +08:00
|
|
|
|
|
|
|
|
if (!body) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<p
|
|
|
|
|
data-slot="form-message"
|
2025-03-24 12:29:52 +08:00
|
|
|
id={`${id}-message`}
|
2025-04-12 11:10:51 +08:00
|
|
|
className={merge('text-fail text-sm', className)}
|
2025-03-14 12:40:51 +08:00
|
|
|
{...props}
|
|
|
|
|
>
|
|
|
|
|
{body}
|
|
|
|
|
</p>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export {
|
|
|
|
|
Form,
|
2025-03-24 12:29:52 +08:00
|
|
|
FormField,
|
2025-03-14 12:40:51 +08:00
|
|
|
FormLabel,
|
|
|
|
|
FormDescription,
|
|
|
|
|
FormMessage,
|
|
|
|
|
}
|