Skip to content

dsheiko/react-html5-form

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

86 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

react-html5-form

NPM Version NPM Downloads

React does not include form validation. Most teams end up either writing it from scratch or pulling in a large library. But browsers already ship a validation API: the HTML5 Constraint Validation API. It understands required, type="email", pattern, min, max, minLength, maxLength — everything you would otherwise reimplement.

This package pairs React with that API. You write your inputs the same way you always have. Form and InputGroup wrap them, read the browser's validity state, and expose it to your render props so you can show errors, toggle classes, and control submission.

React meets HTML Form Validation

Highlights

  • Works with any input: plain HTML elements or third-party React components
  • Validation constraints are standard HTML attributes (required, pattern, type="email", etc.) — nothing new to learn
  • Custom validators slot in via the same API (setCustomValidity)
  • Custom error messages per input and per constraint type (translate prop)
  • Form and InputGroup both expose validity state, error messages, and a ref — directly in the render prop
  • Optional Redux integration: exposes a reducer so form state can live in the store

How it works

Form renders a <form noValidate> wrapper and tracks the overall validity state across all registered input groups.

InputGroup wraps one or more related inputs (for example, three selects for day, month, year). It queries the DOM for inputs by name, reads their ValidityState, and exposes valid, error, errors, and inputGroup in its render prop.

You register inputs with the validate prop. You can pass an array of names (for standard HTML5 validation) or an object of name: validator pairs (when you need custom logic on top).

react-html5-form in a single picture

Installation

npm i react-html5-form

Demo

Live demo

Form

import { Form } from "react-html5-form";

Props

  • <Function> onSubmit — form submit handler (optional). Receives the Form instance. Can be async.
  • <Function> onMount — called in componentDidMount with the component instance (optional)
  • <Function> onUpdate — called when validity state changes (optional). Useful for syncing to Redux.
  • Any standard <form> HTML attributes

Scope parameters

The render prop receives:

  • <String> error — error message set with setError()
  • <Boolean> valid — overall form validity (logical AND of all input groups)
  • <Boolean> pristine — true until the user first interacts with the form
  • <Boolean> submitting — true while the onSubmit handler is running
  • <Boolean> submitted — true after a successful submission
  • <React.Component> form — the component instance (access to the API below)

valid, pristine, submitting, and submitted are also set as data-* attributes on the <form> element, e.g. data-submitting="true".

API

  • checkValidityAndUpdateInputGroups() — validate all input groups and update their state
  • checkValidityAndUpdate() — validate form validity without updating input groups
  • setError( message, ms? ) — set a form-level error message. Pass ms to auto-clear after that many milliseconds.
  • submit() — programmatically submit the form
  • scrollIntoViewFirstInvalidInputGroup() — scroll the first invalid group into view (called automatically on submit)
  • getRef() — returns the Ref for the <form> DOM node (form.getRef().current)
  • debugInputGroups( index? ) — returns debug info for all input groups, or a specific one by index

Basic form

import React from "react";
import { render } from "react-dom";
import { Form } from "react-html5-form";

const MyForm = () => (
  <Form id="myform">
    {({ error, valid }) => (
      <>
        Form content
      </>
    )}
  </Form>
);

render( <MyForm />, document.getElementById( "app" ) );

Form renders a <form noValidate> element and passes any extra props straight through — so id, className, action, etc. all work as expected.

Handling submission and errors

async function onSubmit( form ) {
  try {
    const res = await fetch( "/api/submit" ).then( r => r.json() );
    if ( !res.ok ) {
      form.setError( res.error );
    }
  } catch ( e ) {
    form.setError( "Server error, please try again" );
  }
}

const MyForm = () => (
  <Form onSubmit={onSubmit} id="myform">
    {({ error, valid, pristine, submitting, form }) => (
      <>
        { error && <div className="alert alert-danger">{error}</div> }

        Form content

        <button disabled={ pristine || submitting } type="submit">Submit</button>
      </>
    )}
  </Form>
);

onSubmit is called only when all input groups are valid. While it runs, submitting is true. The submit button is disabled until the user first interacts with the form (pristine) and while the handler is in flight.

InputGroup

InputGroup defines a scope for one or more related inputs. It reads their ValidityState and makes it available in the render prop.

import { Form, InputGroup } from "react-html5-form";

Props

  • <Object|Array> validate — (required) register inputs by name. Accepts an array of name strings or an object of name: validator pairs.
  • <Object> translate — (optional) custom messages per input per constraint. Keys are ValidityState property names (see table below).
  • <String> tag — (optional) tag for the wrapper element, default "div"
  • <Function> onMount — called in componentDidMount with the component instance (optional)
  • <Function> onUpdate — called when validity state changes (optional)
  • Any standard HTML attributes

Scope parameters

  • <String> error — validation message for the first invalid input
  • <String[]> errors — validation messages for all invalid inputs in the group
  • <Boolean> valid — true when all inputs in the group are valid
  • <Boolean> pristine — true until the user first interacts with the form
  • <React.Component> inputGroup — the component instance (access to the API below)

API

  • checkValidityAndUpdate() — validate the group and update its state
  • checkValidity() — returns boolean; does not update state
  • getInputByName( name ) — get an Input instance by name
  • getValidationMessages() — get all validation messages in the group
  • getRef() — returns the Ref for the wrapper DOM node

Basic use

<InputGroup
  tag="fieldset"
  validate={[ "email" ]}
  translate={{
    email: {
      valueMissing: "Email is required",
      typeMismatch: "Enter a valid email address"
    }
  }}>
  {({ error, valid }) => (
    <div className="form-group">
      <label htmlFor="emailInput">Email address</label>
      <input
        type="email"
        required
        name="email"
        className={`form-control ${ !valid && "is-invalid" }`}
        id="emailInput"
        placeholder="Enter email" />

      { error && <div className="invalid-feedback">{error}</div> }
    </div>
  )}
