Skip to content

tswaters/react-form-validation

Repository files navigation

React Form Validation

npm version build status coverage license (MIT)

The goal of this library is to implement the Constraint Validation API in React while not getting in your way to do it.

Of the existing react form libraries, the use of the constraint validation api is woefully inadequate. Using this API properly is important for accessibility - you need to let the user agent know what is going on.

usage

You can import the Form and Input/Select/TextArea exports from this library.

These are wrappers around <form/> and <input/select/textarea> elements. Any additional props you provide to these elements will be passed down to the underlying form/input/select/textarea element. If you need to, you can also access the underlying element by passing a ref.

Input elements must be children of a Form element. Under the covers, this library uses context to keep track of all fields on the form and will validate all of them if the form is submitted.

A Validator component is also provided which attempts to make using the api a bit easier. This is a container element that uses a render prop which is called with ({ error, valid, invalid, validated }). This routine recursively traverses any provided props to replace input/select/textarea elements with the exports from this library, so it will duplicate any work already done up to that point.

api

Form

import { Form } from '@tswaters/react-form-validation'
const example = () => {
  return <Form />
}
  • ref - you can pass a ref to get a reference to the underlying <form> element

  • any additional props will be passed down to the underlying <form> element

  • NOTE: onSubmit will only be called if the form passes validation!

Form element components

Input/Select/TextArea take all the same props.

import { Input, Select, TextArea } from '@tswaters/react-form-validation'
const example = () => (
  <>
    <Input // Select | TextArea
      validation={oneOfType([arrayOf(func), func])}
      other={oneOfType([arrayOf(string), string])}
      recheck={bool}
      blur={bool}
      change={bool}
      onError={func}
      onInvalid={func}
      onValid={func}
      onValidated={func}
    />
  </>
)
  • validation (field, others) => Promise<void|string|Error>

    An function, or array of functions. Will be called for validation with two parameters (field - reference to the input/select/textarea; others - an array of all form inputs). You can return:

    • an error
    • a string (this will be interpreted as an error)
    • null/undefined (returning nothing signifies validation passes)
    • throwing an error will be interpreted as failing validation
  • other string|string[] - provide the name or id of another element on the form. When validating this element, the other(s) will also have their validation routines called, assuming they have not yet been touched.

  • blur bool - validate this field on input blur

  • change bool - validate this field on input change

  • recheck bool - if recheck is passed as TRUE, once a form field is validated it will be revalidated on any change.

  • onError (Error|null) => void - will be called if there is a validation error. This will always be an error object (so check message / code), or null if there is no error.

  • onInvalid (bool) => void will be called after validation with a bool indicating the form field is invalid

  • onValid (bool) => void will be called after validation with a bool indicating the form field is valid

  • onValidated (bool) => void will be called after an input is validated.

Any additional props will be passed down to the underlying input/select/textarea element

Validator

import { Validator } from '@tswaters/react-form-validation'
const example = () => (
  <Validator recheck blur>
    {({ error, valid, invalid }) => (
      <>
        <label htmlFor="my-value">My Value</label>
        <input id="my-value" name="my-value" type="text" required />
        {valid && <div>valid</div>}
        {invalid && <div>invalid</div>}
        {error && <div>{error.message}</div>}
      </>
    )}
  </Validator>
)

The validator will find & replace all input/select/textarea elements with Input/Select/TextArea.

The render prop, ({ error, valid, invalid, validated }) will be updated with any feedback from the constraint validation API.

Any props provided to validator will be passed through to the underlying Input/Select/TextArea elements.

examples

implementing bootstrap's FormGroup

const FormGroup = ({ id, label, ...rest }) => {
  return (
    <Validator recheck blur>
      {({ error, validated }) => (
        <div className={`form-group ${validated ? 'was-validated' : ''}`}>
          <label className="control-label" htmlFor={id}>
            {label}
          </label>
          <input id={id} className="form-control" {...rest} />
          {error && <div className="invalid-feedback">{error.message}</div>}
        </div>
      )}
    </Validator>
  )
}

const LoginForm = () => {
  return (
    <Form onSubmit={(e) => e.preventDefault()}>
      <FormGroup id="user-name" name="user-name" label="User Name" required />
      <FormGroup
        id="password"
        name="password"
        label="Password"
        type="password"
        required
      />
      <button type="submit">Submit</button>
    </Form>
  )
}

custom validation functions

You can provide validations to the <Input/Select/TextArea> element and they will be called as part of validating the element.

Validation routines must be synchronous. Validation will be considered failed with the following returns:

  • a string - the error returned will be new Error(returnValue)

  • an error - the error returned will be returnValue

Otherwise, the validation is considered to have succeeded.

import { Form, Validator } from '@tswaters/react-form-validation'

const MyForm = () => {
  const [error, setError] = useState(null)
  const [response, setResponse] = useState(null)

  const validation = useCallback((inputRef) => {
    if (restrictedWords.includes(inputRef.value))
      return new Error('cant use restricted words')
  }, [])

  const handleSubmit = useCallback((e) => {
    e.preventDefault()
    console.log('completely valid, no restricted words here!')
  }, [])

  return (
    <Form onSubmit={handleSubmit} noValidate>
      <Validator change validation={validation}>
        {({ error }) => {
          return (
            <>
              <input name="user-name" required />
              {error && <div>{error.message}</div>}
            </>
          )
        }}
      </Validator>
      <button type="submit">Submit</button>
    </Form>
  )
}

a note on errors

Any errors for non-custom validation routines will be returned by the browser, so based upon the user's OS/browser language settings, you'll get different translated error messages, for better or worse.

message will be populated with the browser-defined error, and a code is also set that identifies the type of error that occured. As an example, an error raised from a required input not being provided might look something like this:

{
  "message": "Please provide a value",
  "code": "valueMissing"
}

You can override the translations by inspecting the code and providing the correct translation:

const ErrorDisplay = error => {
  const [t] = useTranslation() // or however else you get a translation function
  return error ? t(`validation-error-${error.code}` : null
}

For custom error messages, error.message will be whatever you returned or threw back and the code will be customError

Thoughts on Performance

  • make sure the tree passed via <Validator /> is pretty simple. Validator recursively traverses the tree and replaces html inputs with exports from this library.

  • validation functions should be memoized lest any change invoke a re-render (wrap functions with useCallback, or an array of functions with memo)

limitations / bugs

  • only the first error for a given input element will be returned

  • there's gunna be some weirdness with code returning incorrectly if multiple constraint errors are raised.