Validations

Our little application now already makes use of a bunch of things that you will regularly find in web applications.

We have a form that posts data to another route. The post route picks up the data, and stores it, then redirects to another route, which displays the data. We also use a session, and a query parameter to pass data from one route to another.

Pretty cool. You’ll see a lot of these very same concepts in use when you start building your first Rails application. All of these things will work pretty much the same way in Rails. Except, you’ve now built them manually yourself, so you know how this stuff works under the hood.

Let’s look at one other concept that Rails helps with, too: Validating user input.

If you look at our little application we still are a little naive in accepting whatever data comes in to our post route: We simply store whatever the user sends, whenever they send it.

Do we really want to store duplicate names? What if the same name is being submitted over and over again? And what if there’s no name submitted at all?

What we really want to do is validate the incoming data (in our case the name), and only accept and store it when we find it’s valid. If it’s not, then we want to display a message to the user, and ask them to submit the form again.

We could change our post route like so:

post "/monstas" do
  @name = params["name"]

  if @name.nil? or @name.empty?
    session[:message] = "You need to enter a name."
  elsif read_names.include?(@name)
    session[:message] = "#{@name} is already included in our list."
  else
    store_name("names.txt", @name)
    session[:message] = "Successfully stored the name #{@name}."
  end

  redirect "/monstas?name=#{@name}"
end

This is a valid implementation, and if you restart your application you can try it out.

The if statement first checks if the @name is empty. If it is we simply store a message to the session, and then redirect.

Note the duplicate condition @name.nil? or @name.empty?. The name parameter could either be missing (and thus, be nil), or it could be an empty string, so we need to check both cases.

This could be simplified to one condition like so:

  if @name.to_s.empty?

If it’s nil, then nil.to_s would return an empty string. If it’s an empty string, then "".to_s returns the same empty string again.

The if statement then also checks if the given @name already is included in the names in our file: the array returned by read_names. If we already have the name, then, again, we just add a message to the session, and redirect.

Only in the last case, when the name is not empty, and not already known, we do store it, and add the respective message to the session, and redirect.

Alright, this works.

However, it’s worth considering that this adds quite a bit of stuff to our route. And if we have a lot of routes then that’s a lot of code to add.

So what if we extract this to a separate class?

Extracting code to a class (or method) is a useful technique to keep your code clean and readable. The routes can focus on their job (passing data from the request to the view, reading data from our file, storing new data to the file etc), and the new class can focus on a different task: Finding out if the passed data is valid.

So let’s try that, and implement a little Ruby class that hides some of the logic from the route. Here’s how we could do it:

class NameValidator
  def initialize(name, names)
    @name = name.to_s
    @names = names
  end

  def valid?
    validate
    @message.nil?
  end

  def message
    @message
  end

  private

    def validate
      if @name.empty?
        @message = "You need to enter a name."
      elsif @names.include?(@name)
        @message = "#{@name} is already included in our list."
      end
    end
end

post "/monstas" do
  @name = params["name"]
  validator = NameValidator.new(@name, read_names)

  if validator.valid?
    store_name("names.txt", @name)
    session[:message] = "Successfully stored the name #{@name}."
  else
    session[:message] = validator.message
  end

  redirect "/monstas?name=#{@name}"
end

This adds quite a bit of code that we need to figure out, and type. But it seems worth it: Our route is now much shorter, and way easier to understand from just a quick look at it. We could move the NameValidator to a separate file.

Cool, this works great.

Rerendering the form

However, imagine we’d now have a much bigger form, with lots of fields. And we’d validate each of these fields and have several validation messages. Imagine we’d have like 20 form fields, and the user has made mistakes on 5 of them.

We’d want our application to display the same form again, and display the validation messages alongside with it. The original data should be, again, prefilled to the form, so the user does not have to type it all again.

In order to preserve the form data that had been entered by the user we’d need to append it all to the URL (as we do with the name in "/monstas?name=#{@name}"). And we’d need to store all the validation messages to the session.

This is a lot of stuff. And it actually also might break because URLs cannot be very long.

For this reason modern web applications usually follow a different pattern:

If the submitted data is invalid, instead of redirecting the user, we would simply re-render the same template right there, with the same data.

Here’s how we could do that:

post "/monstas" do
  @name = params["name"]
  @names = read_names
  validator = NameValidator.new(@name, @names)

  if validator.valid?
    store_name("names.txt", @name)
    session[:message] = "Successfully stored the name #{@name}."
    redirect "/monstas?name=#{@name}"
  else
    @message = validator.message
    erb :monstas
  end
end

As you can see we now only store a message to the session, and redirect, only if the given data is valid. If it’s not then we store the validation message to the @message instance variable and render our template again.

Cool. If you restart your application you can try how it works.

However, this now displays an empty “Hello” at the top. Why’s that?

We assign params["name"] to the instance variable @name which we then check in our template: <% if @name %>. However, since this has been submitted by a form, with an input element called name, what we get is an empty string, not nil.

We can fix this by changing our view like so:

<% unless @name.to_s.empty? %>
  <h1>Hello <%= @name %></h1>
<% end %>

Awesome.

With this completed you have now walked through an important pattern for web applications, which is also used in Rails applications by default:

Make sure to remember this pattern. Maybe write it down to a cheatsheet, formulate it in your own words. Maybe try turning it into a comic.

Next, let’s take this a step further and have a look at something that Rails calls “resources”.