Field with TF
Reusable Field field wrappers built on top of shadcn/ui with tanstack-form
Installation
npx shadcn@latest add @glrk-ui/field-wrapper-tfIf you haven't set up the prerequisites yet, check out Prerequest section.
Add following shadcn components: select, 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 { createFormHookContexts, createFormHook } from '@tanstack/react-form'
import {
OTPWrapper as OTP,
InputWrapper as Input,
RadioWrapper as Radio,
SwitchWrapper as Switch,
SelectWrapper as Select,
SliderWrapper as Slider,
NumberWrapper as NumberInput,
TextareaWrapper as Textarea,
CheckboxWrapper as Checkbox,
ComboboxWrapper as Combobox,
DatePickerWrapper as DatePicker,
InputGroupWrapper as InputGroup,
AutocompleteWrapper as Autocomplete,
} from './field-wrapper'
export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
type inputFieldProps = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'value' | 'onChange' | 'onBlur'
> & {
label?: React.ReactNode
}
function InputField(props: inputFieldProps) {
const field = useFieldContext<string>()
return (
<Input
{...props}
name={field.name}
value={field.state.value ?? ''}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type textareaFieldProps = Omit<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
'value' | 'onChange' | 'onBlur'
> & {
label?: React.ReactNode
}
function TextareaField(props: textareaFieldProps) {
const field = useFieldContext<string>()
return (
<Textarea
{...props}
name={field.name}
value={field.state.value ?? ''}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type radioFieldProps = {
label?: React.ReactNode
options: (allowedPrimitiveT | optionT)[]
className?: string
}
function RadioField(props: radioFieldProps) {
const field = useFieldContext<allowedPrimitiveT>()
return (
<Radio
{...props}
name={field.name}
value={field.state.value}
onValueChange={value => field.handleChange(value)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
function CheckboxField(props: radioFieldProps) {
const field = useFieldContext<allowedPrimitiveT[]>()
return (
<Checkbox
{...props}
name={field.name}
value={field.state.value ?? []}
onValueChange={value => field.handleChange(value)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type switchFieldProps = {
label?: React.ReactNode
className?: string
}
function SwitchField(props: switchFieldProps) {
const field = useFieldContext<boolean>()
return (
<Switch
{...props}
name={field.name}
checked={field.state.value ?? false}
onCheckedChange={checked => field.handleChange(checked)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type selectFieldProps = Omit<React.ComponentProps<typeof Select>, 'name' | 'value' | 'onValueChange' | 'error' | 'invalid'>
function SelectField(props: selectFieldProps) {
const field = useFieldContext<allowedPrimitiveT>()
return (
<Select
{...props}
name={field.name}
value={field.state.value}
onValueChange={value => field.handleChange(value)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type datePickerFieldProps = Omit<
React.ComponentProps<typeof DatePicker>,
'name' | 'value' | 'onSelect' | 'error' | 'invalid'
> & {
label?: React.ReactNode
}
function DatePickerField(props: datePickerFieldProps) {
const field = useFieldContext<Date | undefined>()
return (
<DatePicker
{...props}
name={field.name}
value={field.state.value}
onSelect={date => field.handleChange(date)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type comboboxFieldProps = Omit<React.ComponentProps<typeof Combobox>, 'name' | 'value' | 'onValueChange' | 'error' | 'invalid'>
function ComboboxField(props: comboboxFieldProps) {
const field = useFieldContext<allowedPrimitiveT | allowedPrimitiveT[]>()
return (
<Combobox
{...props}
name={field.name}
value={field.state.value}
onValueChange={value => field.handleChange(value as allowedPrimitiveT | allowedPrimitiveT[])}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type numberFieldProps = Omit<
React.ComponentProps<typeof NumberInput>,
'name' | 'value' | 'onValueChange' | 'error' | 'invalid'
> & { label?: React.ReactNode }
function NumberField(props: numberFieldProps) {
const field = useFieldContext<number | null>()
return (
<NumberInput
{...props}
name={field.name}
value={field.state.value}
onValueChange={val => field.handleChange(val)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type sliderFieldProps = Omit<
React.ComponentProps<typeof Slider>,
'name' | 'value' | 'onValueChange' | 'error' | 'invalid'
> & { label?: React.ReactNode }
function SliderField(props: sliderFieldProps) {
const field = useFieldContext<number | number[]>()
return (
<Slider
{...props}
name={field.name}
value={field.state.value}
onValueChange={val => field.handleChange(val as number | number[])}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type autocompleteFieldProps = Omit<
React.ComponentProps<typeof Autocomplete>,
'name' | 'value' | 'onValueChange' | 'error' | 'invalid'
> & { label?: React.ReactNode }
function AutocompleteField(props: autocompleteFieldProps) {
const field = useFieldContext<string>()
return (
<Autocomplete
{...props}
name={field.name}
value={field.state.value ?? ''}
onValueChange={val => field.handleChange(val)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type otpFieldProps = Omit<
React.ComponentProps<typeof OTP>,
'name' | 'value' | 'onValueChange' | 'error' | 'invalid'
> & { label?: React.ReactNode }
function OTPField(props: otpFieldProps) {
const field = useFieldContext<string>()
return (
<OTP
{...props}
name={field.name}
value={field.state.value ?? ''}
onValueChange={(value) => field.handleChange(value)}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
type inputGroupFieldProps = Omit<
React.ComponentProps<typeof InputGroup>,
'name' | 'value' | 'onChange' | 'onBlur' | 'error' | 'invalid'
> & { label?: React.ReactNode }
function InputGroupField(props: inputGroupFieldProps) {
const field = useFieldContext<string>()
return (
<InputGroup
{...props}
name={field.name}
value={field.state.value ?? ''}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
error={
field.state.meta.errors.length > 0 ? { message: field.state.meta.errors[0] } : undefined
}
invalid={field.state.meta.errors.length > 0}
/>
)
}
export const { useAppForm, withForm, withFieldGroup } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
InputField,
TextareaField,
RadioField,
CheckboxField,
SwitchField,
SelectField,
DatePickerField,
ComboboxField,
NumberField,
SliderField,
AutocompleteField,
OTPField,
InputGroupField,
},
formComponents: {},
})
Usage
Basic
You can use field-wrappers directly without relying on this component.
import { useForm } from '@tanstack/react-form'
import { InputWrapper } from '@/components/ui/field-wrapper'
export function Basic() {
type FormData = {/* ... */}
const defaultValues: FormData = {/* ... */}
const form = useForm({
defaultValues,
onSubmit: async ({ value }) => {
console.log('Form submitted:', value)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
// for example used InputWrapper from field-wrappers
<form.Field
name="name"
children={({ state, handleChange }) => (
<InputWrapper
name="name"
value={state.value}
onChange={(e) => handleChange(e.target.value)}
{...otherProps}
/>
)}
/>
</form>
)
}Integration with field-wrapper-tf, these components will handle value, onChange and error states, etc.
import { useAppForm } from '@/components/ui/field-wrapper-tf'
export function Basic() {
type FormData = {/* ... */}
const defaultValues: FormData = {/* ... */}
const form = useAppForm({
defaultValues,
onSubmit: async ({ value }) => {
console.log('Form submitted:', value)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
// Wrapper component
<form.AppField
name="name"
>
{(field) => (
<field.InputField // you will get suggestions for other components
label="First Name"
{...otherProps}
/>
)}
</form.AppField>
</form>
)
}Reference
InputField
type inputFieldProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'onBlur'> & {
label?: React.ReactNode
}<form.AppField name="firstName">
{(field) => (
<field.InputField
label="First Name"
placeholder="Enter your first name"
/>
)}
</form.AppField>TextareaField
type textareaFieldProps = Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'onChange' | 'onBlur'> & {
label?: React.ReactNode
}<form.AppField name="bio">
{(field) => (
<field.TextareaField
label="Bio"
placeholder="Tell us about yourself"
rows={4}
/>
)}
</form.AppField>RadioField
type radioFieldProps = {
label?: React.ReactNode
options: (allowedPrimitiveT | optionT)[]
className?: string
}<form.AppField name="role">
{(field) => (
<field.RadioField
label="Role"
options={['Developer', 'Designer', 'Manager', 'Other']}
/>
)}
</form.AppField>CheckboxField
Checkbox also shares same props as radioFieldProps
<form.AppField name="hobbies">
{(field) => (
<field.CheckboxField
label="Hobbies"
options={['Reading', 'Gaming', 'Sports', 'Music', 'Travel']}
/>
)}
</form.AppField>SwitchField
type switchFieldProps = {
label?: React.ReactNode
className?: string
}<form.AppField name="newsletter">
{(field) => (
<field.SwitchField label="Subscribe to newsletter" />
)}
</form.AppField>SelectField
type selectFieldProps = Omit<selectProps, 'value' | 'onValueChange'> & {
label?: React.ReactNode
}<form.AppField name="country">
{(field) => (
<field.SelectField
label="Country"
options={['USA', 'UK', 'Canada', 'Australia', 'India']}
placeholder="Select your country"
/>
)}
</form.AppField>DatePickerField
type datePickerFieldProps = Omit<React.ComponentProps<typeof DatePicker>, 'name' | 'value' | 'onSelect' | 'error' | 'invalid'> & {
label?: React.ReactNode
}<form.AppField name="birthDate">
{(field) => (
<field.DatePickerField
label="Birth Date"
/>
)}
</form.AppField>ComboboxField
type comboboxFieldProps = Omit<comboboxProps, 'value' | 'onValueChange' | 'name'> & {
label?: React.ReactNode
}<form.AppField name="country">
{(field) => (
<field.ComboboxField
label="Country"
options={['USA', 'UK', 'Canada', 'Australia', 'India']}
placeholder="Select your country"
/>
)}
</form.AppField>SliderField
type sliderFieldProps = Omit<
React.ComponentProps<typeof Slider>,
'name' | 'value' | 'onValueChange' | 'error' | 'invalid'
> & { label?: React.ReactNode }<form.AppField name="volume">
{field => <field.SliderField label="Volume" min={0} max={100} />}
</form.AppField>
// With validation
<form.AppField
name="budget"
validators={{
onChange: ({ value }) =>
(value as number[])[0] === 0 ? 'Must be greater than 0' : undefined,
}}
>
{field => <field.SliderField label="Budget" min={0} max={1000} step={10} />}
</form.AppField>Note: field value type is number | number[]. Use number[] (e.g. [50]) for single thumb, [20, 80] for range.
OTPField
type otpFieldProps = Omit<OTPWrapper, 'name' | 'value' | 'onValueChange' | 'error' | 'invalid'> & {
label?: React.ReactNode
}<form.AppField
name="otp"
validators={{
onChange: ({ value }) =>
value.length > 0 && value.length < 6 ? 'Enter all 6 digits' : undefined,
}}
>
{field => <field.OTPField label="Verification Code" length={6} separator />}
</form.AppField>InputGroupField
type inputGroupFieldProps = Omit<InputGroupWrapper, 'name' | 'value' | 'onChange' | 'onBlur' | 'error' | 'invalid'> & {
label?: React.ReactNode
}<form.AppField name="username">
{field => (
<field.InputGroupField
label="Username"
placeholder="username"
addonStart={<InputGroupText>@</InputGroupText>}
/>
)}
</form.AppField>