Skip to content

Configure Form Validation

Pamela Argentino edited this page Feb 17, 2017 · 2 revisions

Now that we have a fully operational CRUD, let's explore one of the features that redux-form makes it fairly straightforward to implement: form validation.

It's really easy to perform field-level and form-level validation with redux-form: you just supply a function that returns either an error message or undefined and redux-form evaluates it for you. Let's put it to practice now.

Creating a TextInput component

As mentioned in earlier lessons, the Field component can take into its component prop either a string (that has to be input, select or textarea) or a React component, be it either a class-based component or a stateless function-based component. When we pass a React component to Field, it injects various props to that component that's pertinent to it, such as input type, whether it has been touched (focused) before, if it has any error or warning messages, and many more metadata on the field.

The TextInput component we're going to create is a stateless component, therefore we can declare it as a function and create it inside the components directory.

nano src/components/TextInput.js
import React from 'react';

const TextInput = ({ input, label, type, placeholder, meta: { touched, error, warning } }) => (
  <fieldset
      className={`form-group ${
      touched && (
          error ? 'has-error' : warning ? 'has-warning' : 'has-success'
      )
          }`}
  >
    <label htmlFor={input.name} className="control-label">{label}</label>
    <input {...input} placeholder={placeholder} type={type} className="form-control" />
    {
      (() => (
        touched && (
          (error &&
            <span className="help-block">
              <i className="glyphicon glyphicon-remove-sign"></i> {error}
            </span>
          ) || (warning &&
            <span className="help-block">
              <i className="glyphicon glyphicon-exclamation-sign"></i> {warning}
            </span>
          )
        )
      ))()
    }
  </fieldset>
);

export default TextInput;

This component will replace the entirety of the fieldset blocks in ContactForm and will change colors according to the validation error level: if it's a validation error, it will turn red and an error message will appear below the field; if it's a warning, it will turn yellow and a similar message will appear as well. If the form has any validation errors, it won't allow you to submit it.

Refactoring ContactForm

The new ContactForm will look more or less like this:

nano src/containers/ContactForm.js
...
import TextInput from '../components/TextInput';
...
render() {
  const { handleSubmit, pristine, submitting, reset } = this.props;
  return (
    <form className="form-horizontal" onSubmit={handleSubmit}>
      <Field name="id" type="hidden" component="input" />

      <Field
        name="name"
        component={TextInput}
        type="text"
        label="Name"
        placeholder="e.g. John Doe"
      />

      <Field
        name="phone"
        component={TextInput}
        type="text"
        label="Phone"
        placeholder="(111) 111-1111"
      />

      <Field
        name="email"
        component={TextInput}
        type="email"
        label="Email"
        placeholder="john.doe@example.com"
      />

      <fieldset className="form-group">
        <button type="submit" disabled={pristine || submitting} className="btn btn-primary">Submit</button>&nbsp;
        <button type="button" disabled={pristine || submitting} className="btn btn-default" onClick={reset}>Reset</button>&nbsp;
        <Link to="/"><i className="glyphicon glyphicon-chevron-left"></i> Back to Home Page</Link>
      </fieldset>
    </form>
  )
}

Save, refresh your app and test it. It should be working the same as it did previously.

Except the component we created is capable of displaying the errors to the user and actively prevent them from submitting wrongly formatted data to the server.

Creating validation rules

The validation rules will be as following:

  • Name field
    • Must not be empty
    • Must not have more than 25 characters (more than enough for a full name)
  • Phone field
    • Must not be empty
    • Must match standard US phone number format (e.g. +1 (area code) prefix-suffix)
    • Must be unique
  • Email field
    • Must not be empty

Field-level Synchronous Validation

Most of the validation we're going to do on this form is synchronous, meaning we won't need the back-end to do it for us. Validation rules are functions that return a string containing a message if it has errors, or undefined otherwise. Let's apply the aforementioned rules to our form.

nano src/containers/ContactForm.js
...
class ContactForm extends Component {
  required(value) {
    return value && value.length > 0 ? undefined : 'This field is required.';
  }
  
