Glrk UI

Form Wrapper

Reusable form field wrappers built on top of shadcn/ui and react-hook-form

This component is depreciated, use Field related components.

Installation

npx shadcn@latest add @glrk-ui/form-wrapper

If you haven't set up the prerequisites yet, check out Prerequest section.

Add following shadcn components: select, command, popover, calendar, textarea, input, radio-group, form, button, label, checkbox, switch.

Update same components from our site.

Copy and paste the following code into your project.

ui/form-wrapper.tsx
"use client";

import { useState } from 'react';
import { Control, FieldValues, Path } from "react-hook-form";
import { CalendarIcon } from "lucide-react";
import { format } from "date-fns";

import { cn, getKey, getLabel, getValue, parseAllowedPrimitive } from "@/lib/utils";

import { type comboboxProps, type multiSelectComboboxProps, Combobox, MultiSelectCombobox } from "./combobox";
import { type selectProps, SelectWrapper as SelectPrimitiveWrapper } from "./select";
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "./form";
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
import { RadioGroup, RadioGroupItem } from "./radio-group";
import { Calendar } from "./calendar";
import { Textarea } from "./textarea";
import { Checkbox } from './checkbox';
import { Button } from "./button";
import { Switch } from './switch';
import { Input } from "./input";

type BaseProps<T extends FieldValues> = {
  name: Path<T>
  label?: React.ReactNode
  control: Control<T>
  className?: string
}

type InputProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'value' | 'onChange' | 'onBlur'>
export function InputWrapper<T extends FieldValues>({ name, label, control, className, type = "text", placeholder, ...props }: InputProps<T>) {
  return (
    <FormField
      name={name}
      control={control}
      render={({ field }) => (
        <FormItem className={className}>
          {label && <FormLabel>{label}</FormLabel>}

          <FormControl>
            <Input type={type} placeholder={placeholder || `Enter ${label}`} {...field} {...props} />
          </FormControl>

          <FormMessage />
        </FormItem>
      )}
    />
  )
}

type TextareaProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'name' | 'value' | 'onChange' | 'onBlur'>
export function TextareaWrapper<T extends FieldValues>({ name, label, control, className, placeholder, ...rest }: TextareaProps<T>) {
  return (
    <FormField
      name={name}
      control={control}
      render={({ field }) => (
        <FormItem className={className}>
          {label && <FormLabel>{label}</FormLabel>}

          <FormControl>
            <Textarea placeholder={placeholder || `Enter ${label}`} {...rest} {...field} />
          </FormControl>

          <FormMessage />
        </FormItem>
      )}
    />
  )
}

type RadioProps<T extends FieldValues> = BaseProps<T> & {
  options: (allowedPrimitiveT | optionT)[]
}
export function RadioWrapper<T extends FieldValues>({ name, label, control, className, options }: RadioProps<T>) {
  return (
    <FormField
      name={name}
      control={control}
      render={({ field }) => (
        <FormItem className={className}>
          {label && <FormLabel>{label}</FormLabel>}

          <FormControl>
            <RadioGroup
              value={field.value}
              onValueChange={value => field.onChange(parseAllowedPrimitive(value))}
              className="flex items-center flex-wrap gap-4"
            >
              {options.map((option, i) => (
                <FormItem
                  key={getKey(option, i)}
                  className="flex items-center"
                >
                  <FormControl>
                    <RadioGroupItem value={`${getValue(option)}`} />
                  </FormControl>
                  <FormLabel className="font-normal">
                    {getLabel(option)}
                  </FormLabel>
                </FormItem>
              ))}
            </RadioGroup>
          </FormControl>

          <FormMessage />
        </FormItem>
      )}
    />
  )
}

