DEV Community

Alejandro Luperon (he/him)
Alejandro Luperon (he/him)

Posted on • Edited on

React Form Validation

I personally use to find forms difficult. A lot of forms I have worked with would have fields that'd collect different types of data, perform different types of validation, and I never really had a structure or strategy to work with forms in such a way that I liked. A lot of the components in the forms, such as a text field, date picker, or specialized text fields that'd collect currency data or had requirements around the type of data entered (i.e. integers only, such as a zip code), would sometimes be hard for me to understand due to the varying considerations. I'd also feel my form components bloat significantly.

React to me (pre-hooks) really encouraged me to think more in an object-oriented way, which helped me personally to design forms and components that I liked more than previous libraries or patterns I used. It eventually felt a lot easier to visualize ways of managing forms, in terms of keeping the code DRY as possible, and also empowered me with ways to think about how I could have a relatively consistent interface for all of my input components no matter the data.

With that, this post will go into creating a form that will have 8 fields with 6 different types of validation, and an optional field. The form has a date picker, some text fields, an integer-only fixed-length field, and a currency field.

If you would like to access the code in this repo prior to reading the post, check the code out here. πŸ˜ƒπŸ˜ƒπŸ˜ƒ

localhost_3000_sign-up-form-with-urlemail (4).png

The following fields below will be in the form along with the validations that will be performed.

  • First Name: Cannot be empty.
  • Last Name: Cannot be empty.
  • E-mail: Cannot be empty.
  • Start Date: Cannot be empty (I'll use react-datetime, but this can be changed out to your own component or a library of your choosing).
  • Personal URL (Optional): An optional field, that, if filled, must be a valid URL.
  • Salary: Cannot be empty. Must be a valid currency (using react-currency-format) to collect currency information.
  • Occupation: Cannot be empty.
  • Zipcode: Cannot be empty. Is only going to allow for integer inputs.

I will start with a react component, with each field being represented in state with an object:

import React, { Component } from 'react';

class SignUpForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      firstName: {
        value: "",
        error: false
      },
      lastName: {
        value: "",
        error: false
      },
      email: {
        value: "",
        error: false
      },
      startdate: {
        value: "",
        error: false
      },
      url: {
        value: "",
        error: false
      },
      salary: {
        value: "",
        quantity: "",
        error: false,
        errorMessage: "Salary cannot be empty."
      },
      occupation: {
        value: "",
        error: false
      },
      zipcode: {
        value: "",
        error: false
      }
    }
  }
  render() {
    return (null)
  }
}

export default SignUpForm;
Enter fullscreen mode Exit fullscreen mode

Each object in state for each field has at least two values, value and error, set to false. The reason for this is because the form will eventually control the error state of the children field components. The salary key in state also has two additional properties, quantity, and errorMessage. The CurrencyFormat component we use comes from react-currency-format, and will return two values when the onChange event is triggered, a value that represents the formatted currency amount (i.e. '$60,000'), and an unformatted currency amount (i.e. '60000').

The name of each key in state is going to be the name of each 'input' inside our form. So that way, whenever our various onChange events are called, we can update the appropriate key in state.

Next, we'll add the fields to our render method that only collect text, 'First Name', 'Last Name', and 'Occupation', along with the corresponding onChange method.

import React, { Component } from 'react';
import TextField from 'components/TextField.component';
import './signupform.css';

class SignUpForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      firstName: {
        value: "",
        error: false
      },
      lastName: {
        value: "",
        error: false
      },
      email: {
        value: "",
        error: false
      },
      startdate: {
        value: "",
        error: false
      },
      url: {
        value: "",
        error: false
      },
      salary: {
        value: "",
        quantity: "",
        error: false,
        errorMessage: "Salary cannot be empty."
      },
      occupation: {
        value: "",
        error: false
      },
      zipcode: {
        value: "",
        error: false
      }
    }
  }

  onChange(event) {
    this.setState({ [event.target.name]: {
        ...this.state[event.target.name],
        value: event.target.value,
        error: this.state[event.target.name].error
      }
    })
  }

  render() {
    let {
      firstName,
      lastName,
      occupation
    } = this.state;

    return(
      <div className="container">
        <form className="signup-form">
          <TextField
            error={firstName.error}
            value={firstName.value}
            name="firstName"
            errorMessage="First name cannot be empty."
            label="First Name"
            placeholder="First Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={lastName.error}
            value={lastName.value}
            name="lastName"
            errorMessage="Last name cannot be empty."
            label="Last Name"
            placeholder="Last Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={occupation.error}
            value={occupation.value}
            name="occupation"
            errorMessage="Occupation cannot be empty."
            label="Occupation"
            placeholder="Occupation"
            onChange={this.onChange.bind(this)} />
        </form>
      </div>
    )
  }
}

export default SignUpForm;
Enter fullscreen mode Exit fullscreen mode

localhost_3000_sign-up-form-text-only.png

If we look at the onChange event:

  onChange(event) {
    let { name, value } = event.target;
    this.setState({ [name]: {
        ...this.state[name],
        value: event.target.value
      }
    })
  }
Enter fullscreen mode Exit fullscreen mode

The event object from the TextField gets passed to the onChange function in our SignUpForm, where the 'name' and 'value' properties get extracted from event.target.

These values are used to update state for the key indicated by name. If the TextField's name is not represented as a key in state, you may get an undefined error when the onChange event is triggered on that field.

Looking at our TextField component:

import React, { Component } from 'react';
import './TextField.css';

class TextField extends Component {
  static defaultProps = {
    size: "large"
  }

  setSize() {
    let { size } = this.props;
    if (size === 'large') return 'textfield-large';
  }

  render() {
    let {
      name,
      value,
      placeholder,
      label,
      errorMessage,
      error,
      onChange
    } = this.props;

    return(
      <div className={`d-flex flex-column ${this.setSize()}`}>
        <label className="form-item-label">{label}</label>
        <input
          className={`textfield regular-text ${this.setSize()}`}
          value={value}
          name={name}
          placeholder={placeholder}
          onChange={onChange.bind(this)} />
        {(error ? <div className="error-text-container"><div className="error-text form-item-error">{errorMessage}</div></div> : null)}
      </div>
    )
  }
}

export default TextField;
Enter fullscreen mode Exit fullscreen mode

The TextField component accepts several properties. The prop label sets the label of the TextField, onChange is a property that is passed from the parent component, and value is passed down from the parent component as well. Whenever the input's onChange event is triggered, the parent manages the update and passes the new value down as a prop. The property errorMessage is shown when the property error is true.

If a validation is triggered and failed in the parent, the TextField's error property will be set to true.

At this point, instead of continuing to add additional fields to the form, I like to trigger the form submission and see if my currently added fields are working as expected, so let's do that below:

import React, { Component } from 'react';
import TextField from 'components/TextField.component';
import Button from 'components/Button.component';
import {
  isFilled
} from 'common/helpers/validators';
import './signupform.css';

class SignUpForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      firstName: {
        value: "",
        error: false
      },
      lastName: {
        value: "",
        error: false
      },
      email: {
        value: "",
        error: false
      },
      startdate: {
        value: "",
        error: false
      },
      url: {
        value: "",
        error: false
      },
      salary: {
        value: "",
        quantity: "",
        error: false,
        errorMessage: "Salary cannot be empty."
      },
      occupation: {
        value: "",
        error: false
      },
      zipcode: {
        value: "",
        error: false
      },
      result: ""
    }
  }

  onChange(event) {
    let { name, value } = event.target;
    this.setState({ [name]: {
        ...this.state[name],
        value: event.target.value
      }
    })
  }

  isFieldValid(validator, key) {
    let isValid = validator(this.state[key].value);

    this.setState({
      [key]: {
        value: this.state[key].value,
        error: !isValid,
        errorMessage: this.state[key].errorMessage
      }
    });

    return isValid;
  }

  submit(event) {
    event.preventDefault();
    if (!this.validate()) {
      return;
    }
    let { firstName, lastName, occupation } = this.state;
    this.setState({result: `Success! First Name: ${firstName.value}, Last Name: ${lastName.value}, Occupation: ${occupation.value}`});
  }

  validate() {
    let fields = new Set();
    fields.add(this.isFieldValid(isFilled, "firstName"));
    fields.add(this.isFieldValid(isFilled, "lastName"));
    fields.add(this.isFieldValid(isFilled, "occupation"));

    return !fields.has(false);
  }

  render() {
    let {
      firstName,
      lastName,
      occupation
    } = this.state;

    return(
      <div className="container">
        <form onSubmit={this.submit.bind(this)} className="signup-form">
          <TextField
            error={firstName.error}
            value={firstName.value}
            name="firstName"
            errorMessage="First name cannot be empty."
            label="First Name"
            placeholder="First Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={lastName.error}
            value={lastName.value}
            name="lastName"
            errorMessage="Last name cannot be empty."
            label="Last Name"
            placeholder="Last Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={occupation.error}
            value={occupation.value}
            name="occupation"
            errorMessage="Occupation cannot be empty."
            label="Occupation"
            placeholder="Occupation"
            onChange={this.onChange.bind(this)} />
          <Button
            style={{marginTop: '25px'}}
            type="submit"
            label="Submit"/>
        </form>
        <div style={{marginTop: '25px'}}>{this.state.result}</div>
      </div>
    )
  }
}

export default SignUpForm;
Enter fullscreen mode Exit fullscreen mode

There were three functions added in addition to the Submit button: submit, validate, and isFieldValid. Let's first look at submit:

  submit(event) {
    event.preventDefault();
    if (!this.validate()) {
      return;
    }
    let { firstName, lastName, occupation } = this.state;
    this.setState({result: `Success! First Name: ${firstName.value}, Last Name: ${lastName.value}, Occupation: ${occupation.value}`});
  }
Enter fullscreen mode Exit fullscreen mode

We call event.preventDefault() to stop the page from refreshing when the submit event is triggered on the form. After that, we have an if statement, that exits the function if our validate function returns false.
If our validate function returns true, then the values of the fields in our form is printed just below the form, just to temporarily test our expected behavior.

Our validate function runs validations on all of our fields in our form:

  validate() {
    let fields = new Set();
    fields.add(this.isFieldValid(isFilled, "firstName"));
    fields.add(this.isFieldValid(isFilled, "lastName"));
    fields.add(this.isFieldValid(isFilled, "occupation"));

    return !fields.has(false);
  }
Enter fullscreen mode Exit fullscreen mode