</InputGroup>

The input uses standard HTML5 attributes (type="email", required). When the form is submitted with an empty field, the browser sets ValidityState.valueMissing = true and the group receives the translated message in error.

Custom validators

When the built-in HTML5 constraints are not enough, pass a validator function. It receives the Input instance and must return a boolean. Call setCustomValidity to set the error message.

<InputGroup validate={{
  "vatId": ( input ) => {
    if ( !input.current.value.startsWith( "DE" ) ) {
      input.setCustomValidity( "VAT number must start with DE" );
      return false;
    }
    return true;
  }
}}>
  {({ error, valid }) => (
    <div className="form-group">
      <label htmlFor="vatIdInput">VAT number (optional)</label>
      <input
        className={`form-control ${ !valid && "is-invalid" }`}
        id="vatIdInput"
        name="vatId"
        placeholder="DE123456789" />

      { error && <div className="invalid-feedback">{error}</div> }
    </div>
  )}
</InputGroup>

Group with multiple inputs

A single InputGroup can wrap several related inputs. All their error messages are collected into the errors array.

const validateSelect = ( input ) => {
  if ( input.current.value === "Choose..." ) {
    input.setCustomValidity( `Please select a ${input.current.title}` );
    return false;
  }
  return true;
};
<InputGroup validate={{ "day": validateSelect, "month": validateSelect }}>
  {({ errors, valid }) => (
    <div className="form-group">
      <div className="form-row">
        <div className="form-group col-md-6">
          <label htmlFor="selectDay">Day</label>
          <select name="day" id="selectDay" title="day" className={`form-control ${ !valid && "is-invalid" }`}>
            <option>Choose...</option>
            { [ ...Array( 31 ).keys() ].map( i => <option key={i}>{i + 1}</option> ) }
          </select>
        </div>
        <div className="form-group col-md-6">
          <label htmlFor="selectMonth">Month</label>
          <select name="month" id="selectMonth" title="month" className={`form-control ${ !valid && "is-invalid" }`}>
            <option>Choose...</option>
            { [ "January", "February", "March", "April", "May", "June",
                "July", "August", "September", "October", "November", "December"
              ].map( ( m, i ) => <option key={i}>{m}</option> ) }
          </select>
        </div>
      </div>

      { errors.map( ( error, i ) => <div key={i} className="alert alert-danger">{error}</div> ) }
    </div>
  )}
</InputGroup>

On-the-fly validation

By default, groups validate on submit. To validate as the user types, call checkValidityAndUpdate from an onInput handler.

const onInput = ( e, inputGroup, form ) => {
  inputGroup.checkValidityAndUpdate();
  form.checkValidityAndUpdate();
};
<InputGroup
  validate={[ "firstName" ]}
  translate={{
    firstName: {
      patternMismatch: "Must be 5 to 30 characters"
    }
  }}>
  {({ error, valid, inputGroup }) => (
    <div className="form-group">
      <label htmlFor="firstNameInput">First name</label>
      <input
        pattern="^.{5,30}$"
        required
        className={`form-control ${ !valid && "is-invalid" }`}
        id="firstNameInput"
        name="firstName"
        onInput={( e ) => onInput( e, inputGroup, form )}
        placeholder="Enter first name" />

      { error && <div className="invalid-feedback">{error}</div> }
    </div>
  )}
</InputGroup>

Constraint reference

All standard HTML5 constraints are supported. Each maps to a ValidityState property name, which is the key to use in the translate prop.

HTML attribute ValidityState key Default message
required valueMissing "Please fill out this field."
type (email, url, …) typeMismatch "Enter a valid {type}."
pattern patternMismatch title attribute, or "Value does not match the required format."
min rangeUnderflow "Value must be {min} or more."
max rangeOverflow "Value must be {max} or less."
minlength tooShort "Use at least {minLength} characters."
maxlength tooLong "Use {maxLength} characters or fewer."
step stepMismatch "Value does not match the required step ({step})."
(browser cannot parse the value) badInput "Enter a valid value."
via setCustomValidity() customError (message you passed)

All keys can be overridden per input using the translate prop:

<InputGroup
  validate={[ "age" ]}
  translate={{
    age: {
      valueMissing: "Age is required",
      rangeUnderflow: "Must be 18 or older",
      rangeOverflow: "Must be 120 or younger",
      stepMismatch: "Whole numbers only"
    }
  }}>
  {({ error, valid }) => (
    <input type="number" name="age" required min="18" max="120" step="1" />
  )}
</InputGroup>

Input

Each input registered in an InputGroup is wrapped in an Input instance. You get access to it via inputGroup.getInputByName( name ).

API

  • current — the underlying HTML element
  • setCustomValidity( message ) — put the input in an invalid state with a custom message. Pass an empty string to clear.
  • checkValidity() — returns boolean
  • getValidationMessage() — returns the current message, respecting any translate mapping

Connecting to Redux store

Import the html5form reducer and add it to your store. Then pass formActions and formState props to Form so it keeps the store in sync.

import { createStore, combineReducers } from "redux";
import { html5form } from "react-html5-form";

const store = createStore( combineReducers({ html5form }) );

Once connected, all form, input group, and input validity states are available in the Redux tree:

react-html5-form and Redux

See the full Redux example.

About

React form validation using the browser's built-in HTML5 Constraint Validation API. No extra validation library needed. 💥

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors