Recently I have worked on form-validation systems in a couple of modern Rails apps, and I learned a few things along the way I would like to share.

My starting point was:

Example and source code

I prepared a demo project here. It’s a Rails 5.2 application with a scaffold generated for managing users. A user has 2 attributes: name and email. The validations are:

  • Both name and email are required: to exercise client-side validations.
  • The email has to be unique, to exercise server-side validations.

Client side validations with HTML 5

Form validations are captured in the html markup. You can read a nice reference here. For our purposes, we will modify the generated scaffold form to use them:

<%= form.text_field :name, required: true %>
...
<%= form.email_field :email, required: true %>

With this in place, the browser won’t allow submitting invalid data, and it will show an error message based on the kind of validation.

This works but you probably won’t love how it looks, and you can’t style it at all.

Invalid fields get an :invalid pseudo-class, which lets you target them via CSS, but it comes with a big caveat: fields get that class when the form loads for the first time. So, for example, you empty required attributes will appear marked as invalid when the page loads. Again, this feels weird. A more common pattern is highlighting invalid fields on form submission or after focusing on and leaving each field.

In my experience, these practical implementation issues are a common issue in many W3C specs (design-by-committee is a thing). Fortunately, in this case, you can circumvent these problems with Javascript and the constraint validation API, and still being able to enjoy the good parts.

You can check the stimulus controller doing this job. Some notes on how it works:

  • It disables the validation system with the attribute novalidate. This will still let us use the API, but it will stop showing native validation messages.
  • It validates each field on blur events, and whole forms when they are submitted.
  • It prevents invalid forms from being submitted. It works with both regular forms and rails remote forms.
  • It marks invalid fields with a class .invalid. I prefer to use a custom class instead of :invalid because, in addition to the problem mentioned above, this lets you generate invalid fields in the server, which will be handy as we will see later.
  • It shows native error messages with a custom field .error next to each input field.

The new form validations look like this:

The constraints API also supports custom validations, and we could validate email uniqueness with it, but I really dislike custom Javascript form validations. They require a considerable amount of work when you can get essentially the same with Rails, for free, and better. Also, you should have server-side validations in place anyway. So we will use Rails built-in support as explained in the next section.

Server-side validations with Rails

I love Rails validations. They are a powerful mechanism to capture your domain model validation rules, which are an essential component of any app I can think of. For our example, we can capture the model constraints with something like this:

class User < ApplicationRecord
  validates :name, :email, presence: true
  validates :email, uniqueness: true
end

A common Rails pattern for dealing with form errors is re-rendering the form with the invalid model carrying the errors to inform about them:

class UsersController < ApplicationController
  ...
  def update
    if @user.update(user_params)
      redirect_to @user, notice: 'User was successfully updated.'
    else
      render :edit
    end
  end
  ...
end

If you are going to use this approach there are two problems you need to solve:

  • Showing errors next to fields
  • Dealing with remote forms (default since Rails 5)

Showing errors next to fields in forms

For dealing with the first problem, you can customize ActionView::Base.field_error_proc which is a block of code that Rails uses to render fields with errors. By default, it will wrap them in a div.field_with_errors tag. You can configure it to render the same structure we are using for your client side validations. In our example:

  • Invalid fields are marked with a .invalid class
  • Information about the error is shown in a p.error element next to the invalid field.
# Place this code in a initializer. E.g: config/initializers/form_error.rb
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance_tag|
  fragment = Nokogiri::HTML.fragment(html_tag)
  field = fragment.at('input,select,textarea')

  model = instance_tag.object
  error_message = model.errors.full_messages.join(', ')

  html = if field
           field['class'] = "#{field['class']} invalid"
           html = <<-HTML
              #{fragment.to_s}
              <p class="error">#{error_message}</p>
           HTML
           html
         else
           html_tag
         end

  html.html_safe
end

Remote forms

Remote forms are forms that are submitted with an Ajax request. They are great because they are fast. It is the new default since Rails 5: form_with will generate remote forms unless told otherwise.

The problem is that support for dealing with remote forms in Rails is incomplete:

Building ad-hoc RJS responses for every form didn’t sound very appealing to me, so I published a gem called turbolinks_render. You can read more about the problem it solves here. With this gem in place you have the best of both worlds:

  • You can enjoy the speed of remote forms
  • You can enjoy the simplicity of just re-rendering your forms when an error happens.

With this in place, we will now have a form that validates duplicated emails consistently with our system for client-side validations. Notice you have to remove local: true from the generated scaffold form to make it remote.

What I love about this approach is that you don’t need to do any extra work to show model errors in your forms. Just add model validations, which you should be using nevertheless, use regular Rails form helpers and enjoy fast remote forms. To me, this approach is as productive as it gets.

Progressive enhancements

While this is more a nice side-effect that a deliberate design decision, this approach is based on progressive enhancements on top of web standards, and that is a good thing.

If no Javascript support, it would default to the standard browser support for form validations. Also, in this case, forms would be submitted as regular HTTP requests and render would work with those normally, rendering an HTTP response. Server-side validations will work as intended.

Also, when client-side validations are skipped for any reason, invalid form submissions will get a properly handled response showing the form fields marked with errors.

One can argue that HTML5 form validations will probably cover most of your forms, so you don’t need to handle error rendering in those controllers at all. I think the discussed system is much more robust and has no extra cost.

Conclusions

The discussed approach works great because, with a little bit of infrastructure in place, it lets you express your form validations very succinctly while being robust and comprehensive:

  • Use HTML5 form validations when they cover the validation you need
  • Use model validations in your server. This is something you want to do nevertheless.

In the past, I have experienced the pain of working with client-side validation libraries, custom Javascript validations and backend endpoints for dealing with server side validations. Compared to that, the presented approach is pure gold in terms of developer happiness.

References