type CheckboxProps<T extends FieldValues> = BaseProps<T> & {
  options: (allowedPrimitiveT | optionT)[]
}
export function CheckboxWrapper<T extends FieldValues>({
  name,
  label,
  control,
  className,
  options
}: CheckboxProps<T>) {
  return (
    <FormField
      name={name}
      control={control}
      render={({ field }) => {
        const valueArr: allowedPrimitiveT[] = Array.isArray(field.value)
          ? field.value
          : []

        const toggleValue = (v: allowedPrimitiveT) => {
          if (valueArr.includes(v)) {
            field.onChange(valueArr.filter(x => x !== v))
          } else {
            field.onChange([...valueArr, v])
          }
        }

        return (
          <FormItem className={className}>
            {label && <FormLabel>{label}</FormLabel>}

            <div className="flex items-center flex-wrap gap-4">
              {options.map((option, i) => {
                const val = getValue(option)
                const isChecked = valueArr.includes(parseAllowedPrimitive(val))

                return (
                  <FormItem
                    key={getKey(option, i)}
                    className="flex items-center gap-2 space-y-0"
                  >
                    <FormControl>
                      <Checkbox
                        checked={isChecked}
                        onCheckedChange={() => toggleValue(parseAllowedPrimitive(val))}
                      />
                    </FormControl>

                    <FormLabel className="font-normal">
                      {getLabel(option)}
                    </FormLabel>
                  </FormItem>
                )
              })}
            </div>

            <FormMessage />
          </FormItem>
        )
      }}
    />
  )
}

export function SwitchWrapper<T extends FieldValues>({ name, label, control, className }: BaseProps<T>) {
  return (
    <FormField
      name={name}
      control={control}
      render={({ field }) => (
        <FormItem className={className}>
          <div className='flex items-center justify-between gap-4'>
            {label && <FormLabel className="font-normal">{label}</FormLabel>}

            <FormControl>
              <Switch
                checked={field.value}
                onCheckedChange={field.onChange}
                aria-label={typeof label === "string" ? label : name}
              />
            </FormControl>
          </div>

          <FormMessage />
        </FormItem>
      )}
    />
  )
}

type SelectProps<T extends FieldValues> = BaseProps<T> & Omit<selectProps, "value" | "onValueChange">
export function SelectWrapper<T extends FieldValues>({ name, label, control, className, options, placeholder, ...props }: SelectProps<T>) {
  return (
    <FormField
      name={name}
      control={control}
      render={({ field }) => (
        <FormItem className={className}>
          {label && <FormLabel>{label}</FormLabel>}
          <FormControl>
            <SelectPrimitiveWrapper
              {...props}
              options={options}
              value={`${field.value}`}
              placeholder={placeholder ?? `Select ${label}`}
              onValueChange={value => field.onChange(parseAllowedPrimitive(value))}
            />
          </FormControl>

          <FormMessage />
        </FormItem>
      )}
    />
  )
}

