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)
Pingback: 10 Validates Presence Blogs| villagegatenews.com
Nice patch, unfortunately it doesn’t take localization into account.
Adding
content ||= begin
worked for me…I18n.t("activerecord.attributes.#{object.class.name.to_s.downcase}.#{method}", :default=>"")
rescue
method.to_s.humanize
end
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.
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.
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
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)…
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 !).
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.
Good catch, Wolfgang ! I forgot to consider those cases…
Thanks so much for this. It’s is exactly what I need! :D
Glad I could help, Brian !
Great piece of code, thanks for posting this. Would be great to see this common functionality pulled into Rails core.
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”.
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 :)
Great addition, thanks !
Thanks to all! I’m new to Rails and you saved me very much time :)
Glad to hear it was helpful !
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)
Thanks for your contribution, Jordan !