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, 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 { createFormHookContexts, createFormHook } from '@tanstack/react-form'
import { type multiSelectComboboxProps, type comboboxProps } from './combobox'
import { type selectProps } from './select'
import {
InputWrapper as Input,
TextareaWrapper as Textarea,
RadioWrapper as Radio,
CheckboxWrapper as Checkbox,
SwitchWrapper as Switch,
SelectWrapper as Select,
DatePickerWrapper as DatePicker,
ComboboxWrapper as Combobox,
MultiSelectComboboxWrapper as MultiSelectCombobox,
} 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<selectProps, 'value' | 'onValueChange'> & {
label?: React.ReactNode
}
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>()
return (
<DatePicker
{...props}
name={field.name}
value={field.state.value}
onSelect={(date) => field.handleChange(date as 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<comboboxProps, 'value' | 'onValueChange' | 'name'> & {
label?: React.ReactNode
}
function ComboboxField(props: comboboxFieldProps) {
const field = useFieldContext<allowedPrimitiveT>()
return (
<Combobox
{...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 multiSelectComboboxFieldProps = Omit<multiSelectComboboxProps, 'value' | 'onValueChange' | 'name'> & {
label?: React.ReactNode
}
function MultiSelectComboboxField(props: multiSelectComboboxFieldProps) {
const field = useFieldContext<allowedPrimitiveT[]>()
return (
<MultiSelectCombobox
{...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}
/>
)
}
export const { useAppForm, withForm, withFieldGroup } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
InputField,
TextareaField,
RadioField,
CheckboxField,
SwitchField,
SelectField,
DatePickerField,
ComboboxField,
MultiSelectComboboxField,
},
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>MultiSelectComboboxField
type multiSelectComboboxFieldProps = Omit<multiSelectComboboxProps, 'value' | 'onValueChange' | 'name'> & {
label?: React.ReactNode
}<form.AppField name="country">
{(field) => (
<field.MultiSelectComboboxField
label="Country"
options={['USA', 'UK', 'Canada', 'Australia', 'India']}
placeholder="Select your country"
/>
)}
</form.AppField>