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

required_mark = ''
required_mark = ' *'.html_safe if object.class.validators_on(method).map(&:class).include? ActiveModel::Validations::PresenceValidator

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_helper, so we can pass on the arguments to the original method (on line 18) 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 13 is where the magic happens: we loop through all of the model’s validators for the given attribute and see if one of them is a presence validator. If it is, we set the required mark.

On lines 15 and 16, we manually set the label text to the given label (or the attribute name) and tack on the required mark if necessary.

With line 18, we pass on our modified label text to the aliased 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 Mike, Nicholas, and Wolfgang suggest in the comments, to get i18n working with this patch, replace line 15 above with


content ||= I18n.t("activerecord.attributes.#{object.class.name.underscore}.#{method}", :default=>method.to_s.humanize)

This entry was posted in Rails. Bookmark the permalink.

19 Responses to Self-marking required fields in Rails 3

  1. Pingback: 10 Validates Presence Blogs| villagegatenews.com

  2. mike says:

    Nice patch, unfortunately it doesn’t take localization into account.
    Adding content ||= begin
    I18n.t("activerecord.attributes.#{object.class.name.to_s.downcase}.#{method}", :default=>"")
    rescue
    method.to_s.humanize
    end
    worked for me…

  3. david says:

    You’re absolutely right. I wanted to keep the example simple, but I should’ve mentioned i18n. I’ve updated the post with your suggestion.

  4. Thanks a lot for this. Was looking for a gem, and found your blog :)

    Imo, the code can be simplified.

    For example: why there is the need to use the begin…rescue while passing :default as a parameter?

    Also, we are already working with a specific method already, so instead of doing all this:

    required_mark = ''
    required_mark = ' *'.html_safe if object.class.validators_on(method).map(&:class).include? ActiveModel::Validations::PresenceValidator

    Could be done as this (no need for html_safe here also, as it won’t get escaped anyway):

    content << " *" unless object.class.validators_on(method).empty?

    That leave us with this implementation:


    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.is_a?(Hash)
    options = content_or_options
    else
    content = content_or_options
    end

    content ||= I18n.t("activerecord.attributes.#{object.class.name.to_s.downcase}.#{method}", :default => method.to_s.humanize)

    content << " *" unless object.class.validators_on(method).empty?

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

    end
    end

    Hope this helps.

  5. Capripot says:

    Hey,
    Thank you for your good post.
    Do you know if there is a solution to know if a field is in “validate_presence_of” in the model in Rails 2.3 ?
    Obviously I would like to replace this line :
    if object.class(method).map(&:class).include? ActiveModel::Validations::PresenceValidator
    by one works with Rails 2
    Regards

  6. david says:

    As far as I know, this won’t be possible in Rails 2.3 because it relies on validation reflection (which was introduced in Rails 3)…

  7. david says:

    Hi Nicholas,

    You’re right, we could add the ‘*’ on one line, but I thought doing it in several steps was easier to read (i.e. for someone trying to understand how this modification works). However, unless I’m mistaken, if you drop the “ActiveModel::Validations::PresenceValidator” portion, the * will be added for any validator (e.g. a length validator), which wasn’t the behavior I was looking for.

    I’ve updated the blog post with your input on the i18n portion. Thanks for commenting (and I apologize for taking so long to address this, but it completely slipped my mind !).

  8. Wolfgang says:

    I had to change

    content ||= I18n.t("activerecord.attributes.#{object.class.name.to_s.downcase}.#{method}", :default => method.to_s.humanize)

    into

    content ||= I18n.t("activerecord.attributes.#{object.class.name.underscore}.#{method}", :default => method.to_s.humanize)

    in case of tables with complex names like DistributionChannel to get distribution_channel as the correct attribute-model name for my yml file.

  9. david says:

    Good catch, Wolfgang ! I forgot to consider those cases…

  10. Brian Archer says:

    Thanks so much for this. It’s is exactly what I need! :D

  11. david says:

    Glad I could help, Brian !

  12. Chris Blunt says:

    Great piece of code, thanks for posting this. Would be great to see this common functionality pulled into Rails core.

  13. david says:

    I agree this bit of code can be quite useful. However, having it pulled into Rails might not be the best way to go, since it would (unnecessarily ?) bloat the code base.

    If I had to choose, I’d much rather the core team focus on easy extensibility rather than PHP-style “there’s a function for everything”.

  14. Joost says:

    Some added features are available at:
    http://joostonrails.blogspot.com/2012/04/markeren-van-optionele-verplichte.html
    Sorry for writing dutch overthere, read the source luke :)

  15. david says:

    Great addition, thanks !

  16. Luigi says:

    Thanks to all! I’m new to Rails and you saved me very much time :)

  17. david says:

    Glad to hear it was helpful !

  18. I found this code very helpful, David. Thanks for the post!

    I found the additions by Joost helpful as well with attribute conditions on my :presence validators. However, I found some problems with String or Symbol conditional arguments, so I made some checks against the argument types for those blocks:
    https://gist.github.com/9d6812f0c99a20cacb2c (working in Rails 3.1)

  19. david says:

    Thanks for your contribution, Jordan !