目录结构与表单组件结构调整

This commit is contained in:
2025-03-24 12:29:52 +08:00
parent 60155e9d9d
commit e16ef8e509
9 changed files with 530 additions and 615 deletions

View File

@@ -1,143 +1,145 @@
"use client"
'use client'
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import * as LabelPrimitive from '@radix-ui/react-label'
import {Slot} from '@radix-ui/react-slot'
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
ControllerProps,
SubmitHandler,
FieldValues, useFormContext, FieldPath, UseFormReturn, ControllerRenderProps,
ControllerFieldState, UseFormStateReturn, FieldError,
} from 'react-hook-form'
import { merge } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import {merge} from '@/lib/utils'
import {Label} from '@/components/ui/label'
const Form = FormProvider
import {ComponentProps, createContext, ReactNode, useContext, useId} from 'react'
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
type FormProps<T extends FieldValues> = {
form: UseFormReturn<T>
onSubmit: SubmitHandler<T>
} & Omit<ComponentProps<'form'>, 'onSubmit'>
function Form<T extends FieldValues>(rawProps: FormProps<T>) {
const {children, onSubmit, ...props} = rawProps
const form = props.form
return (
<FormProvider {...form}>
<form {...props} onSubmit={form.handleSubmit(onSubmit)}>
{children}
</form>
</FormProvider>
)
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
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'>
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
type FormFieldContext = {
id: string
error?: FieldError
}
const FormFieldContext = createContext<FormFieldContext | null>(null)
function FormField<
V extends FieldValues = FieldValues,
N extends FieldPath<V> = FieldPath<V>,
>(props: FormFieldProps<V, N>) {
const form = useFormContext<V>()
const id = useId()
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
<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"
data-error={!!fieldState.error}
className={merge('data-[error=true]:text-destructive')}
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"
className={merge('text-destructive text-sm')}>
{fieldState.error?.message}
</p>
)}
</FormFieldContext>
</div>
)}
/>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
const context = useContext(FormFieldContext)
if (!context) {
throw new Error('FormField components must be used within a FormField component')
}
return context
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={merge("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
function FormLabel({className, ...props}: ComponentProps<typeof LabelPrimitive.Root>) {
const {id, error} = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={merge("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
className={merge('data-[error=true]:text-destructive', className)}
htmlFor={id}
{...props}
/>
)
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
function FormDescription({className, ...props}: ComponentProps<'p'>) {
const {id} = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={merge("text-muted-foreground text-sm", className)}
id={`${id}-description`}
className={merge('text-muted-foreground text-sm', className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
function FormMessage({className, ...props}: ComponentProps<'p'>) {
const {id, error} = useFormField()
const body = error ? String(error?.message ?? '') : props.children
if (!body) {
return null
@@ -146,8 +148,8 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="form-message"
id={formMessageId}
className={merge("text-destructive text-sm", className)}
id={`${id}-message`}
className={merge('text-destructive text-sm', className)}
{...props}
>
{body}
@@ -156,12 +158,9 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
}
export {
useFormField,
Form,
FormItem,
FormField,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}