Field Wrapper
Reusable Field field wrappers built on top of shadcn/ui
Installation
npx shadcn@latest add @glrk-ui/field-wrapperIf 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.
"use client";
import { useState } from 'react';
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 { Popover, PopoverContent, PopoverTrigger } from "./popover";
import { Field, FieldLabel, FieldError } from "./field";
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 = {
name: string
label?: React.ReactNode
error?: { message?: string }
invalid?: boolean
className?: string
}
type InputProps = BaseProps & React.InputHTMLAttributes<HTMLInputElement>
export function InputWrapper({ name, label, error, invalid, className, type = "text", placeholder, ...props }: InputProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} data-invalid={isInvalid}>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<Input
id={name}
name={name}
type={type}
placeholder={placeholder || `Enter ${label}`}
aria-invalid={isInvalid}
{...props}
/>
{isInvalid && <FieldError errors={[error]} />}
</Field>
)
}
type TextareaProps = BaseProps & React.TextareaHTMLAttributes<HTMLTextAreaElement>
export function TextareaWrapper({ name, label, error, invalid, className, placeholder, ...rest }: TextareaProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} data-invalid={isInvalid}>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<Textarea
id={name}
name={name}
placeholder={placeholder || `Enter ${label}`}
aria-invalid={isInvalid}
{...rest}
/>
{isInvalid && <FieldError errors={[error]} />}
</Field>
)
}
type RadioProps = BaseProps & {
options: (allowedPrimitiveT | optionT)[]
value?: allowedPrimitiveT
onValueChange?: (value: allowedPrimitiveT) => void
}
export function RadioWrapper({ name, label, error, invalid, className, options, value, onValueChange }: RadioProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} data-invalid={isInvalid}>
{label && <FieldLabel htmlFor={`${name}-0`}>{label}</FieldLabel>}
<RadioGroup
value={value ? String(value) : undefined}
onValueChange={(val) => onValueChange?.(parseAllowedPrimitive(val))}
className="flex items-center flex-wrap gap-4"
aria-invalid={isInvalid}
>
{options.map((option, i) => (
<div key={getKey(option, i)} className="flex items-center gap-2">
<RadioGroupItem value={`${getValue(option)}`} id={`${name}-${i}`} />
<FieldLabel htmlFor={`${name}-${i}`} className="font-normal">
{getLabel(option)}
</FieldLabel>
</div>
))}
</RadioGroup>
{isInvalid && <FieldError errors={[error]} />}
</Field>
)
}
type CheckboxProps = BaseProps & {
options: (allowedPrimitiveT | optionT)[]
value?: allowedPrimitiveT[]
onValueChange?: (value: allowedPrimitiveT[]) => void
}
export function CheckboxWrapper({ name, label, error, invalid, className, options, value = [], onValueChange }: CheckboxProps) {
const isInvalid = invalid || !!error
const toggleValue = (v: allowedPrimitiveT) => {
if (value.includes(v)) {
onValueChange?.(value.filter(x => x !== v))
} else {
onValueChange?.([...value, v])
}
}
return (
<Field className={className} data-invalid={isInvalid}>
{label && <FieldLabel htmlFor={`${name}-0`}>{label}</FieldLabel>}
<div className="flex items-center flex-wrap gap-4" aria-invalid={isInvalid}>
{options.map((option, i) => {
const val = getValue(option)
const parsedVal = parseAllowedPrimitive(val)
const isChecked = value.includes(parsedVal)
return (
<div key={getKey(option, i)} className="flex items-center gap-2 space-y-0">
<Checkbox
id={`${name}-${i}`}
checked={isChecked}
onCheckedChange={() => toggleValue(parsedVal)}
/>
<FieldLabel htmlFor={`${name}-${i}`} className="font-normal">
{getLabel(option)}
</FieldLabel>
</div>
)
})}
</div>
{isInvalid && <FieldError errors={[error]} />}
</Field>
)
}
type SwitchProps = BaseProps & {
checked?: boolean
onCheckedChange?: (checked: boolean) => void
}
export function SwitchWrapper({ name, label, error, invalid, className, checked, onCheckedChange }: SwitchProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} data-invalid={isInvalid}>
<div className='flex items-center justify-between gap-4'>
{label && <FieldLabel htmlFor={name} className="font-normal">{label}</FieldLabel>}
<Switch
id={name}
checked={checked}
onCheckedChange={onCheckedChange}
aria-label={typeof label === "string" ? label : name}
aria-invalid={isInvalid}
/>
</div>
{isInvalid && <FieldError errors={[error]} />}
</Field>
)
}
type SelectProps = BaseProps & Omit<selectProps, "value" | "onValueChange"> & {
value?: allowedPrimitiveT
onValueChange?: (value: allowedPrimitiveT) => void
}
export function SelectWrapper({ name, label, error, invalid, className, options, placeholder, value, onValueChange, ...props }: SelectProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} data-invalid={isInvalid}>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<SelectPrimitiveWrapper
{...props}
id={name}
options={options}
value={value ? String(value) : undefined}
placeholder={placeholder ?? `Select ${label}`}
onValueChange={(val) => onValueChange?.(parseAllowedPrimitive(val))}
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={[error]} />}
</Field>
)
}
type DatePickerProps = BaseProps & Omit<React.ComponentProps<typeof Calendar>, "selected" | "onSelect"> & {
value?: Date
onSelect?: (date: Date | undefined) => void
}
export function DatePickerWrapper({ name, label, error, invalid, className, value, onSelect, ...calendarProps }: DatePickerProps) {
const [open, setOpen] = useState(false)
const isInvalid = invalid || !!error
return (
<Field className={className} data-invalid={isInvalid}>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
id={name}
variant={"outline"}
className={cn("w-full pl-3 text-left font-normal", !value && "text-muted-foreground")}
aria-invalid={isInvalid}
>
{value ? format(value, "dd/MM/yyyy") : <span>Pick a date</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
autoFocus
mode="single"
captionLayout="dropdown"
selected={value}
onSelect={(date) => {
onSelect?.(date)
setOpen(false)
}}
defaultMonth={value}
{...calendarProps as any}
/>
</PopoverContent>
</Popover>
{isInvalid && <FieldError errors={[error]} />}
</Field>
)
}
type ComboboxProps = BaseProps & comboboxProps
export function ComboboxWrapper({ name, label, error, invalid, className, placeholder, value, onValueChange, ...rest }: ComboboxProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} data-invalid={isInvalid}>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<Combobox
{...rest}
id={name}
value={value}
placeholder={placeholder || `Select ${label}`}
onValueChange={onValueChange}
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={[error]} />}
</Field>
)
}
type MultiSelectComboboxProps = BaseProps & multiSelectComboboxProps
export function MultiSelectComboboxWrapper({ name, label, error, invalid, className, placeholder, value, onValueChange, ...rest }: MultiSelectComboboxProps) {
const isInvalid = invalid || !!error
return (
<Field className={className} data-invalid={isInvalid}>
{label && <FieldLabel htmlFor={name}>{label}</FieldLabel>}
<MultiSelectCombobox
{...rest}
id={name}
value={value}
placeholder={placeholder || `Select ${label}`}
onValueChange={onValueChange}
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={[error]} />}
</Field>
)
}These wrappers automatically handle:
- Label
- Form control wiring
- Error messages
- Placeholder generation
- Value parsing (e.g., converting
"1"→ number)
Usage
Basic
export function Basic() {
const [value, setValue] = useState({
/* ... */
})
function onChange(key: string, value: allowedPrimitiveT | allowedPrimitiveT[]) {
setValue(prev => ({
...prev,
[key]: value
}))
}
function onSubit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
console.log(value)
}
return (
<form
onSubmit={onSubit}
>
// Wrapper component
<Comp
value={value.field}
onValueChange={val => onChange("field", val)}
/>
</form>
)
}With Errors
export function Controlled() {
const [value, setValue] = useState({
/* ... */
})
const [errors, setErrors] = useState({
/* ... */
})
function onChange(key: string, value: allowedPrimitiveT | allowedPrimitiveT[]) {
setValue(prev => ({
...prev,
[key]: value
}))
}
function onSubit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
console.log(value)
}
return (
<form
onSubmit={onSubit}
>
// Wrapper component
<Comp
value={value.field}
onValueChange={val => onChange("field", val)}
error={errors.field ? { message: errors.field }: undefined}
invalid={!!errors.field}
/>
</form>
)
}Reference
type BaseProps = {
name: string
label?: React.ReactNode
error?: { message?: string }
invalid?: boolean
className?: string
}InputWrapper
type InputProps = BaseProps &
React.InputHTMLAttributes<HTMLInputElement><InputWrapper
name="username"
label="Username"
value={value}
onChange={e => onChange(e.target.value)}
/>TextareaWrapper
type TextareaProps = BaseProps &
React.TextareaHTMLAttributes<HTMLTextAreaElement><TextareaWrapper
name="bio"
label="Bio"
value={value}
onChange={e => onChange(e.target.value)}
/>RadioWrapper
type RadioProps = BaseProps & {
options: (allowedPrimitiveT | optionT)[]
value?: allowedPrimitiveT
onValueChange?: (value: allowedPrimitiveT) => void
}<RadioWrapper
name="gender"
label="Gender"
options={["male", "female", "other"]}
value={value}
onValueChange={val => onChange(val)}
/>CheckboxWrapper
type CheckboxProps = BaseProps & {
options: (allowedPrimitiveT | optionT)[]
value?: allowedPrimitiveT[]
onValueChange?: (value: allowedPrimitiveT[]) => void
}<CheckboxWrapper
name="interest"
label="Interest"
options={["Book reading", "Music", "TV", "Movie"]}
value={value}
onValueChange={val => onChange(val)}
/>SwitchWrapper
type SwitchProps = BaseProps & {
checked?: boolean
onCheckedChange?: (checked: boolean) => void
}<SwitchWrapper
name="isCompleted"
label="Is completed"
checked={value}
onCheckedChange={val => onChange(val)}
/>SelectWrapper
type SelectProps = BaseProps & Omit<selectProps, "value" | "onValueChange"> & {
value?: allowedPrimitiveT
onValueChange?: (value: allowedPrimitiveT) => void
}<SelectWrapper
name="country"
label="Country"
options={[
{ value: "in", label: "India" },
{ value: "us", label: "USA" }
]}
value={value}
onValueChange={val => onChange(val)}
/>DatePickerWrapper
type DatePickerProps = BaseProps & Omit<React.ComponentProps<typeof Calendar>, "selected" | "onSelect"> & {
value?: Date
onSelect?: (date: Date | undefined) => void
}<DatePickerWrapper
name="dob"
label="Date of Birth"
value={value}
onSelect={val => onChange(val)}
/>ComboboxWrapper
type ComboboxProps = BaseProps & comboboxProps<ComboboxWrapper
name="fruit"
label="Favorite Fruit"
options={["Apple", "Banana", "Mango"]}
value={value}
onValueChange={val => onChange(val)}
/>MultiSelectComboboxWrapper
type MultiSelectComboboxProps = BaseProps & multiSelectComboboxProps<MultiSelectComboboxWrapper
name="hobbies"
label="Hobbies"
options={["Music", "Sports", "Travel"]}
value={value}
onValueChange={val => onChange(val)}
/>