Handling forms with React and HTML5 Form Validation API

JavaScript

When we talk about user input within a web app we often think first of HTML forms. Web forms have been available with the very first editions of HTML. Apparently the feature was introduced already in 1991 and standardized in 1995 as RFC 1866. We use them everywhere, with almost every library and framework. But what about React? Facebook gives a limited input on how to deal with forms . Mainly it’s about subscribing form and controls for interaction events and passing state with “value” property. So form validation and submission logic is up to you. Decent UI implies you cover such logic as “on submit”/“on input” field validation, inline error messaging, toggling elements depending on validity, “pristine”, “submitting” states and more. Cannot we abstract this logic and simply plug it in our forms? Definitely we can. The only question is what approach and solution to pick up.

Forms in a DevKit

If you go with a DevKit like ReactBootstrap or AntDesign you are likely already happy with the forms. Both provide components to build a form that meets diverse requirements. For example in AntDesign we define a form with Form element and a form field with FormItem, what is wrapper for any input control out of the set. You can set validation rules on FormItem like:

  <FormItem>
     {getFieldDecorator('select', {
       rules: [
         { required: true, message: 'Please select your country!' },
       ],
     })(
       <Select placeholder="Please select a country">
         <Option value="china">China</Option>
         <Option value="use">U.S.A</Option>
       </Select>
     )}
   </FormItem>

Then, for an instance in form submission handler you can run this.props.form.validateFields() to apply validation. It looks like everything is taken care of. Yet the solution is specific to to the framework. If you do not work with a DevKit, you cannot benefit of its features.

Building form based on schema

Alternatively we can go with a standalone component that builds form for us based on provided JSON spec. For example we can import Winterfell component and build a form as simple as that:

<Winterfell schema={loginSchema} ></Winterfell>

However the scheme can be quite complex. Besides we bind ourselves to a custom syntax. Another solution react-jsonschema-form looks similar, but relies on

JSON schema. JSON schema is a project-agnostic vocabulary designed to annotate and validate JSON documents. Yet it bind us to the only features implemented in the builder and defined in the schema.

Formsy

I would prefer a wrapper for my arbitrary HTML form that would take care of validation logic. Here one of most popular solutions - Formsy. how does it look like? We create our own component for a form field and wrap it with HOC withFormsy:

import { withFormsy } from "formsy-react";
import React from "react";

class MyInput extends React.Component {

  changeValue = ( event ) => {
    this.props.setValue( event.currentTarget.value );
  }

  render() {
    return (
      <div>
        <input
          onChange={ this.changeValue }
          type="text"
          value={ this.props.getValue() || "" }
        />
        <span>{ this.props.getErrorMessage() }</span>
      </div>
    );
  }
}

export default withFormsy( MyInput );

As you can see the component receives getErrorMessage() function in props, which we can use for inline error messaging.

So we made a field component. Let’s place it in a form:

import Formsy from "formsy-react";
import React from "react";
import MyInput from "./MyInput";

export default class App extends React.Component {

  onValid = () => {
    this.setState({ valid: true });
  }

  onInvalid = () => {
    this.setState({ valid: false });
  }

  submit( model ) {
    //...
  }

  render() {
    return (
      <Formsy onValidSubmit={this.submit} onValid={this.onValid} onInvalid={this.onInvalid}>
        <MyInput
          name="email"
          validations="isEmail"
          validationError="This is not a valid email"
          required
        ></MyInput>
        <button type="submit" disabled={ !this.state.valid }>Submit</button>
      </Formsy>
    );
  }
}

We specify all the required field validators with validations property (see list of available validators). With validationError we set the desired validation message and receive from the form validity state in onValid and onInvalid handlers.

That looks simple, clean and flexible. But I wonder why don’t we rely on HTML5 built-in form validation, rather then going with countless custom implementations?

HTML5 Form Validation

The technology emerged quite long ago. The first implementation came together with Opera 9.5 already in 2008. Nowadays it is available in all the modern browsers. Form (Data) Validation introduces extra HTML attributes and input types, that can be used to set form validation rules. The validation can be also controlled and customized from JavaScript by using dedicated API.

Let’s examine the following code:

<form>
  <label for="answer">What do you know, Jon Snow?</label>
  <input id="answer" name="answer" required>
  <button>Ask</button>
</form>

It’s a simple form, except one thing - the input element has required attribute. So if we press the submit button immediately the form won’t be sent to the server. Instead we will see a tooltip next to the input saying that the value doesn’t comply the given constraint (shall be not empty).

Handling forms with React and HTML5 Form Validation API

Now we set on the input additional constraint:

<form>
  <label for="answer">What do you know, Jon Snow?</label>
  <input id="answer" name="answer" required pattern="nothing|nix">
  <button>Ask</button>
</form>

So the value is not just required, but must comply to the regular expression given with attribute pattern.

Handling forms with React and HTML5 Form Validation API

Error message isn’t so informative, is it? We can customize it (for example to explain what exactly we expect from the user) or just translate:

<form>
  <label for="answer">What do you know, Jon Snow?</label>
  <input id="answer" name="answer" required pattern="nothing|nix">
  <button>Ask</button>
</form>
const answer = document.querySelector( "[name=answer]" );
answer.addEventListener( "input", ( event ) => {
  if ( answer.validity.patternMismatch ) {
    answer.setCustomValidity("Oh, it's not a right answer!");
  } else {
    answer.setCustomValidity( "" );
  }
});

