Skip to content

[form] field-level validate skips first change after error added to errors prop #4095

@DavidCodina

Description

@DavidCodina

Bug Report

I'm not sure if this is ultimately a bug with Form or Field.

When Form's errors={errors} contains an error associated to a particular field, the first change to that field skips the Field.Root's validate handler. Presumably, there's some kind of internal logic that tells the Field.Root to disregard the errors prop after the first subsequent change. That part makes sense. However, Field.Root shouldn't opt out of calling the validate handler on that first subsequent change.

Expected Behavior

Given an associated error in Form's errors prop, the first subsequent change to the field should still run field-level validation logic.

Reproducible Example

In this example if one were to type in abcde then submit, validation would fail within onFormSubmit. However, if one then deleted the e, validation would not fail in the field-level validate (but it should) because field-level validation doesn't run. In practice, the Field.Root and Input are given data-valid. If the d was then deleted, validation would fail because now field-level validation is running again.

import * as React from 'react'
import { Form } from '@base-ui/react/form'
import { Field } from '@base-ui/react/field'
import { Input } from '@base-ui/react/input'

const FIELD_FOCUS_MIXIN = `
focus-visible:shadow-none
focus-visible:border-blue-500
focus-visible:ring-[3px]
focus-visible:ring-blue-500/40
`

const FIELD_VALID_MIXIN = `
data-valid:not-data-disabled:border-green-500
data-valid:focus-visible:border-green-500
data-valid:focus-visible:ring-green-500/40
`

const FIELD_INVALID_MIXIN = `
data-invalid:not-data-disabled:border-red-500
data-invalid:focus-visible:border-red-500
data-invalid:focus-visible:ring-red-500/40
`

const inputClasses = `
flex bg-card
w-full min-w-0
px-[0.5em] py-[0.25em]
rounded-[0.375em]
border border-neutral-400 outline-none
placeholder:text-muted-foreground
${FIELD_FOCUS_MIXIN}
${FIELD_VALID_MIXIN}
${FIELD_INVALID_MIXIN}
`

/* ======================

====================== */

export const ValidationBugDemo = () => {
  const [resetKey, setResetKey] = React.useState(0)
  const [submitting, setSubmitting] = React.useState(false)
  const [errors, setErrors] = React.useState<Record<string, string>>({})

  return (
    <Form
      noValidate
      key={resetKey}
      className='mx-auto max-w-[600px] space-y-2 rounded-lg border border-neutral-400 bg-white p-2 shadow'
      onFormSubmit={(formValues, _eventDetails) => {
        setSubmitting(true)
        setErrors({})
        const formErrors: Record<string, string> = {}

        const lastName = formValues.lastName

        if (typeof lastName !== 'string') {
          formErrors.lastName = 'Invalid type.'
        } else if (lastName === 'abcde') {
          formErrors.lastName = 'abcde is not a real last name!'
        }

        const isErrors = Object.keys(formErrors).length > 0

        if (isErrors) {
          setErrors(formErrors)
          setSubmitting(false)
          return
        }

        setResetKey((v) => v + 1)
        setSubmitting(false)
      }}
      errors={errors}
    >
      <Field.Root
        name='lastName'
        validate={(value, _formValues) => {
          console.log('validate callback ran...')
          let error = ''

          if (typeof value !== 'string') {
            error = 'Invalid type'
            return error
          }

          if (!value || value.length === 0) {
            error = 'Required'
            return error
          }

          if (value.toLocaleLowerCase() === 'abcd') {
            return 'abcd is not a real last name!'
          }

          if (value.toLocaleLowerCase() === 'abc') {
            return 'abc is not a real last name!'
          }

          return null
        }}
      >
        <Input
          autoCapitalize='none'
          autoCorrect='off'
          spellCheck={false}
          data-slot='input'
          className={inputClasses}
        />
        <Field.Error className='text-sm text-red-500' />
      </Field.Root>

      <button
        className='w-full cursor-pointer rounded-[0.375em] border border-blue-700 bg-blue-500 px-2 py-1 font-medium text-white uppercase'
        disabled={submitting}
        type='submit'
      >
        {submitting ? 'Submitting...' : 'Submit'}
      </button>
    </Form>
  )
}

Base UI Version

v1.2.0

Browser

Chrome

OS

Mac

Metadata

Metadata

Assignees

No one assigned

    Labels

    type: bugIt doesn't behave as expected.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions