Form validations with HTML5 and modern Rails
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:
- I wanted to use modern Rails: Webpacker, Turbolinks, remote forms and Stimulus.
- I wanted to leverage standard HTML form validations when they made sense.
- I wanted to support server-side validations.
- I wanted to show server-side and client-side validations consistently.
- I wanted to avoid ad-hoc client-side validation logic like the plague.
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:
- When using Turbolinks, you can use
redirect_to
to seamlessly respond to remote requests, butrender
is not really supported. You will get an HTML page as your Ajax response, and nothing will happen. - You can build responses to remote requests with Server-generated JavaScript Responses (SJR). In fact, doing this is the recommended approach for dealing with forms with errors.
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
- Html form validations on MDN. A good reference on both the form validation system and the constraints API.
- Good read on the forms validation API from css-tricks.com I borrowed a few ideas from this one.
- Rails remote elements