Handling Validation Errors

Exercises

Objective

Now that we have validations in the Movie model, let's turn our attention to the user interface. We still need to:

  • Change the create action to redisplay the form when a user tries to create an invalid movie.

  • Display validation errors to help the user fix the problem.

  • Change the update action to handle validation errors when editing an existing movie.

1. Handle Errors During Create

The create action currently assumes that calling save always successfully creates the movie in the database:

def create
  @movie = Movie.new(movie_params)
  @movie.save
  redirect_to @movie
end

But that's wishful thinking! Now that we have validations, save could return either true or false. So we need to update the action to handle both cases.

  1. Change the create action so that if the save call returns true (the model was saved), the action redirects to the movie's detail page. If the movie fails to save, redisplay the new form populated with any valid data so the user can try again.

    If the save fails, you don't want to redirect to the new action because all the valid form data will be lost (the form will be empty). Instead, you simply want to render the same template that the new action renders. That way, all the valid form data entered by the customer will show up in the form.

    def create
      @movie = Movie.new(movie_params)
      if @movie.save
        redirect_to @movie
      else
        render :new
      end
    end
    
  2. Back in your browser, create a valid movie. The movie should get created and you should end up getting redirected to the new movie's detail page. That's the happy path!

  3. Now try creating an invalid movie by entering a title, but leaving the rest of the form blank. You should see the form redisplayed. The valid title you entered should be populated in the title field. The other (invalid) fields should be highlighted in red and yellow.

  4. How did those invalid fields get highlighted? Rails automatically wraps any form elements that contain invalid data in an HTML div with the class attribute set to field_with_errors. (Take a peek at the the page source. Rails won't mind.)

    And in the custom.scss file we have CSS rules that style field_with_errors accordingly. If you'd like to change the colors of invalid fields, just search for field_with_errors in the custom.scss file.

2. Display Validation Error Messages

At this point our controller is doing its job. Unfortunately, the user doesn't have a lot of clues as to why the movie wasn't created. To give them some actionable feedback, we need to display the actual error messages. Displaying information is a view's job, and in this case it makes sense to show the errors at the top of the form.

  1. In the _form partial just inside the form_with block, start by simply displaying the errors as an English sentence just like we did earlier in the console.

    Show Answer

    Hide Answer

    <%= form_with(model: movie, local: true) |f| %>
      <%= movie.errors.full_messages.to_sentence %>
      ...
    <% end %>
    
  2. Then back in the browser try creating an invalid movie again and you should see the error messages displayed at the top of the form. It's not very pretty output, but at least we're communicating better with the user.

  3. Instead of displaying the errors as a sentence, now we want to display the error messages in a neatly-formatted list with a bit of custom styling. We'll want to display errors this way on other forms we'll create later on in the course. Sounds like a great opportunity to practice what we learned earlier about partials!

    First, create an app/views/shared directory. Then inside that directory create an _errors.html.erb partial and paste in the following code:

    <% if object.errors.any? %>
      <section class="errors">
        <h2>
          Oops! Your form could not be saved.
        </h2>
        <h3>
          Please correct the following
          <%= pluralize(object.errors.size, "error") %>:
        </h3>
        <ul>
          <% object.errors.full_messages.each do |message| %>
            <li><%= message %></li>
          <% end %>
        </ul>
      </section>
    <% end %>
    

    Notice that it assumes that a local variable named object references an ActiveRecord model object. And if that object has any errors, it displays the number of errors and iterates through all the error messages to generated a styled list. Groovy!

  4. Then at the top of the _form.html.erb partial, render the _errors.html.erb partial (yup, partials can render other partials). When rendering the _errors.html.erb partial, you'll need to assign the movie object to the object variable.

    When calling a partial, we assign local variables using a hash. Each hash key will be the name of a local variable and the value will be that variable's value in the partial. So in this case the key needs to be object and the value needs to be the movie object. Since the _errors.html.erb partial lives in the app/views/shared directory, you'll need to refer to it as shared/errors.

    <%= form_with(model: movie, local: true) do |f| %>
      <%= render "shared/errors", object: movie %>
       ...
    <% end %>
    
  5. Then try creating an invalid movie again in the browser. This time you should get a snazzy list of validation errors displayed at the top of the form. Don't care for our taste in colors? No problem. Just search for errors in the custom.scss file and customize to your heart's content!

3. Handle Errors During Update

OK, so we have error handling in place when creating new movies, but what about editing existing movies? The update action currently assumes that calling update always successfully updates the movie attributes in the database:

def update
  @movie = Movie.find(params[:id])
  @movie.update(movie_params)
  redirect_to @movie
end

Similar to calling save, calling update returns either true or false depending on whether the object is valid. So we need to add a conditional here, too.

  1. Change the update action so that if the update call returns true (the model was updated), the action redirects to the movie's detail page. If the movie fails to update, redisplay the edit form populated with any valid data so the user can give it another go.

    Again, if the update fails you don't want to redirect to the edit action. Instead, you want to render the same template that the edit action renders.

    def update
      @movie = Movie.find(params[:id])
      if @movie.update(movie_params)
        redirect_to @movie
      else
        render :edit
      end
    end
    
  2. Then, back in your browser, edit a movie and enter valid information. You should get redirected to the movie's detail page and see the updated information. So far, so good!

  3. Then try updating a movie by entering a blank title and a negative total gross value, but leaving the other fields intact. You should see the form redisplayed with the list of validation errors at the top. (Remember, the edit template uses the same form partial as the new template.) In the form itself, the title and total gross fields should be highlighted in red, but all the other fields should have their original values.

Solution

The full solution for this exercise is in the handling-validation-errors directory of the code bundle.