UIPackage

Form

React form
Edit on GitHub

Zod-first form block built on React Hook Form. Wires field labels, descriptions, error messages, and validation together; bind a field once and the rest is automatic.

Also available for Vue ->

Installation

$ npx shadcn@latest add https://react.uipkge.dev/r/react/form.json

Or with the named registry: npx shadcn@latest add @uipkge-react/form

Examples

Schema

Type aliases from this item's source — use them to shape the data you pass in.

FormItemContextValue
type FormItemContextValue {
  id: string
}

Dependencies

Files (2)

  • components/ui/form/form.tsx 4.1 kB
    'use client'
    
    import * as React from 'react'
    import { Slot } from '@radix-ui/react-slot'
    import {
      Controller,
      FormProvider,
      useFormContext,
      useFormState,
      type ControllerProps,
      type FieldPath,
      type FieldValues,
    } from 'react-hook-form'
    
    import { cn } from '@/lib/utils'
    import { Label } from '@/components/ui/label'
    
    // The canonical shadcn form: react-hook-form + zod + @hookform/resolvers.
    // `Form` is just FormProvider — wire your form with useForm({ resolver:
    // zodResolver(schema) }) and spread the returned methods. `FormField` is a
    // thin Controller wrapper that publishes the field name on context so the
    // label / control / description / message parts can derive their ids and
    // error state without prop-drilling.
    const Form = FormProvider
    
    type FormFieldContextValue<
      TFieldValues extends FieldValues = FieldValues,
      TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
    > = {
      name: TName
    }
    
    const FormFieldContext = React.createContext<FormFieldContextValue>(
      {} as FormFieldContextValue,
    )
    
    function FormField<
      TFieldValues extends FieldValues = FieldValues,
      TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
    >({ ...props }: ControllerProps<TFieldValues, TName>) {
      return (
        <FormFieldContext.Provider value={{ name: props.name }}>
          <Controller {...props} />
        </FormFieldContext.Provider>
      )
    }
    
    function 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,
      }
    }
    
    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-uipkge=""
            data-slot="form-item"
            className={cn('grid gap-1.5', className)}
            {...props}
          />
        </FormItemContext.Provider>
      )
    }
    
    function FormLabel({
      className,
      ...props
    }: React.ComponentProps<typeof Label>) {
      const { error, formItemId } = useFormField()
    
      return (
        <Label
          data-uipkge=""
          data-slot="form-label"
          data-error={!!error}
          className={cn('data-[error=true]:text-destructive', className)}
          htmlFor={formItemId}
          {...props}
        />
      )
    }
    
    function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
      const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
    
      return (
        <Slot
          data-uipkge=""
          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()
    
      return (
        <p
          data-uipkge=""
          data-slot="form-description"
          id={formDescriptionId}
          className={cn('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
    
      if (!body) {
        return null
      }
    
      return (
        <p
          data-uipkge=""
          data-slot="form-message"
          id={formMessageId}
          className={cn('text-destructive text-sm', className)}
          {...props}
        >
          {body}
        </p>
      )
    }
    
    export {
      useFormField,
      Form,
      FormItem,
      FormLabel,
      FormControl,
      FormDescription,
      FormMessage,
      FormField,
    }
  • components/ui/form/index.ts 0.1 kB
    export {
      useFormField,
      Form,
      FormItem,
      FormLabel,
      FormControl,
      FormDescription,
      FormMessage,
      FormField,
    } from './form'

Raw manifest: https://react.uipkge.dev/r/react/form.json