So it basically checks on input event the state of patternMismatch property of input validity state. Any time the actual value doesn’t match the pattern we define the error message. If we have on the control any (other constrains we can cover them also in the event handler.

Handling forms with React and HTML5 Form Validation API

Are you not happy with the tooltips? Yeah, they look not the same in different browsers. Let’s add novalidate to the form element and customize error reporting:

<form novalidate>
  <label for="answer">What do you know, Jon Snow?</label>
  <input id="answer" name="answer" required pattern="nothing|nix">
  <div data-bind="message"></div>
  <button>Ask</button>
</form>
const answer = document.querySelector( "[name=answer]" ),
      answerError = document.querySelector( "[name=answer] + [data-bind=message]" );

answer.addEventListener( "input", ( event ) => {
  answerError.innerHTML = answer.validationMessage;
});
Handling forms with React and HTML5 Form Validation API

Even through this super brief introduction you can guess the power and flexibility behind the technology. What is most significant, it’s native form validation. So why we rely on countless custom libraries. Why not going with built-in validation?

React meets Form Validation API

react-html5-form connect React (and optionally Redux) to HTML5 Form Validation API. It exposes components Form and InputGroup (similar to Formsy custom input or FormItem of AntDesign). So Form defines the form and its scope and InputGroup the scope of the field, which can have one or more inputs. We simply wrap with these components an arbitrary form content (just plain HTML or React components). On user events we can request form validation and get the updated states of Form and InputGroup components accordingly to underlying input validity.

Well, let’s see it in practice. First, we define the form scope:

import React from "react";
import { render } from "react-dom";
import { Form, InputGroup } from "Form";

const MyForm = props => (
  <Form>
  {({ error, valid, pristine, submitting, form }) => (
      <>
      Form content
      <button disabled={ ( pristine || submitting ) } type="submit">Submit</button>
      </>
    )}
  </Form>
);

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

The scope receives state object with properties:

  • error - form error message (usually server validation message). We can set it with form.setError()
  • valid - boolean indicating if all the underlying inputs comply the specified constraints
  • pristine - boolean indicating if user has not interacted with the form yet
  • submitting - boolean indicating the form is being processed (switches to true when submit button pressed and back to false as soon as user-defined asynchronous onSubmit handler resolves)
  • form - instance of Form component to access the API

Here we use just pristine and submitting properties to switch submit button in disabled state.

In order to register inputs for validation while contributing form content we wrap them with InputGroup

  <InputGroup validate={[ "email" ]} }}>
  {({ error, valid }) => (
          <div>
            <label htmlFor="emailInput">Email address</label>
            <input
              type="email"
              required
              name="email"
              id="emailInput" />
            { error %26%26 (<div className="invalid-feedback">{error}</div>)  }
          </div>
  )}
  </InputGroup>

With the validate prop we specify what inputs of the group shall be registered. [ “email” ] means we have the only input, which name is “email”.

In the scope we receive the state object with following properties:

  • errors - array of error messages for all the registered inputs
  • error - the last emitted error message
  • valid - boolean indicating if all the underlying inputs comply the specified constraints
  • inputGroup - instance of the component to access the API

After rendering we get a form with email field. If the value is empty or contains an invalid email address on submission it shows the corresponding validation message next to the input.

Handling forms with React and HTML5 Form Validation API

Remember we were struggling with customizing errors messages while using native Form Validation API? It’s much easier with InputGroup:

<InputGroup
    validate={[ "email" ]}
    translate={{
      email: {
        valueMissing: "C'mon! We need some value",
        typeMismatch: "Hey! We expect an email address here"
      }
    }}>
...

We can specify a map per input, where keys are validity properties and values are custom messages.

Well the message customization was easy. What about custom validation? We can do though validate prop:

<InputGroup validate={{
    "email": ( input ) => {
      if ( !EMAIL_WHITELIST.includes( input.current.value ) ) {
        input.setCustomValidity( "Only whitelisted email allowed" );
        return false;
      }
      return true;
    }
  }}>
...

In this case instead of array of input names we provide a map, where keys are input names and values validation handlers. Handler checks input value (can be done asynchronously) and return validity state as a boolean. With input.setCustomValidity we assign a case specific validation message.

Validation on submit isn’t always what we want. Let’s implement “on-the-fly” validation. First we define event handler for input event:

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

Actually we just make here the input group re-validate every time user types in the input. We subscribe the control as follows:

<input
  type="email"
  required
  name="email"
  onInput={( e ) => onInput( e, inputGroup, form ) }
  id="emailInput" />

From now as soon as we change the input value it gets validated, if invalid we immediately receive the error message.

You can find the source code of a demo with examples from above.

By the way, do you fancy connecting the component-derived form state-tree to Redux store? We can do it also.

The package exposes reducer html5form containing state-trees of all the registered forms. We can connect it to the store like that:

import React from "react";
import { render } from "react-dom";
import { createStore, combineReducers } from "redux";
import { Provider } from "react-redux";

import { App } from "./Containers/App.jsx";
import { html5form } from "react-html5-form";

const appReducer = combineReducers({
  html5form
});

// Store creation
const store = createStore( appReducer );

render( <Provider store={store}>
  <App ></App>
 </Provider>, document.getElementById( "app" ) );

Now as we run the application we can find all form related states in the store.

Handling forms with React and HTML5 Form Validation API

Here is the source code of a dedicated demo.

Recap

React has no built-in form validation logic. Yet we can use 3rd party solutions. So it can be a DevKit, it can be a form builder, it can be a HOC or wrapper component mixing form validation logic into arbitrary form content. My personal approach in a wrapper component that relies on HTML built-in form validation API and exposes validity state in scopes of a form and a form field.