Skip to content

Field-Level Validation

Example demonstrating field-level validation using the useFormField hook.

Demo

jsx
import { useForm, useFormField } from 'yet-another-form/react'

function TextField({ name, label, type = 'text', validate }) {
  const field = useFormField(name, { validate })

  return (
    <div className="field">
      <label htmlFor={name}>{label}</label>
      <input
        id={name}
        type={type}
        value={field.value || ''}
        onChange={field.setValue}
        onBlur={field.setTouched}
        className={field.touched && field.error ? 'error' : ''}
      />
      {field.touched && field.error && (
        <span className="error-message">{field.error}</span>
      )}
      {field.isValidating && <span className="validating">Validating...</span>}
    </div>
  )
}

function SignupForm() {
  const { Form } = useForm({
    onSubmit: (values) => {
      alert(`Welcome, ${values.username}!`)
    },
  })

  return (
    <Form>
      <TextField
        name="username"
        label="Username"
        validate={(value) => {
          if (!value) return 'Username is required'
          if (value.length < 3) return 'Must be at least 3 characters'
          if (!/^[a-zA-Z0-9_]+$/.test(value)) {
            return 'Only letters, numbers, and underscores allowed'
          }
        }}
      />

      <TextField
        name="email"
        label="Email"
        type="email"
        validate={(value) => {
          if (!value) return 'Email is required'
          if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
            return 'Invalid email address'
          }
        }}
      />

      <TextField
        name="age"
        label="Age"
        type="number"
        validate={(value) => {
          if (!value) return 'Age is required'
          const age = parseInt(value, 10)
          if (age < 13) return 'Must be at least 13 years old'
          if (age > 120) return 'Please enter a valid age'
        }}
      />

      <button type="submit">Sign Up</button>
    </Form>
  )
}

Reusable Field Component

Create a reusable field component with built-in validation display:

jsx
function FormField({ name, label, type = 'text', validate, ...props }) {
  const field = useFormField(name, { validate })

  return (
    <div className="form-field">
      <label htmlFor={name}>
        {label}
        {validate && <span className="required">*</span>}
      </label>

      <input
        id={name}
        type={type}
        value={field.value || ''}
        onChange={field.setValue}
        onBlur={field.setTouched}
        aria-invalid={field.touched && !!field.error}
        aria-describedby={field.error ? `${name}-error` : undefined}
        {...props}
      />

      {field.isValidating && (
        <span className="field-status validating">Checking...</span>
      )}

      {field.touched && field.error && (
        <span id={`${name}-error`} className="field-status error">
          {field.error}
        </span>
      )}

      {field.isDirty && !field.error && !field.isValidating && (
        <span className="field-status success">✓</span>
      )}
    </div>
  )
}

Cross-Field Validation

Validate a field based on other field values:

jsx
function PasswordFields() {
  const { Form, values } = useForm()

  return (
    <Form>
      <TextField
        name="password"
        label="Password"
        type="password"
        validate={(value) => {
          if (!value) return 'Password is required'
          if (value.length < 8) return 'Must be at least 8 characters'
          if (!/[A-Z]/.test(value)) return 'Must contain uppercase letter'
          if (!/[a-z]/.test(value)) return 'Must contain lowercase letter'
          if (!/[0-9]/.test(value)) return 'Must contain a number'
        }}
      />

      <TextField
        name="confirmPassword"
        label="Confirm Password"
        type="password"
        validate={(value, formValues) => {
          if (!value) return 'Please confirm your password'
          if (value !== formValues.password) {
            return 'Passwords do not match'
          }
        }}
      />
    </Form>
  )
}

Async Field Validation

Validate field asynchronously (e.g., check username availability):

jsx
const checkUsername = async (username) => {
  const response = await fetch(`/api/check-username/${username}`)
  const { available } = await response.json()
  return available
}

function UsernameField() {
  const field = useFormField('username', {
    validate: async (value) => {
      if (!value) return 'Username is required'
      if (value.length < 3) return 'Too short'

      const isAvailable = await checkUsername(value)
      if (!isAvailable) {
        return 'Username is already taken'
      }
    },
  })

  return (
    <div>
      <label>Username</label>
      <input
        value={field.value || ''}
        onChange={field.setValue}
        onBlur={field.setTouched}
      />
      {field.isValidating && <span>Checking...</span>}
      {field.touched && field.error && <span>{field.error}</span>}
    </div>
  )
}

Custom Validation Messages

Create a validator factory for common validations:

jsx
const validators = {
  required:
    (message = 'This field is required') =>
    (value) => {
      return value ? undefined : message
    },

  minLength: (length, message) => (value) => {
    if (!value) return undefined
    return value.length >= length
      ? undefined
      : message || `Must be at least ${length} characters`
  },

  maxLength: (length, message) => (value) => {
    if (!value) return undefined
    return value.length <= length
      ? undefined
      : message || `Must be no more than ${length} characters`
  },

  pattern: (regex, message) => (value) => {
    if (!value) return undefined
    return regex.test(value) ? undefined : message
  },

  email:
    (message = 'Invalid email address') =>
    (value) => {
      if (!value) return undefined
      return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? undefined : message
    },
}

// Combine multiple validators
const composeValidators =
  (...validators) =>
  (value, formValues) => {
    for (const validator of validators) {
      const error = validator(value, formValues)
      if (error) return error
    }
  }

// Usage
function EmailField() {
  const field = useFormField('email', {
    validate: composeValidators(
      validators.required('Email is required'),
      validators.email('Please enter a valid email'),
      validators.maxLength(100)
    ),
  })

  return (
    <div>
      <input
        type="email"
        value={field.value || ''}
        onChange={field.setValue}
        onBlur={field.setTouched}
      />
      {field.touched && field.error && (
        <span className="error">{field.error}</span>
      )}
    </div>
  )
}

Field Array Validation

Validate items in an array:

jsx
function TodoList() {
  const { Form, values } = useForm({
    initialValues: {
      todos: ['', '', ''],
    },
  })

  return (
    <Form>
      {values.todos.map((_, index) => (
        <TextField
          key={index}
          name={`todos[${index}]`}
          label={`Todo ${index + 1}`}
          validate={(value) => {
            if (!value) return 'Todo cannot be empty'
            if (value.length > 100) return 'Too long (max 100 chars)'
          }}
        />
      ))}
      <button type="submit">Save Todos</button>
    </Form>
  )
}

Conditional Validation

Only validate when certain conditions are met:

jsx
function ShippingForm() {
  const { Form, values } = useForm()

  return (
    <Form>
      <label>
        <input
          type="checkbox"
          name="differentShippingAddress"
          onChange={setValue}
          checked={values.differentShippingAddress || false}
        />
        Use different shipping address
      </label>

      {values.differentShippingAddress && (
        <TextField
          name="shippingAddress"
          label="Shipping Address"
          validate={(value) => {
            // Only validates when checkbox is checked
            if (!value) return 'Shipping address is required'
          }}
        />
      )}
    </Form>
  )
}

Released under the MIT License.