If a field does not contain valid data, then a false is added to the set, and thus validate returns false. Our function isFieldValid takes two arguments, a function to validate the data of the field, and the second argument is the key in state that the field corresponds to. That key is used to retrieve the field's value and pass it to it's validator:

  isFieldValid(validator, key) {
    let isValid = validator(this.state[key].value);

    this.setState({
      [key]: {
        value: this.state[key].value,
        error: !isValid,
        errorMessage: this.state[key].errorMessage
      }
    });
Enter fullscreen mode Exit fullscreen mode

We import a function isFilled, which checks to see if the item passed is empty. To do that, I have used a function from validator. We didn't have to use validator, I just elected to use it for ease and convenience, but if you don't want to include another package, we can also replace the code within the isFilled function with your own logic.

export const isFilled = (value) => {
  return !validator.isEmpty(value + "") && value !== null;
}
Enter fullscreen mode Exit fullscreen mode

At this point, if we click submit on an empty form, we will see:

localhost_3000_sign-up-form-with-button.png

If we add data and click the submit button, we will see the values we added to the fields in our form:

localhost_3000_sign-up-form-with-button (1).png

Next, we'll add our url and email fields, and make corresponding updates to the validate function to check the validity of these two fields. url is an optional field, but if it is not empty, it needs to be a valid URL.

import React, { Component } from 'react';
import TextField from 'components/TextField.component';
import Button from 'components/Button.component';
import {
  isFilled,
  isEmailValid,
  isURLValid,
} from 'common/helpers/validators';
import './signupform.css';

class SignUpForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      firstName: {
        value: "",
        error: false
      },
      lastName: {
        value: "",
        error: false
      },
      email: {
        value: "",
        error: false
      },
      startdate: {
        value: "",
        error: false
      },
      url: {
        value: "",
        error: false
      },
      salary: {
        value: "",
        quantity: "",
        error: false,
        errorMessage: "Salary cannot be empty."
      },
      occupation: {
        value: "",
        error: false
      },
      zipcode: {
        value: "",
        error: false
      },
      result: ""
    }
  }

  onChange(event) {
    let { name, value } = event.target;
    this.setState({ [name]: {
        ...this.state[name],
        value: event.target.value
      }
    })
  }

  isFieldValid(validator, key) {
    let isValid = validator(this.state[key].value);

    this.setState({
      [key]: {
        value: this.state[key].value,
        error: !isValid,
        errorMessage: this.state[key].errorMessage
      }
    });

    return isValid;
  }

  submit(event) {
    event.preventDefault();
    if (!this.validate()) {
      return;
    }
    let { firstName, lastName, occupation } = this.state;
    this.setState({result: `Success! First Name: ${firstName.value}, Last Name: ${lastName.value}, Occupation: ${occupation.value}`});
  }

  validate() {
    let fields = new Set();
    fields.add(this.isFieldValid(isFilled, "firstName"));
    fields.add(this.isFieldValid(isFilled, "lastName"));
    fields.add(this.isFieldValid(isFilled, "occupation"));
    fields.add(this.isFieldValid(isEmailValid, "email"))
    fields.add(this.isPersonalURLValid());

    return !fields.has(false);
  }

  isPersonalURLValid() {
    let { value } = this.state.url;

    let isValid = isURLValid(value) || value.length === 0;

    this.setState({
      url: {
        ...this.state.url,
        error: !isValid
      }
    });

    return isValid;
  }

  render() {
    let {
      firstName,
      lastName,
      occupation,
      url,
      email
    } = this.state;

    return(
      <div className="container">
        <form onSubmit={this.submit.bind(this)} className="signup-form">
          <TextField
            error={firstName.error}
            value={firstName.value}
            name="firstName"
            errorMessage="First name cannot be empty."
            label="First Name"
            placeholder="First Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={lastName.error}
            value={lastName.value}
            name="lastName"
            errorMessage="Last name cannot be empty."
            label="Last Name"
            placeholder="Last Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={occupation.error}
            value={occupation.value}
            name="occupation"
            errorMessage="Occupation cannot be empty."
            label="Occupation"
            placeholder="Occupation"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={url.error}
            value={url.value}
            name="url"
            errorMessage="Please enter a vaild url."
            label="Personal Website (Optional)"
            placeholder="Personal Website"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={email.error}
            value={email.value}
            name="email"
            errorMessage="Please enter a vaild e-mail."
            label="E-mail"
            placeholder="E-mail"
            onChange={this.onChange.bind(this)} />
          <Button
            style={{marginTop: '25px'}}
            type="submit"
            label="Submit"/>
        </form>
        <div style={{marginTop: '25px'}}>{this.state.result}</div>
      </div>
    )
  }
}

export default SignUpForm;
Enter fullscreen mode Exit fullscreen mode

localhost_3000_sign-up-form-with-urlemail.png

