Basic form validation

Linked Projects:

Calreact
 
Lesson code - https://github.com/learnetto/calreact/tree/m6l1-rails-validation

Transcript

In this lesson we're going to look at handling Rails form validations with React.

We'll use our Calendar appointments app and add a simple validation to check that a value for the title of an appointment is present.

At the moment, in our app, the user can submit the form without entering anything in the title field.

The appointment gets added anyway, with just an appointment date and time.

We don't want that. So we'll add a Rails validation and handle the error when that validation fails.

Let's start off by adding a validation on the title field of the Appointment model.

validates_presence_of :title

Now, if we try creating an appointment without a title, the appointment won't get added.

And if we look in the browser console, we'll see an error.

POST http://localhost:5000/appointments 422 (Unprocessable Entity)

That's coming from the appointments controller.

  def create
    @appointment = Appointment.new(appointment_params)
    if @appointment.save
      render json: @appointment
    else
      render json: @appointment.errors, status: :unprocessable_entity
    end
  end

It tries to save the appointment, and when it fails, it renders @appointment.errors and sets the status to Unprocessable Entity which is the HTTP status code 422.

But, we don't show any error message in the browser. So the user won't know what went wrong.

Let's now add a way to handle the error that the controller is rendering.

Remember, we handle the response from the controller in the Appointments component in the handleFormSubmit function.

  handleFormSubmit () {
    const appointment = {title: this.state.title, appt_time: this.state.appt_time};
    $.post('/appointments',
            {appointment: appointment})
          .done((data) => {
            this.addNewAppointment(data);
          });
  }

We've defined how to handle the response to a successful AJAX call with .done but we haven't set how to handle a failed call. 
Let's do that now by chaining a .fail callback.

          .fail((response) => {
            console.log(response);
          });

Now, let's submit the form again.

So now when I submit the form, in addition to the POST error, I also get this console log output here which is our error object.

If I open it up, I can see the details are inside this responseJSON object.
The error object

It contains a key "title" with an array as its value.
And this array has 1 element which is a string with the value "can't be blank", which is what we expect from a failed presence validation.

Now let's take this error message and display it above the form.

We'll do that by adding the errors returned by the controller to the state of our app and displaying that state value in our Appointments component.

So let me start by adding an initial state value.

Let's call it formErrors and set its initial value to an empty object.

  constructor (props, railsContext) {
    super(props)
    this.state = {
      appointments: this.props.appointments,
      title: 'Team standup meeting',
      appt_time: '25 January 2016 9am',
      formErrors: {}
    }
  }

And now let's use it in our fail callback.

Let's call:

          .fail((response) => {
            console.log(response);
            this.setState({formErrors: response.responseJSON})
          });

And now let's use this value in our render function.

Let's create a new component called FormErrors and pass this value as a prop to it

  render () {
    return (
      <div>
        <FormErrors formErrors = {this.state.formErrors} />
        <AppointmentForm input_title={this.state.title}
          input_appt_time={this.state.appt_time}
          onUserInput={(obj) => this.handleUserInput(obj)}
          onFormSubmit={() => this.handleFormSubmit()} />
        <AppointmentsList appointments={this.state.appointments} />
      </div>
    )
  }

Now let's create this component.

We'll create a new file in our components directory and save it as FormErrors.jsx.

Let's hard code a value first just to see this works

import React from 'react';

export const FormErrors = ({formErrors}) =>
  <div>
    {formErrors["title"]}
  </div>

Now we need to import this component into the Appointments component.

import { FormErrors } from './FormErrors';

Ok let's try this in the browser now.

We need to refresh the page this time because we've added a new value to the initial state.

We'll submit the form with an empty title field.

Now our title field error mesage appears above the form.

But of course, we don't want to hard code the name of the field in our component.

We want to be able to handle all the errors on any fields in our form.

So let's generalise the code. 

import React from 'react';

export const FormErrors = ({formErrors}) =>
  <div>
    {Object.keys(formErrors).map((formErrorField) => {
      return (
        formErrors[formErrorField].map((error) => {
          return (
            <p>{formErrorField} {error}</p>
          )
        })
      )
    })}
  </div>
We first get all the error field names. They are stored as keys in the responseJSON object.

Then we loop through them by calling Object.keys(formErrors).map.

Now, each key in the object has an array of error messages as its value i.e. each field that fails a validation is a key in the object.

And each of those keys has an array of error messages associated with each validation that it failed.

So we access the array of messages inside the formErrors object with the key formErrorField and then we'll loop through each message and return that to be displayed.

Now let's try the form again

When we submit it, this time we get a complete error message saying "title can't be blank".

So our error handling is working now.

But there's one thing we still need to take care of.

Now if we fill in the title and submit the form, we'll be able to create an appointment.

However, we'll still see this error message above the form.

That's because we didn't update the form errors in our app state after making a successful form submission.

So let's do that now.

In our AJAX call, inside the done callback, let's call a new function called resetFormErrors.

          .done((data) => {
            this.addNewAppointment(data);
            this.resetFormErrors();
          })

and let's define that as

  resetFormErrors () {
    this.setState({formErrors: {}})
  }

Now when we make a successful form submission, the error message disappears because the state got reset to contain no errors.

Now let’s add some more validation rules.

Let’s add the presence validation to appt_time field as well.

At the moment, we can submit the form without setting a value.

If we remove the initial state value for appt_time and submit the form with just a title, the appointment gets created with a blank value which shows as an Invalid Date.

So let’s add a validation for this field in the appointment model.

validates_presence_of :title, :appt_time

Now if we try to submit the form, we get an error.

appt_time can’t be blank.

If we submit the form with both fields blank, we’ll get two errors.

Let’s add some more rules.

Let’s say we want the length of the title to be at least 3 characters.

So we’ll add another validation:

validates :title, length: {minimum: 3}

And now if we try to create an appointment with 1 or 2 character title, we’ll get a length validation error.

Finally, let’s make sure that new appointments are not created for a date or time in the past. We’ll add a custom validation method for that.

validate :appt_time_cannot_be_in_the_past

And let’s define it as a private method:

  private
  def appt_time_cannot_be_in_the_past
    if appt_time.present? && appt_time < Time.now
      errors.add(:appt_time, "can't be in the past")
    end
  end

Ok, now let’s try to make an appointment on a past date and see what happens.

We’ll add a valid title with a past date. 
And when we try to create an appointment, it fails with the error:

appt_time can't be in the past

Now if we choose a future date and time, we can submit the form successfully and the error disappears.

So that's some basic form validation using the default Rails model errors.

In the next lesson, we’ll look at doing some client side validation with React.

Liked this tutorial? Get more like this in your inbox