Glrk UI

Field with TF

Reusable Field field wrappers built on top of shadcn/ui with tanstack-form

Role
Hobbies

Installation

npx shadcn@latest add @glrk-ui/field-wrapper-tf

If 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.

ui/field-wrapper-tf.tsx
'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>