Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Handling Forms With React and HTML5 Form Validation API

DZone's Guide to

Handling Forms With React and HTML5 Form Validation API

In this tutorial, we will take a look at how to handle form submission and validation by using the modern combination of React and HTML5.

· Web Dev Zone ·
Free Resource

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

When we talk about user input within a web app we often think first of HTML forms. Web forms have been available since 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 forms and controls for interaction events and passing state with the "value" property. So form validation and submission logic are 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. Can we not abstract this logic and simply plug it in our forms? We definitely 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 the Form element and a form field with FormItem, which is a 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 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 the framework. If you do not work with a DevKit, you cannot benefit from its features.

Building Form Based on Schema

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

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

However, the schema 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 binds 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 the most popular solutions is Formsy. What 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 the  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 the validations property (see the 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 we don't rely on HTML5 built-in form validation, rather than going with countless custom implementations.

HTML5 Form Validation

The technology emerged quite a while ago. The first implementation came together with Opera 9.5 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 a 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 for one thing - the input element has a 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 (i.e. the field should not be empty).


Now we set the input an 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.

The error message isn't that informative though, 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 basically, on an input event, it checks the state of the patternMismatch property of the input validity state. Any time the actual value doesn't match the pattern we define the error message. If we have any other constraints on the control, we can also cover them in the event handler.

Not happy with the tooltips? Yeah, they don't look the same in different browsers. Let's add the following code:

<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;
});

Even with just this super brief introduction, you can see the power and flexibility of the technology. Native form validation is pretty great. So why are we relying on countless custom libraries? Why not going with built-in validation?

React Meets Form Validation API

react-html5-form connects React (and, optionally, Redux) to the HTML5 Form Validation API. It exposes the components Form and InputGroup (similar to Formsy's custom input or FormItem in AntDesign). So Form defines the form and its scope and InputGroup defines the scope of the field, which can have one or more inputs. We simply wrap an arbitrary form content (just plain HTML or React components) with these components. On user events, we can request form validation and get the updated states of the 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 with the specified constraints.
  • pristine - boolean indicating if the user has not interacted with the form yet.
  • submitting - boolean indicating the form is being processed (switches to true when the 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 the submit button to a 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 && (<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, with the name "email" applied to it.

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

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

After rendering, we get a form with an 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.

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 that through the 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 an array of input names, we provide a map, where keys are input names and values are validation handlers. The handler checks the input value (can be done asynchronously) and returns the validity's state as a boolean. With input.setCustomValidity, we assign a case-specific validation message.

Validation upon submission isn't always what we want. Let's implement an "on-the-fly" validation. First, we define an event handler for input event:

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

Actually, we just make the input group re-validate every time the 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 on, as soon as we change the input value it gets validated and if it's 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 a Redux store? We can do that also.

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

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.

Here is the source code of a dedicated demo.

Recap

React has no built-in form validation logic. Yet we can use third-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 is a wrapper component that relies on HTML built-in form validation APIs and exposes validity state in scopes of a form and a form field.

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

Topics:
react ,html5 ,web dev ,forms ,validation

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}