We've updated the validate function:

  validate() {
    let fields = new Set();
    fields.add(this.isFieldValid(isFilled, "firstName"));
    fields.add(this.isFieldValid(isFilled, "lastName"));
    fields.add(this.isFieldValid(isFilled, "occupation"));
    fields.add(this.isFieldValid(isEmailValid, "email"))
    fields.add(this.isPersonalURLValid());

    return !fields.has(false);
  }
Enter fullscreen mode Exit fullscreen mode

To check for e-mail validity, I have again used a function from the validator library like so:

export const isEmailValid = (email) => {
  return validator.isEmail(email)
}
Enter fullscreen mode Exit fullscreen mode

There is another new function also called when validate is called, and that is isPersonalURLValid. Because 'Personal Website' is an optional field, it's okay for it to be empty, it just has to be a valid URL if not. Our function looks like:

  isPersonalURLValid() {
    let { value } = this.state.url;

    let isValid = isURLValid(value) || value.length === 0;

    this.setState({
      url: {
        ...this.state.url,
        error: !isValid
      }
    });

    return isValid;
  }
Enter fullscreen mode Exit fullscreen mode

This function checks to see if the url value is either an empty string or a valid URL using our isURLValid function, again leaning on the validator library to provide a function to check validity:

export const isURLValid = (url) => {
  return validator.isURL(url);
}
Enter fullscreen mode Exit fullscreen mode

With these fields added, our form now looks like this whenever submit is triggered and no data is entered:

localhost_3000_sign-up-form-with-urlemail (1).png

If data is entered into the 'Personal Website' text field, our form looks like:

localhost_3000_sign-up-form-with-urlemail (2).png

We have three fields left, our 'Desired Start Date', 'Zipcode' and 'Desired Salary' fields, so let's add them:

import React, { Component } from 'react';
import TextField from 'components/TextField.component';
import Button from 'components/Button.component';
import DatePicker from 'components/DatePicker.component';
import CurrencyFormat from 'react-currency-format';
import {
  isFilled,
  isEmailValid,
  isURLValid,
  isLengthValid
} from 'common/helpers/validators';
import './signupform.css';

class SignUpForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      firstName: {
        value: "",
        error: false
      },
      lastName: {
        value: "",
        error: false
      },
      email: {
        value: "",
        error: false
      },
      startdate: {
        value: "",
        error: false
      },
      url: {
        value: "",
        error: false
      },
      salary: {
        value: "",
        quantity: "",
        error: false,
        errorMessage: "Salary cannot be empty."
      },
      occupation: {
        value: "",
        error: false
      },
      zipcode: {
        value: "",
        error: false
      },
      result: ""
    }
  }

  onChange(event) {
    let { name, value } = event.target;
    this.setState({ [name]: {
        ...this.state[name],
        value: event.target.value
      }
    })
  }

  isFieldValid(validator, key) {
    let isValid = validator(this.state[key].value);

    this.setState({
      [key]: {
        value: this.state[key].value,
        error: !isValid,
        errorMessage: this.state[key].errorMessage
      }
    });

    return isValid;
  }

  submit(event) {
    event.preventDefault();
    if (!this.validate()) {
      return;
    }
    let { firstName, lastName, occupation, email, url, salary, startdate, zipcode } = this.state;
    this.setState({result: `Success! First Name: ${firstName.value}, Last Name: ${lastName.value}, Occupation: ${occupation.value}, Email: ${email.value}, URL: ${url.value}, Zipcode: ${zipcode.value}, Desired Start Date: ${startdate.value}, Desired Salary: ${salary.value}`});
  }

  validate() {
    let fields = new Set();
    fields.add(this.isFieldValid(isFilled, "firstName"));
    fields.add(this.isFieldValid(isFilled, "lastName"));
    fields.add(this.isFieldValid(isFilled, "occupation"));
    fields.add(this.isFieldValid(isEmailValid, "email"))
    fields.add(this.isPersonalURLValid());
    fields.add(this.isFieldValid(isFilled, "startdate"));
    fields.add(this.isSalaryMin(min, 60000, "salary", "Minimum salary is $60,000."));
    fields.add(this.isZipcodeValid());

    return !fields.has(false);
  }

  isPersonalURLValid() {
    let { value } = this.state.url;

    let isValid = isURLValid(value) || value.length === 0;

    this.setState({
      url: {
        ...this.state.url,
        error: !isValid
      }
    });

    return isValid;
  }


  isSalaryMin(validator, value, key, errorMessage) {
    this.setState({
      [key]: {
        quantity: this.state[key].quantity,
        value: this.state[key].value,
        error: !validator(this.state[key].quantity, value),
        errorMessage: errorMessage
      }
    });

    return validator(this.state[key].quantity, value);
  }

  onValueChange(values) {
    const {formattedValue, value} = values;
    this.setState({ salary: {
        ...this.state.salary,
        value: formattedValue,
        quantity: value,
      }
    });
  }

  onChangeDate(key, value) {
    this.setState({
      [key]: {
        value,
        error: false
      },
    });
  }

  onChangeZipcode(event) {
    let { value } = event.target

    if (value.length > 5) return;

    let quantity = 0;

    let OK = /[0-9+$]/.test(value)

    if (!OK && value.length > 0) return;

    if (value.length > 0) {
      value = parseInt(value);
    } else {
      value = "";
    }

    this.setState({ zipcode: {
        value
      }
    })
  }

  isZipcodeValid() {
    let value = this.state.zipcode.value.toString();
    let isValid = isLengthValid(value, 5);
    let errorMessage = "Zipcode cannot be empty.";

    if (!isValid && value.length > 0) {
      errorMessage = "Zipcode must be five digits.";
    }

    this.setState({
      zipcode: {
        ...this.state.zipcode,
        error: !isValid,
        errorMessage
      }
    });
    return isValid;
  }

  render() {
    let {
      firstName,
      lastName,
      occupation,
      url,
      email,
      startdate,
      salary,
      zipcode
    } = this.state;

    return(
      <div className="container">
        <form onSubmit={this.submit.bind(this)} className="signup-form">
          <TextField
            error={firstName.error}
            value={firstName.value}
            name="firstName"
            errorMessage="First name cannot be empty."
            label="First Name"
            placeholder="First Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={lastName.error}
            value={lastName.value}
            name="lastName"
            errorMessage="Last name cannot be empty."
            label="Last Name"
            placeholder="Last Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={occupation.error}
            value={occupation.value}
            name="occupation"
            errorMessage="Occupation cannot be empty."
            label="Occupation"
            placeholder="Occupation"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={url.error}
            value={url.value}
            name="url"
            errorMessage="Please enter a vaild url."
            label="Personal Website (Optional)"
            placeholder="Personal Website"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={email.error}
            value={email.value}
            name="email"
            errorMessage="Please enter a vaild e-mail."
            label="E-mail"
            placeholder="E-mail"
            onChange={this.onChange.bind(this)} />
          <DatePicker
            timeFormat={false}
            isValidDate={(current) => current > new Date()}
            value={(startdate.value ? new Date(startdate.value) : null)}
            placeholder="Desired Start Date"
            errorMessage="Desired start date cannot be empty."
            error={startdate.error}
            onChange={this.onChangeDate.bind(this, "startdate")}
            label="Desired Start Date"
            size="large"/>
          <CurrencyFormat
            thousandSeparator={true}
            prefix='$'
            customInput={TextField}
            name="salary"
            value={salary.quantity}
            error={salary.error}
            errorMessage={salary.errorMessage}
            label="Desired Salary - Min. $60,000"
            placeholder='Desired Salary'
            onValueChange={this.onValueChange.bind(this)} />
          <TextField
            name="zipcode"
            label="Zipcode"
            error={zipcode.error}
            value={zipcode.value}
            errorMessage={zipcode.errorMessage}
            placeholder="Zipcode"
            onChange={this.onChangeZipcode.bind(this)} />
          <Button
            style={{marginTop: '25px'}}
            type="submit"
            label="Submit"/>
        </form>
        <div style={{marginTop: '25px'}}>{this.state.result}</div>
      </div>
    )
  }
}

