Daily Code Reading #27 – Formtastic inputs

Yesterday I read through formtastic‘s SemanticFormBuilder#semantic_form_for. Today I’m reading through SemanticFormBuilder#inputs which formtastic uses to create a fieldset and automatically generate all of the form fields needed.

The Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
module Formtastic #:nodoc:
 
  class SemanticFormBuilder < ActionView::Helpers::FormBuilder
 
    def inputs(*args, &block)
      title = field_set_title_from_args(*args)
      html_options = args.extract_options!
      html_options[:class] ||= "inputs"
      html_options[:name] = title
 
      if html_options[:for] # Nested form
        inputs_for_nested_attributes(*(args << html_options), &block)
      elsif block_given?
        field_set_and_list_wrapping(*(args << html_options), &block)
      else
        if @object && args.empty?
          args  = self.association_columns(:belongs_to)
          args += self.content_columns
          args -= RESERVED_COLUMNS
          args.compact!
        end
        legend = args.shift if args.first.is_a?(::String)
        contents = args.collect { |method| input(method.to_sym) }
        args.unshift(legend) if legend.present?
 
        field_set_and_list_wrapping(*((args << html_options) << contents))
      end
    end
    alias :input_field_set :inputs
 
  end
end

Review

The first part of #inputs just sets up options like the title and css classes. The if statement is where formtastic does the form generation:

  1. When the :for option is passed, #inputs will build fields for nested attributes using #inputs_for_nested_attributes.
  2. When a block is passed (like with explicit input calls) then #field_set_and_list_wrapping is called which generates the block’s HTML and wraps it in a fieldset.
  3. Otherwise, #inputs will automatically generate form fields for the object based on the model.

Automatic form fields generation

I wanted to take a closer look at how formtastic automatically generates the form fields because this is what attracted me to formtastic in the first place.

1
2
3
4
5
6
7
8
9
10
11
if @object && args.empty?
  args  = self.association_columns(:belongs_to)
  args += self.content_columns
  args -= RESERVED_COLUMNS
  args.compact!
end
legend = args.shift if args.first.is_a?(::String)
contents = args.collect { |method| input(method.to_sym) }
args.unshift(legend) if legend.present?
 
field_set_and_list_wrapping(*((args << html_options) << contents))

When there is an @object but no args, formtastic builds a list of the @object‘s columns and of the associated belongs_to models. Then reserved columns like “created_at”, “updated_at”, and “lock_version” are removed from the list so fields are not created for them (they shouldn’t be editable).

1
legend = args.shift if args.first.is_a?(::String)

Next the legend is generated from the first arg. This confused me at first because the code above overrode the args with an array of the column names and I thought legend would pick up the first column name to use that as the legend. I was wrong though, the column names come across as Symbols so the guard on that line wouldn’t run. For example: args are [:title, :description, :name] has no strings.

Next, #inputs runs #input on each column to create the field. This, combined with the column finder above, is how formtastic can automatically find all of the columns from the model that need fields built. So it looks like the #input method will be the next code I read to figure out how the fields are actually created.