Generalised form validations

Linked Projects:

Calreact
 
Lesson code - https://github.com/learnetto/calreact/tree/m6l7-generalised-validations

Now, we’re finally ready to generalise our validation code so that we don’t hardcode the names of the fields.

We’ll refactor the code such that we can add multiple validations for multiple fields without writing custom code for each.

Instead of using a switch statement based on the fieldName, we’ll pass the validations for that field to this validateField method from AppointmentForm.

We’ll start by defining a static property on the AppointmentForm class called formValidations which will be an object with our field names as keys and an array of validations as their values.

export default class AppointmentForm extends React.Component {
  static formValidations = {
    title: [],
    appt_time: []
  }

Each field can have more than one validation. We’ll simply specify which validations to apply to each field here, and we’ll move the actual validation logic into a separate utility file.

We’ll pass formValidations for the field in question to the onUserInput call.

Because it’s a property of the class, we need to access it as AppointmentForm.formValidations.

handleChange = (e) => {
  const fieldName = e.target.name;
  const fieldValue = e.target.value;
  this.props.onUserInput(fieldName, fieldValue, AppointmentForm.formValidations);
}

Let’s do the same for appt_time.

setApptTime = (e) => {
  const fieldName = 'appt_time';
  const fieldValue = e.toDate();
  this.props.onUserInput(fieldName, fieldValue, AppointmentForm.formValidations);
}

And now let’s define the validations. We'll do title first. Let’s define it as an arrow function to which we pass a string and check its length.

We’ll use a method called checkMinLength which takes a string and a minimum length as arguments. Let’s return the value.

static formValidations = {
  title: [
    (s) => {return(checkMinLength(s, 3)) }
  ],

We’ll define checkMinLength in a utility file called validations.js in our utils directory.

export const validations = {
  checkMinLength: function(text, minLength) {
    if (text.length < minLength) {
      return `length should be at least ${minLength} characters`;
    }
  }
}

We're using backticks here for string interpolation.

Then let’s import it in our AppointmentForm component.

import moment from'moment';
import { validations } from '../utils/validations';

Then let’s change this to:

static formValidations = {
  title: [
    (s) => {return(validations.checkMinLength(s, 3)) }
  ],
  appt_time: []
}

Now let’s use it in our Appointments component in the handleUserInput method.

handleUserInput = (fieldName, fieldValue, validations) => {

Actually, in AppointmentForm we only need to pass the validations for the field we’re handling in that instance.

So then let’s use the key fieldName.

handleChange = (e) => {
  const fieldName = e.target.name;
  const fieldValue = e.target.value;
  this.props.onUserInput(fieldName, fieldValue,
                          AppointmentForm.formValidations[fieldName]);
}

Let’s do the same for appt_time.

setApptTime = (e) => {
  const fieldName = 'appt_time';
  const fieldValue = e.toDate();
  this.props.onUserInput(fieldName, fieldValue,
                          AppointmentForm.formValidations[fieldName]);
}

With these changes in our code, we’re getting just the field-specific validations in there.

We just need to pass the validations along to validateField in our Appointments component.

handleUserInput = (fieldName, fieldValue, validations) => {
  const newFieldState = update(this.state[fieldName],
                                {value: {$set: fieldValue}});
  this.setState({[fieldName]: newFieldState},
                () => { this.validateField(fieldName, fieldValue, validations) });
}

validateField (fieldName, fieldValue, validations) {

Then, let’s remove this switch statement like shown in the code below:

validateField (fieldName, fieldValue, validations) {
  let fieldValid;
  let fieldErrors = [];

  // removed switch statement

  const newFieldState = update(this.state[fieldName],

Now we’ll loop through this validations array, apply each validation to the field value, and, if it fails, we’ll add a corresponding error message to the fieldErrors array.

Then we’ll set the value of fieldValid based on whether fieldErrors is empty or not.

Let’s set fieldErrors to validations.reduce, which will take two arguments.

let fieldErrors = validations.reduce(

);

First, a function which will take an accumulator errors and a validation:

let fieldErrors = validations.reduce((errors, v) => {

});

The second argument for reduce will be an empty array, which is the initial value for errors.

let fieldErrors = validations.reduce((errors, v) => {

}, []);

Then let’s use a variable e to store the value of calling the validation v with fieldValue.

let fieldErrors = validations.reduce((errors, v) => {
  let e = v(fieldValue);
}, []);

Then, if e is not an empty string, we’ll add it to the errors array.

let fieldErrors = validations.reduce((errors, v) => {
  let e = v(fieldValue);
  if(e !== '') {
    errors.push(e);
  }
}, []);

And then return errors:

let fieldErrors = validations.reduce((errors, v) => {
  let e = v(fieldValue);
 if(e !== '') {
    errors.push(e);
  }
  return(errors);
}, []);

But we don’t actually return anything when the validation passes, so let’s return an empty string in our validation method checkMinLength.

export const validations = {
  checkMinLength: function(text, minLength) {
    if (text.length < minLength) {
      return `length should be at least ${minLength} characters`;
    } else {
      return '';
    }
  }
}

So let’s just go through the logic again.

We’re saying here that we want to apply one validation checkMinLength to the title, which will go into validateField here.

We’ll call it with fieldValue and store the return value in e.

If there’s an error, we’ll push it to the fieldErrors array.

Now let’s set fieldValid in validateField. We just need to check if fieldErrors.length equals zero. If fieldErrors is empty, that means that the field is valid.

fieldValid = fieldErrors.length === 0;

Now let’s test this by putting some letters into the appointment title bar.
[add screenshot 5:23-24]

It works fine for title.title.

For appt_time, we can choose any value and it’s valid because we haven’t defined any validations yet. Let’s do that now.

So in AppointmentForm, let’s add a validation for appt_time.

static formValidations = {
  title: [
    (s) => {return(validations.checkMinLength(s, 3)) }
  ],
  appt_time: [
    (t) => { return(validations.timeShouldBeInTheFuture(t))}
  ]
}

We’ll define it in validations js. We’ll use moment, so let’s import that first.

import moment from 'moment';

export const validations = {

Then:

import moment from 'moment'

export const validations = {
  checkMinLength: function(text, minLength) {
    if (text.length < minLength) {
      return `length should be at least ${minLength} characters`;
    } else {
      return '';
    }
  }

  timeShouldBeInTheFuture: function (t) {
    if(moment(t).isValid() && moment(t).isAfter()) {
      return '';
    } else {
      return 'can\'t be in the past';
    }
  }
}

Let’s just refactor checkMinLength to make the passing condition as the first check, so that we’re consistent.

checkMinLength: function(text, minLength) {
  if (text.length >= minLength) {
    return '';
  } else {
    return `length should be at least ${minLength} characters`;
  }
},

This just makes it clearer to read.

Now let’s try to run the app again.

Title validation works, appt_time is still not working. Looks like I forgot a comma here. 

 },

  timeShouldBeInTheFuture: function (t) {

The comma should fix that error. And so both validations work now. 

Then, let’s try the form and see if we can make an appointment.

[screenshot 7:14]

Looks like something’s broken. Maybe we’re not using our new state value somewhere.

Yes, in handleFormSubmit here, we need to add

handleFormSubmit = () => {
  const appointment = {title: this.state.title.value,
                       appt_time: this.state.appt_time};

and the same for appt_time.

handleFormSubmit = () => {
  const appointment = {title: this.state.title.value,
                       appt_time: this.state.appt_time.value};

Now let’s try again. Choose a title and date time, submit the form and it works.

[screenshot 7:40]

So we have successfully generalised our validations, but at the moment we’re only doing one validation per field.

So just to make sure, let’s add another validation to the title field and see if that also works.

So let’s add another validation called checkMaxLength.

static formValidations = {
  title: [
    (s) => {return(validations.checkMinLength(s, 3)) },
    (s) => {return(validations.checkMaxLength(s, 10)) }
  ],

Then in validations.js let’s define checkMaxLength. I’ll just copy paste checkMinLength and modify it.

checkMaxLength: function(text, maxLength) {
  if (text.length <= maxLength) {
    return '';
  } else {
    return `length should be a maximum of ${maxLength} characters`;
  }
},

Then let’s try it. Add a title longer than 10 characters.

When I type a validation longer than 10 characters, we get the error message for that validation. 
[screenshot 8:39] 

So our generalised validation code works!

But we don’t actually want to check for max length so let’s remove this bit of code. I just wanted to show you that it works.

That’s our refactor of the validations code done.

We changed a lot of things in refactoring our validation code so let's recap. Originally, we were doing all of our validations in the line, in validate form method. It worked but it was quite basic. Adding more validations would have been a bit messy and we couldn't add air messages for individual fields. Also, if two fields had the same type of validation, we couldn't reuse the code. We'd have to repeat it. So we decided to generalise that code and make it easily reusable and extendable.

First, we changed the fieldValues and state to object keys for the value and for validity, so that we could separately store the validity for each field. This made updating the value in state a little bit more complicated because now the value is nested inside an object. 

So we had to use immutability-helper to set new values.

We added a new method called validateField which accepts a field name, value and the list of validations, and sets the field validity and errors in the state.

We then moved the list of validations to a static property called formValidations in the AppointmentForm class.

And finally, we moved the actual validation logic to a separate utility file. Note that we are still choosing validations inside our component. We could go even further and abstract them out such that we could choose validations at the time of using the component by using a prop. 

For example, we could define a new component called Input, and part validations prop. In fact, a number of React validation form libraries do use this pattern so you can look them up to learn more. But that requires a lot more work and we don't really need it for our app at the moment. We'll just use our own simple validation code.

Liked this tutorial? Get more like this in your inbox