  maxLength(max) {
    return value => value && value.length <= max ? undefined : `Must have ${max} characters or less.`;
  }
  
  phoneFormat(value) {
    return value && /^\+1 \([0-9]{3}\) [0-9]{3}-[0-9]{4}$/.test(value) ? undefined : 'Must be in US format, e.g. +1 (111) 111-1111';
  }
...
<Field
  name="name"
  component={TextInput}
  type="text"
  label="Name"
  placeholder="e.g. John Doe"
  validate={[
    this.required,
    (() => this.maxLength(25))() // maxLength returns the actual validation function,
                                 // so we need to actually call it here instead of just passing it
  ]}
/>
<Field
  name="phone"
  component={TextInput}
  type="text"
  label="Phone"
  placeholder="+1 (111) 111-1111"
  validate={[
    this.required,
    this.phoneFormat
  ]}
/>
<Field
  name="email"
  component={TextInput}
  type="email"
  label="Email"
  placeholder="john.doe@example.com"
  validate={this.required}
/>
...
}

Save and refresh the app. You'll notice that, if you fill a field with invalid values, the field will be highlighted red and you won't be able to submit the form.

Field-level Asynchronous Validation

There's one rule that we have specified that the phone number must be unique across the database. But how should we know that the phone number is unique if there's no other number to compare to? This is one of the cases when we need to check the server for validation while the form is being filled out.

Firstly, we will need to Base64-encode the phone before sending it to the server. We can do it with the btoa function that's already available in the browser. btoa converts the string to Base64 whilst atob de-converts it back. We also need to take care of, when the user is editing a contact, they must be able to submit the form while not changing the phone number, so the query must exclude the ID of the contact being edited. We will also need to adjust the back-end server to deal with such filtering.

Refactoring the server

First off, we will refactor the GET /contacts endpoint on server.js to parse a query string and pass it as a filter clause to MongoDB. Also, since Node.js doesn't have atob and btoa functions natively, we'll need to install them manually. Fortunately, we only need atob here.

yarn add atob
nano server.js
...
const atob = require('atob');
...
app.get('/contacts', (req, res) => {
  const { query } = req;
  let filters = {};

  if (Object.keys(query).length > 0) {
    filters = {
      phone: atob(query.phone)
    };

    if (query.id !== 'undefined' && query.id.length > 0) {
      Object.assign(filters, {
        _id: {
          '$ne': new ObjectID(query.id)
        }
      });
    }
  }

  db.collection(COLLECTION).find(filters).toArray((error, docs) => {
  ...
  });
});
...

We're retrieving the query string from the req object as an object itself and creating a filters object that will contain the query we'll pass to MongoDB. If the query string isn't empty, we'll pass in the decoded phone string and, if there's an id and it isn't undefined or empty (meaning we're editing a contact), we must eliminate it from the results, else we would just find the same contact we're editing. Then we pass the filters object to MongoDB and retrieve the results.

Implementing async validation to ContactForm

To implement async validation, instead of assigning it directly to the phone field like we did with the other validation rules, we need to declare it outside of the ContactForm class as a const (just like we usually do with Redux prop mapping functions) and pass it to the reduxForm call.

nano src/containers/ContactForm.js
...
const phoneUnique = values => {
  const { phone, id } = values;
  return fetch(`/contacts?phone=${btoa(phone)}&id=${id}`)
    .then(response => response.json())
    .then(json => {
      if (json.length > 0) {
        // eslint-disable-next-line
        throw { phone: 'This phone number already exists.' };
      }
    });
}

export default reduxForm({
  form: 'contact', // This is the form's name and must be unique across the app
  asyncValidate: phoneUnique,
  asyncBlurFields: ['phone'],
  enableReinitialize: true
})(ContactForm);

Note that redux-form will only apply async validators to fields once they pass the sync validation rules.

Save, reload and test the app.

git add .
git commit -m "Add sync and async validation rules to ContactForm"

Next step: Deploy to Heroku

Clone this wiki locally