//
you're reading...
Form Builders

How to write a tabular form builder

I wanted to create a form using old good tables. Like this one:

This table consists of 3 columns: label, field and comment (optional). Some of the cells span for 2 columns. Also, the third row has several elements in it.

If I directly embed code into HTML, my view will result in the following mostrosity:

<table border="0" cellpadding="0" cellspacing="0">
  <%form_for :event, @event, :html =>{:method => 'post'}, :url => {:action => 'create'}, :width => '90%' do |f| %>
        <tbody><tr>
                  <td align="right" width="25%">
                         <%= f.label "<span class="asterick">*</span> Event Name: %>
                   </td>
                  <td align="left">
                         <%= f.text_field :name,  :style => "width: 200px"  %>
                   
                 </td>
                 <td class="comment" width="60%">Choose a meaningful name, e.g. Bob's Birthday Party, Susie's   Baby Shower
                </td>
         </tr>
         <tr>
                <td align="right" width="25%">
                      <%= f.label "<span class="asterick">*</span> Event Type:"
                 </td>
                  <td align="left">
                          <%= f.select :eventtype, {'Dinner'=>1, 'Breakfast'=>2, 'Lunch'=>3, 'Brunch'=>4}, :html_options => {:style => "width:150px"} %>
                  </td>
                  <td class="comment" width="60%">
                            Leave it blank if there is no meal
                 </td>
         </tr>
...

etc. And this is just a part of the form.

Ugly, isn’t it?

I would like to spell out my form in a simpler and DRYer way, like:


<% table_form_for :event, @event, :html =>{:method => 'post'}, :url => {:action => 'create'}, :width => '90%' do |f| %>
   <%= f.text_field :name, :required => true, :label => "Event Name", :comment => "Choose a meaningful name, e.g. Bob's Birthday Party, Susie's Baby Shower ", :style => "width: 200px"  %>
   <%= f.select :eventtype, {'Dinner'=>1, 'Breakfast'=>2, 'Lunch'=>3, 'Brunch'=>4},:label => "Event Type", :include_blank => true, :comment => 'Leave it blank if there is no meal', :html_options => {:style => "width:150px"} %>
   <%  f.row_of_elements :nocells => true, :colspan => '2' do |r| %>
      <%= r.check_box :picnic, :text => 'Picnic' %>
      <%= r.check_box :party, :text => 'Party of' %>
      <%= r.select :party_size, {"1-4" => [1,4], "5-10" => [5,9], "11-20" => [11, 20], ">20" => [21, 1000]}, :include_blank => true, :text => 'people', :html_options => {:style => "width:70px"} %>
   <% end %>
   <%= f.datetime_select :date, :required => true, :label => 'Date and Time', :colspan => "2", :html_options => {:style => "width: 70px"}%>
   <%= f.text_area :directions, :required => true, :label => "Address and Directions", :colspan => "2", :style => "width: 400px; height: 120px" %>
   <%= f.submit 'Submit', :style => 'width:200px' %>      
<% end %>

where each field takes “label” and “comment” as attributes, and has other attributes like “required” – if the field is compulsory and “text” – for the text to append after the field. Also, the “colspan” attribute takes care of the fields that span for more then 1 column. In addition, if my fields are in the same row, they are wrapped into the “row_of_elements” wrapper. Is it too much to ask for?

This problem can be solved if we make a custom form builder take care of the formatting. First, we create a basic form builder with methods to encapsulate elements into ‘tr’ and ‘td’ tags by subclassing FormBuilder

class TabularFormBuilder < ActionView::Helpers::FormBuilder

# the constructor
def initialize(object_name, object, template, options, proc)
  #initialization parameters
  @create_row = options[:create_row].nil? ? true : options.delete(:create_row)
  @create_cells = options[:create_cells].nil? ? true : options.delete(:create_cells)
  super(object_name, object, template, options, proc)
end

  
# empty row
def empty_row(options = {})
    @template.content_tag('tr', @template.content_tag('td', '&nbsp;', options))
end


# cells incapsulated into a tr tag
def table_row(contents, options = {})
  @template.content_tag('tr', options) do
      table_cells(contents)
  end
end