type DatePickerProps<T extends FieldValues> = BaseProps<T> & Omit<React.ComponentProps<typeof Calendar>, "selected" | "onSelect">
export function DatePickerWrapper<T extends FieldValues>({ name, label, control, className, ...calendarProps }: DatePickerProps<T>) {
  const [open, setOpen] = useState(false)

  return (
    <FormField
      name={name}
      control={control}
      render={({ field }) => (
        <FormItem className={className}>
          {label && <FormLabel>{label}</FormLabel>}

          <Popover open={open} onOpenChange={setOpen}>
            <PopoverTrigger asChild>
              <FormControl>
                <Button
                  variant={"outline"}
                  className={cn("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
                >
                  {field.value ? format(field.value, "dd/MM/yyyy") : <span>Pick a date</span>}
                  <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
                </Button>
              </FormControl>
            </PopoverTrigger>

            <PopoverContent className="w-auto p-0" align="start">
              <Calendar
                autoFocus
                mode="single"
                captionLayout="dropdown"
                selected={field.value}
                onSelect={(date: any) => {
                  field.onChange(date)
                  setOpen(false)
                }}
                defaultMonth={field.value}
                {...calendarProps}
              />
            </PopoverContent>
          </Popover>

          <FormMessage />
        </FormItem>
      )}
    />
  )
}

type ComboboxProps<T extends FieldValues> = BaseProps<T> & Omit<comboboxProps, "value" | "onValueChange">
export function ComboboxWrapper<T extends FieldValues>({ name, label, control, className, placeholder, ...rest }: ComboboxProps<T>) {
  return (
    <FormField
      name={name}
      control={control}
      render={({ field }) => (
        <FormItem className={className}>
          {label && <FormLabel>{label}</FormLabel>}

          <FormControl>
            <Combobox
              {...rest}
              value={field.value}
              placeholder={placeholder || `Select ${label}`}
              onValueChange={field.onChange}
            />
          </FormControl>

          <FormMessage />
        </FormItem>
      )}
    />
  )
}

type MultiSelectComboboxProps<T extends FieldValues> = BaseProps<T> & Omit<multiSelectComboboxProps, "value" | "onValueChange">
export function MultiSelectComboboxWrapper<T extends FieldValues>({ name, label, control, className, placeholder, ...rest }: MultiSelectComboboxProps<T>) {
  return (
    <FormField
      name={name}
      control={control}
      render={({ field }) => (
        <FormItem className={className}>
          {label && <FormLabel>{label}</FormLabel>}

          <FormControl>
            <MultiSelectCombobox
              {...rest}
              value={field.value}
              placeholder={placeholder || `Select ${label}`}
              onValueChange={field.onChange}
            />
          </FormControl>

          <FormMessage />
        </FormItem>
      )}
    />
  )
}

These wrappers automatically handle:

  • Label
  • Form control wiring
  • Error messages
  • Placeholder generation
  • Value parsing (e.g., converting "1" → number)

Usage

Basic

import { FormProvider, useForm } from "react-hook-form"

export function Basic() {
  const form = useForm({
    defaultValues: {/* ... */},
  })

  return (
    <FormProvider {...form}>
      <form
        onSubmit={form.handleSubmit((d) => console.log(d))}
      >
      // Wrapper component
      </form>
    </FormProvider>
  )
}

Controlled

import { FormProvider, useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"

const formSchema = z.object({
  // your schema
})

export function Controlled() {
  const form = useForm({
    resolver: zodResolver(formSchema),
    defaultValues: {/* ... */},
  })

  return (
    <FormProvider {...form}>
      <form
        onSubmit={form.handleSubmit((d) => console.log(d))}
      >
      // Wrapper component
      </form>
    </FormProvider>
  )
}

Reference

import { Control, FieldValues, Path } from "react-hook-form";

type BaseProps<T extends FieldValues> = {
  name: Path<T>
  label?: React.ReactNode
  control: Control<T>
  className?: string
}

InputWrapper

type InputProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'value' | 'onChange' | 'onBlur'>
<InputWrapper
  name="username"
  label="Username"
  control={form.control}
/>

TextareaWrapper

type TextareaProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'name' | 'value' | 'onChange' | 'onBlur'>
<TextareaWrapper
  name="bio"
  label="Bio"
  control={form.control}
/>

RadioWrapper

type RadioProps<T extends FieldValues> = BaseProps<T> & {
  options: (allowedPrimitiveT | optionT)[]
}
<RadioWrapper
  name="gender"
  label="Gender"
  control={form.control}
  options={["male", "female", "other"]}
/>

CheckboxWrapper

type CheckboxProps<T extends FieldValues> = BaseProps<T> & {
  options: (allowedPrimitiveT | optionT)[]
}
<CheckboxWrapper
  name="interest"
  label="Interest"
  control={form.control}
  options={["Book reading", "Music", "TV", "Movie"]}
/>

SwitchWrapper

type SwitchProps<T extends FieldValues> = BaseProps<T>
<SwitchWrapper
  name="isCompleted"
  label="Is completed"
  control={form.control}
/>

Note: Value need to be boolean.

SelectWrapper

type SelectProps<T extends FieldValues> =
  BaseProps<T> &
  Omit<selectProps, "value" | "onValueChange">
<SelectWrapper
  name="country"
  label="Country"
  control={form.control}
  options={[
    { value: "in", label: "India" },
    { value: "us", label: "USA" }
  ]}
/>

DatePickerWrapper

type DatePickerProps<T extends FieldValues> =
  BaseProps<T> &
  Omit<React.ComponentProps<typeof Calendar>, "selected" | "onSelect">
<DatePickerWrapper
  name="dob"
  label="Date of Birth"
  control={form.control}
/>

ComboboxWrapper

type ComboboxProps<T extends FieldValues> =
  BaseProps<T> &
  Omit<comboboxProps, "value" | "onValueChange">
<ComboboxWrapper
  name="fruit"
  label="Favorite Fruit"
  control={form.control}
  options={["Apple", "Banana", "Mango"]}
/>

MultiSelectComboboxWrapper

type MultiSelectComboboxProps<T extends FieldValues> =
  BaseProps<T> &
  Omit<multiSelectComboboxProps, "value" | "onValueChange">
<MultiSelectComboboxWrapper
  name="hobbies"
  label="Hobbies"
  control={form.control}
  options={["Music", "Sports", "Travel"]}
/>