Glrk UI

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-tf

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