export default SignUpForm;
Enter fullscreen mode Exit fullscreen mode

We've added another five functions for the three added fields. Starting with zipcode, we've added another validate function, isZipcodeValid, and onChangeZipcode. The 'Zipcode' field is 5 digits, and can only contain integers, thus, our onChangeZipcode function will disallow non-integer characters, and limit the length of the value to 5:

  onChangeZipcode(event) {
    let { value } = event.target

    if (value.length > 5) return;

    let OK = /[0-9+$]/.test(value)

    if (!OK && value.length > 0) return;

    if (value.length > 0) {
      value = parseInt(value);
    } else {
      value = "";
    }

    this.setState({ zipcode: {
        value
      }
    })
  }
Enter fullscreen mode Exit fullscreen mode

For this field, I use a regular expression to check for validity, but you can use any library or method you prefer.

For the function to check the inputs validity, we check for the fields length:

  isZipcodeValid() {
    let value = this.state.zipcode.value.toString();
    let isValid = isLengthValid(value, 5);
    let errorMessage = "Zipcode cannot be empty.";

    if (!isValid && value.length > 0) {
      errorMessage = "Zipcode must be five digits.";
    }

    this.setState({
      zipcode: {
        ...this.state.zipcode,
        error: !isValid,
        errorMessage
      }
    });
    return isValid;
  }
Enter fullscreen mode Exit fullscreen mode

If the value for zipcode is greater than 0 in length, but less than 5, then the errorMessage for zipcode becomes "Zipcode must be five digits.". If the field is empty, then the errorMessage is: "Zipcode cannot be empty."

This example is to illustrate an instance in which a field could have multiple possible reasons of failure that you may want to communicate to the user.

Our next field that we'll look at is 'Desired Start Date'. We've added an onChange function specific to this field:

  onChangeDate(key, value) {
    this.setState({
      [key]: {
        value,
        error: false
      },
    });
  }
Enter fullscreen mode Exit fullscreen mode

Our DatePicker component itself looks like:

import Datetime from 'react-datetime'
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import './DatePicker.css';

const dateFormat = 'MM/DD/YYYY';

class DatePicker extends Component {
  static propTypes = {
    label: PropTypes.string,
    size: 'large'
  }

  constructor(props) {
    super(props);
    this.state = {
      value: props.value,
      active: false,
      date: null,
      focused: false
    };
  }

  setSize() {
    let { size } = this.props;
    if (size === 'large') return 'textfield-large';
  }

  getBase() {
    let { timeFormat, value, onChange, placeholder, isValidDate } = this.props;
    let className = `textfield regular-text ${this.setSize()}`;
    return (<Datetime
      isValidDate={isValidDate}
      timeFormat={timeFormat}
      dateFormat="YYYY-MM-DD"
      placeholder={placeholder}
      value={(value ? moment(value, dateFormat) : null)}
      onChange={onChange.bind(this)}
      inputProps={{ className, readOnly: true, placeholder: `${placeholder}`}} />);
  }

  render() {
    let { size, error, errorMessage } = this.props;

    return (
      <div className={`d-flex flex-column ${this.setSize()}`}>
        <label className="form-item-label">{this.props.label}</label>
        {this.getBase()}
        {(error ? <div className="error-text-container"><div className="error-text form-item-error">{errorMessage}</div></div> : null)}
      </div>
    );
  }
}

export default DatePicker;
Enter fullscreen mode Exit fullscreen mode