# a row of td cells
def table_cells(contents)
  cells = ""
  contents.each do |content, options|
    options = {} if options.nil?
     cells += @template.content_tag('td', options) do
       "#{content}"
    end
  end
  return cells
end

# a row for a block of elements
def create_row(builder, nocells = true, header = '&nbsp;', rowoptions = {}, celloptions = {}, headeroptions = {}, &block)
  raise ArgumentError, "Missing block" unless block_given?
  builder = ActionView::Base.default_form_builder if builder.nil?
  cells = ""
  # make a header cell
  unless header.nil?
    cells += @template.content_tag('td', header, headeroptions)
  end
  if nocells
    # make one cell for all the elements
    cells += @template.content_tag('td', celloptions) do
        yield builder.new(@object_name, @object, @template, @options.merge(:create_row => false, :create_cells => false), @proc)
    end
  else
    # let the elements handle their own cells
    cells += @template.capture do
       yield builder.new(@object_name, @object, @template, @options.merge(:create_row => false, :create_cells => true), @proc)
    end
  end 
  row = ""
  # wrap
  row = @template.content_tag('tr', rowoptions) do
      "#{cells}"
  end
  @template.concat(row)
end

end

Let’s look at it closer.

initialize (constructor)
We pass 2 options to the constructor. The option create_row is telling whether the form builder creates a row for every element (true) or arranges the elements into 1 row (false). The option create_cells is telling whether the form builder creates a separate cell for each element (true) or the elements are placed into the same cell (false).

table_cells encapsulates each element in an array into a ‘td’ tag. As a parameter it takes an array where each member is a pair of an element and the options to format the cell.

table_row calls table_cells to create ‘td’ cells for the elements and then encapsulates them into a ‘tr’ tag. As the parameters it takes an array where each member is a pair of an element and the options to format the cell, and options to format the row.

create_row processes the wrapper for a row of elements. It encapsulates the content into a ‘tr’ tag, and also creates a cell for the elements if they will be in the same cell. Then the builder passed to it takes care of each element. As the parameters it takes the class of the internal builder, a parameter whether there are no separate cells created for each element, the header of the row (first column), options for the row, options for the content cell and the header cell.

There also a method to create an empty row empty_row

In order to apply those methods to my form, I extended this form builder. Also, we’re missing the submit method, so I added it.

class MyTabularFormBuilder < TabularFormBuilder

def submit(title = 'Submit', options = {})
  button = super(title, options)
  table_row(['&nbsp;', [button, {:align => 'left'}]])
end

def get_required
    "<span class='asterick'>*</span>"
end

def row_of_elements(*args, &block)
  raise ArgumentError, "Missing block" unless block_given?

  extract the options and call create_row
    ...
end


def build_row(... )

  build the contents array and call table_row

    ... 
end

def build_cell(... )

  build the contents array and call table_cells 
     ...
end


def self.build_tabular_field(name)
  define_method(name) do |field, *args|
        options = args.extract_options!
        extract the options
           ...
  
        if @create_row
           call build_row 
        elsif @create_cells
           call build_cell 
        else
           the content is "content + &nbsp;&nbsp;&nbsp;" 
        end
    end
end

helpers = field_helpers +
     %w{date_select datetime_select time_select} +
     %w{collection_select select country_select time_zone_select} -
     %w{hidden_field label fields_for}

helpers.each do |name|
  self.build_tabular_field(name)
end


end

In addition, to wrap everything into a ‘table’ tag and tell my form to use the new form builder, I had to add this method to application_helper.rb

  def table_form_for(name, *args, &proc)
  options = args.extract_options!
  width = options.delete(:width)
  if width.nil?
    width = '100%'
  end
  args.push(options)
  concat("<table cellspacing='0', cellpadding ='0', border = '0' width = '" + width + "'>")
  form_for(name, *(args << {builder => MyTabularFormBuilder})), &proc)
  concat("</table>")
end

We are all set now. Use the method table_form_for instead of form_for to build very DRY tabular forms.

References:
Professional Ruby on Rails by Noel Rappin
Advanced Rails Recipes by Mike Clark

Advertisements

About RailsBlogger

I'm a Software Developer with over 10 years of experience, Java and Ruby on Rails.

Discussion

No comments yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: