Thursday, October 04, 2012

Self-marking required fields in Rails

David Sulc wrote a nice article entitled, Self-marking required fields in Rails 3 that utilized a key element of Ryan Bates' Railscast on Validations in Rails 3.  In Ryan's original solution, he creates a new helper method named mark_required that uses the Rails validation reflection to determine if the model requires that the field to be present (via validates :presence => true or validates_presence_of).  If the field is required, this helper method adds a '*', otherwise it does nothing. Using this helper method in your code, your code would look like:
<%= f.label :name %><%= mark_required(@user, :name) %>
<%= f.text_field :name %>  

David improves upon this idea by enhancing the ActionView::Helpers::FormBuilder label helper instead of adding a new helper.  This eliminates the need to add the extra call to the mark_required helper for every label, which can be easily forgotten.  If the field is required, the enhanced label helper appends a "*" to the label text to indicate that the field is required.  So now your code is back to the normal form of:
<%= f.label :name %>
<%= f.text_field :name %>  

That's a nice improvement, but I thought it would be an even better design practice to simply give the label a required CSS class, and then allow the designer to be able to easily modify how required fields should be indicated.   My version of config/initializers/form_builder.rb looks like:

class ActionView::Helpers::FormBuilder
  alias :orig_label :label

  # add a 'required' CSS class to 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
      if options.class != Hash
        options = {:class => "required"}
      else
        options[:class] = ((options[:class] || "") + " required").split(" ").uniq.join(" ")
      end
    end

    self.orig_label(method, content, options || {}, &block)
  end
end
With this revised version in place, all labels of required fields are given a required CSS class.  If you want to indicate required fields with an asterisk after the label, you can do that by adding this CSS rule to your stylesheet,

/* add required field asterisk */
label.required:after {
    content: " *";
}
However, by extracting the method of indicating that the field is required from being hard-coded in the helper to instead using a CSS class, anything you can do with CSS is open to you and can be quickly and easily modified without updating the helper code.  You can change colors, fonts, borders, add images before or after, you could even use the dreaded text-decoration:blink.  The options are nearly endless.

Update for Rails 4! 


Thanks to the input from alex_m and Dan in the comments below, the follow revised version should work for Rails 4 (with ActiveAdmin):

class ActionView::Helpers::FormBuilder
  alias :orig_label :label

  # add a 'required' CSS class to 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.respond_to?(:validators_on) &&
      object.class.validators_on(method).map(&:class).include?(ActiveRecord::Validations::PresenceValidator)

      if options.class != Hash
        options = {:class => "required"}
      else
        options[:class] = ((options[:class] || "") + " required").split(" ").uniq.join(" ")
      end
    end

    self.orig_label(method, content, options || {}, &block)
  end
end

Thanks again alex_m and Dan!

6 comments:

alex m said...

Thanks for the post Steven.

In Rails 4 the validations class has been changed to ActiveRecord. So replacing ActiveModel::Validations::PresenceValidator with ActiveRecord::Validations::PresenceValidator should do the trick.

Dan said...

Thank you, this is fantastic. And thanks to @alex_m for the Rails 4 update.

Dan said...

There's a bug in interacting with ActiveAdmin and Ransack search, which I fix here: http://stackoverflow.com/a/24985923/1935918

Teller said...

"Update for Rails 4" 2.0
Code above doesn't work, if options[:class] have an array (e.g. ["label"]). So I make some modifications:

if options[:class].instance_of?(Array)
options[:class] = (options[:class].join(" ") + " required").split(" ").uniq.join(" ")
else
options[:class] = (options[:class] + " required").split(" ").uniq.join(" ")
end

Bruno said...

How do you write test for this ?

Bruno said...

How do you write a test for this ?