And, in our form, the instance of our DatePicker component in our form looks like:

          <DatePicker
            timeFormat={false}
            isValidDate={(current) => current > new Date()}
            value={(startdate.value ? new Date(startdate.value) : null)}
            placeholder="Desired Start Date"
            errorMessage="Desired start date cannot be empty."
            error={startdate.error}
            onChange={this.onChangeDate.bind(this, "startdate")}
            label="Desired Start Date"
            size="large"/>
Enter fullscreen mode Exit fullscreen mode

I personally find react-datetime to be very friendly, as you can set a date range, pass a css class to the DatePicker which will style it to your liking, and specify a time option as well, if interested. But, you can use any DatePicker of your choice.

Our last field is our salary field:

          <CurrencyFormat
            thousandSeparator={true}
            prefix='$'
            customInput={TextField}
            name="salary"
            value={salary.quantity}
            error={salary.error}
            errorMessage={salary.errorMessage}
            label="Desired Salary - Min. $60,000"
            placeholder='Desired Salary'
            onValueChange={this.onValueChange.bind(this)} />
Enter fullscreen mode Exit fullscreen mode

We've added two functions for this field, 'isSalaryMin' and 'onValueChange'.

The CurrencyFormat component from react-currency-format I also find to be easy to use. I didn't want to spend time to make my own currency component, so I used this library and integrated it to work with my form.

CurrencyFormat is great because you can pass a base component to the customInput prop, which will effectively wrap the CurrencyFormat magic around the passed component. You can still access the props of your base component as well.

onChange of the currency input, two values are returned, the formatted currency amount, and the unformatted currency amount. We have a custom function onValueChange to grab those two values and set them in state for the salary key:

  onValueChange(values) {
    const {formattedValue, value} = values;
    this.setState({ salary: {
        ...this.state.salary,
        value: formattedValue,
        quantity: value,
      }
    });
  }
Enter fullscreen mode Exit fullscreen mode

The function we use to validate the value of the salary amount, isSalaryMin, is invoked in the validate function. It's had a couple updates, so let's take a closer look:

  validate() {
    let fields = new Set();
    fields.add(this.isFieldValid(isFilled, "firstName"));
    fields.add(this.isFieldValid(isFilled, "lastName"));
    fields.add(this.isFieldValid(isFilled, "occupation"));
    fields.add(this.isFieldValid(isEmailValid, "email"))
    fields.add(this.isPersonalURLValid());
    fields.add(this.isFieldValid(isFilled, "startdate"));
    fields.add(this.isSalaryMin(min, 60000, "salary", "Minimum salary is $60,000."));
    fields.add(this.isZipcodeValid());

    return !fields.has(false);
  }
Enter fullscreen mode Exit fullscreen mode

The isSalaryMin function is passed a validator, the minimum salary of $60,000 in integer form, and an error message that is set on error.

I added these for arguments to the isSalaryMin function because I thought about also adding an upper bound to salary and naming the function isSalaryValid instead, so that way I could reuse the function for both the lower and upper bound validation. It'd allow me to pass a different validator function and other arguments, but for the purposes of this example, there is no upper bound.

We see that our other new fields added are validated. The isZipcodeValid function is called, and we also check to see if a date has been selected.

Finally, looking at our isSalaryMin function, we have:


  isSalaryMin(validator, value, key, errorMessage) {
    this.setState({
      [key]: {
        quantity: this.state[key].quantity,
        value: this.state[key].value,
        error: !validator(this.state[key].quantity, value),
        errorMessage: errorMessage
      }
    });

    return validator(this.state[key].quantity, value);
  }

Enter fullscreen mode Exit fullscreen mode

With our form complete, our form looks like:

localhost_3000_sign-up-form-with-urlemail (4).png

With this approach, I am able to share my validators across my entire app, I can easily change out the guts of my components at any time, and the parent form component is in charge of validation.

My form component is under 300 lines of code, and I am definitely certain there is probably opportunity to make the form smaller and more lean or easier to understand.

Please check out a repo with the final code here. πŸ˜ƒπŸ˜ƒπŸ˜ƒ

Top comments (0)