Self-marking required fields in Rails 3
In a Rails 3 project I’m currently working on, I wanted to mark required fields automatically: if the model required the field to be present (via
validates :presence => true
or
validates_presence_of
), that field in the form would automatically be marked as required. Since I couldn’t find any resources on doing this by augmenting FormBuilder
, I’ll guide you through it.
The idea
I avoid the NIH syndrome as much as I can, so this is heavily based on Railscast 211 (watch it, read it). As Ryan Bates explain in the screencast, we’re going to use Rails 3’s validation reflection to check whether the model requires a field, and if yes, we’ll mark it with a ‘*’.
Where this blog post differs from the screencast is that we’re going to add this functionality directly to the form builder. One of the advantages is that you can continue to use the same syntax for creating form labels (i.e.
f.label :attribute
) and it will automatically be marked as required if applicable.
The basics
Ruby classes can always be re-opened later and manipulated later, known as
monkey patching
(checkout the etymology, it’s pretty interesting). That’s exactly what we’re going to do here: the
FormBuilder
that shipped with Rails 3 doesn’t quite do what we want, so we’ll enhance it to automatically mark required form fields.
One of the ways to do this is to simply drop the Ruby file with our modifications somewhere in the
config/initialilzers
directory. I personally prefer to use a monkey_patches
directory for files that rewrite Rails functionality, but that’s not a requirement.
The code
Add the following code in a file at config/initializers/form_builder.rb
:
class ActionView::Helpers::FormBuilder
alias :orig_label :label
# add a '*' after the field label if the field is required
def label(method, content_or_options = nil, options = nil, &block)
if content_or_options && content_or_options.class == Hash
options = content_or_options
else
content = content_or_options
end
if object.class.validators_on(method).map(&:class).include? ActiveModel::Validations::PresenceValidator
required_mark = ''
else
required_mark = ' *'
end
content ||= method.to_s.humanize
content = content + required_mark
self.orig_label(method, content, options || {}, &block)
end
end
The explanation
On line 1, we reopen the
ActionView::Helpers::FormBuilder
class, because that’s where the label
helper is defined (per the api).
Then on line 2, we alias the original implementation of the label
helper as
:orig_label
, so we can pass on the arguments to the original method (on line 21) after we’ve marked the required field.
On line 5, we simply copy the method signature from the api and follow with determining what the arguments are on lines 6 through 10.
Line 12 is where the magic happens: we loop through all of the model’s validators for the given attribute and check whether at least one of them is a presence validator. If it is, we set the required mark.
On lines 18 and 19, we manually set the label text to the given label (or the attribute name) and tack on the required mark if necessary.
With line 21, we pass on our modified label text to the aliased
self.orig_label
method (i.e. the original) and let the Rails framework do the rest.
The beauty of it is that there is nothing else for you to do: the forms you’ve already written will automatically have their required fields marked, as will any forms that are created in the future (whether by hand or via a generator). All you need to do is have a presence validator to the model…
Internationaliztion
Update: I wanted to keep the example as simple as possible, and therefore ignored i18n issues. I’ve added this new section to address that shortcoming. As suggested by some readers, to get i18n working with this patch, replace line 18 above with
content ||= I18n.t(
"activerecord.attributes.#{object.class.name.underscore}.#{method}",
:default=>method.to_s.humanize
)
Would you like to see more Elixir content like this? Sign up to my mailing list so I can gauge how much interest there is in this type of content.