Self-marking required fields in Rails 3

Posted on May 1, 2011

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.