Archive: December 2008

How to write a custom form builder in Rails?

28 December, 2008 06:00

Most 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 :-)

Posted in: Software | 6 Comments » tags:

Don’t cry for me Argentina

4 December, 2008 08:49

I am back. I took almost one month of vacation to make my first steps to South America.

I was traveling around Argentina: learning tango in Buenos Aires, admiring glaciers in El Calafate, playing with whales and penguins in Puerto Madrid, watching the beauty of waterfalls and jungle in Iguazu and learning about history and culture heritage of Argentina in Salta.

Traveling is for me more about gathering new experiences rather than just pure relaxation. If a trip shook up my perception of the world and changed it a little then journey was a success.

My visit to Argentina was definitely successful. My perception of South America and Argentina in particular has changed.

I made friends with people from Chile, Peru and a few other countries, experienced the beauty of Argentina which I will try to share on the natrasie.pl.

I am sure I will come back to this part of the world. Don’t cry for me Argentina. I will be back!

Hasta la vista.

Posted in: Life | 1 Comment » tags: ,