# react-html5-form

[![NPM Version](https://img.shields.io/npm/v/react-html5-form.svg?style=flat)](https://www.npmjs.com/package/react-html5-form)
[![NPM Downloads](https://img.shields.io/npm/dm/react-html5-form.svg?style=flat)](https://www.npmjs.com/package/react-html5-form)

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](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation). 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](https://github.com/dsheiko/react-html5-form/raw/master/docs/react-html5-form-logo.png)


## 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](https://github.com/dsheiko/react-html5-form/raw/master/docs/react-html5-form.png)


## Installation

```bash
npm i react-html5-form
```

## Demo

[Live demo](https://dsheiko.github.io/react-html5-form/)

- [Bootstrap 4 example](./demo/bootstrap/src/index.jsx) — plain HTML inputs
- [Material UI example](./demo/materialui/src/index.jsx) — third-party input components
- [Redux example](./demo/bootstrap-redux/src/index.jsx) — form state in Redux store


## Form

```js
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

```jsx
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

```jsx
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.

```js
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

```jsx
<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.

```jsx
<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.

```js
const validateSelect = ( input ) => {
  if ( input.current.value === "Choose..." ) {
    input.setCustomValidity( `Please select a ${input.current.title}` );
    return false;
  }
  return true;
};
```

```jsx
<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.

```js
const onInput = ( e, inputGroup, form ) => {
  inputGroup.checkValidityAndUpdate();
  form.checkValidityAndUpdate();
};
```

```jsx
<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:

```jsx
<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.

```jsx
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](https://github.com/dsheiko/react-html5-form/raw/master/docs/react-h5-redux.png)

See the [full Redux example](./demo/bootstrap-redux/src/index.jsx).
