How to write a custom form builder in Rails?
28 December, 2008 06:00Most of the current web applications contain many different types of forms. Usually they look very similar across the whole application and they are a source of repetitive code in your presentation layer.
In this blog post I would like to share with you how to minimize this repetitive code and make your form nice, concise and dry by writing custom form builders.
Let’s say you are building a new web application with your close friend. He is a ninja of css, a web standards orthodox and requires that application generate forms that look like the piece of html below.
<form action="/users" class="new_user" id="new_user" method="post"> <fieldset> <legend>Sign up:</legend> <dl class="required correct"> <dt><label for="user_login">Login</label></dt> <dd><input id="user_login" name="user[login]" size="30" type="text" value="tomek" /></dd> <dd class="notice">Lowercase letters (a-z) & numbers only - no spaces.</dd> </dl> <dl class="required error"> <dt><label for="user_email">E-mail</label></dt> <dd class="error">can't be blank</dd> <dd><input id="user_email" name="user[email]" size="30" type="text" value="" /></dd> <dd class="notice">We won't get you spammed!</dd></dl> ... </fieldset> </form>
You are fully aware you cannot generate this kind of html with the standard helper methods provided by Rails.
On the one hand you don’t want to upset your friend and even start negotiations on how forms should be styled. On the other hand you would like to keep the code of your view concise and easy to maintain… let’s say more or less like the snippet below.
<% form_for @user do |f| -%> <fieldset> <legend>Sign up:</legend> <%= f.text_field :login, :notice => "Lowercase letters (a-z) & numbers only - no spaces." %> <%= f.text_field :email, :label => 'E-mail', :notice => "We wont get you spammed, we promise!" %> ... </fieldset> <% end -%>
Well… in tis situation… there is no other way. You have to write your own form builder!
What is a form builder?
ActionView::Helpers::FormBuilder is one of the “hidden hero” Rails classes. It is used on a frequent basis but not everybody realizes it even exists.
Whenever you use form_for helper in your view template, you pass an instance of FormBuilder class to the block associated with the method. Look at the snippet above. This is f variable.
So… if you want to create your own LabeledFormBuilder class with the enriched functionality all you need is to subclass ActionView::Helpers::FormBuilder and instruct Rails to use it. There are a few ways to do it.
One of the solutions is to create a new folder with Ruby file that will contain LabeledFormBuilder class and add it to load paths of Rails environment.
Let’s say you have just created a folder named builders with labeled_form_builder.rb file under app directory of your application. In order to tell Rails to load ruby code located in this place you have to add one line to your environment.rb file.
# app/config/envirnoment.rb
Rails::Initializer.run do |config|
...
config.load_paths += %W( #{RAILS_ROOT}/app/builders )
...
end
Next time you restart the server Ruby code placed in the labeled_form_builder.rb file will be loaded.
It is high time you rolled up your sleeves and wrote custom form builder!
Below I would like to show exemplary implementation that may help you to stay on speaking terms with your css ninja friend. I will also share a trick that I consider very useful and cool.
# app/builders/labeled_form_builder.rb
class LabeledFormBuilder < ActionView::Helpers::FormBuilder
%w[text_field password_field text_area].each do |method_name|
define_method(method_name) do |field_name, *args|
options = args.extract_options!
# Create field
field = @template.content_tag(:dt, label(field_name, options[:label]))
# Add validation errors
field += field_errors(field_name)
# Add field element
field += @template.content_tag(:dd, super)
# Add notice message
field += @template.content_tag(:dd, options[:notice], :class => 'notice') if options[:notice]
# Render field container with all elements
@template.content_tag(:dl, field, :class => field_style(field_name))
end
end
def check_box(field_name, *args)
options = args.extract_options!
@template.content_tag(:dl, super + label(field_name, options[:notice]), :class => "checkbox")
end
private
def is_required?(field_name)
object.class.reflect_on_validations_for(field_name).map(&:macro).include?(:validates_presence_of)
end
def has_errors?(field_name)
object.errors.invalid? field_name
end
def field_errors(field_name)
field_errors = ''
if has_errors?(field_name)
object.errors[field_name].each do |msg|
field_errors += @template.content_tag(:dd, msg, :class => 'error')
end
end
field_errors
end
def field_style(field_name)
field_style = ''
if is_required?(field_name)
field_style += 'required'
end
if has_errors?(field_name)
field_style += ' error'
elsif !object.errors.empty?
field_style += ' correct'
end
field_style
end
def objectify_options(options)
super.except(:notice, :label)
end
end
Now let me share a trick with you. Please look at is_required method. In the line…
# app/builders/labeled_form_builder.rb ... object.class.reflect_on_validations_for(field_name).map(&:macro).include?(:validates_presence_of) ...
… I use validation reflection plugin to find out if a given field is required or not.
I also use a piece of metaprogramming to provide own implementation of text_field, password_field, text_area helper methods.
The only thing that is missing to make this little example work is intruction to Rails to use LabeledFormBuilder instead of the default one. There are two ways you can do it.
First of all you can add builder attribute to form_for helper.
<% form_for @user, :builder => LabeledFormBuilder do |f| -%> <fieldset> <legend>Sign up:</legend> <%= f.text_field :login, :notice => "Lowercase letters (a-z) & numbers only - no spaces." %> <%= f.text_field :email, :label => 'E-mail', :notice => "We wont get you spammed, we promise!" %> ... </fieldset> <% end -%>
Secondly you can go to your environment config file and add one line to register your custom form builder as the default one.
# app/config/envirnoment.rb ActionView::Base.default_form_builder = LabeledFormBuilder
That’s all! Now you can send a message to your friend that his html code is safe and you can open bottle of wine



















