Daily Code Reading #29 – Formtastic default_input_type

Today I’m looking into formtastic‘s SemanticFormBuilder#default_input_type. It’s used to guess what type of data is stored in a field so it can show the correct form element.

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
33
34
35
36
37
38
39
module Formtastic #:nodoc:
 
  class SemanticFormBuilder < ActionView::Helpers::FormBuilder
      def default_input_type(method, options = {}) #:nodoc:
        if column = self.column_for(method)
          # Special cases where the column type doesn't map to an input method.
          case column.type
          when :string
            return :password  if method.to_s =~ /password/
            return :country   if method.to_s =~ /country$/
            return :time_zone if method.to_s =~ /time_zone/
          when :integer
            return :select    if method.to_s =~ /_id$/
            return :numeric
          when :float, :decimal
            return :numeric
          when :timestamp
            return :datetime
          end
 
          # Try look for hints in options hash. Quite common senario: Enum keys stored as string in the database.
          return :select    if column.type == :string && options.key?(:collection)
          # Try 3: Assume the input name will be the same as the column type (e.g. string_input).
          return column.type
        else
          if @object
            return :select  if self.reflection_for(method)
 
            file = @object.send(method) if @object.respond_to?(method)
            return :file    if file && @@file_methods.any? { |m| file.respond_to?(m) }
          end
 
          return :select    if options.key?(:collection)
          return :password  if method.to_s =~ /password/
          return :string
        end
      end
  end
end

Review

#default_input_type has two main branches for it’s checks:

  1. Is the field a database column?
  2. Or is the field a virtual column (e.g. attr_accessor)

This logic for this is handled by ActiveRecord’s column_for method and it will either return an ActiveRecord column object or nil.

1
2
3
4
5
>> Issue.send(:column_for, :subject)
#
 
>> Issue.send(:column_for, :not_a_column)
nil

Database columns

1
2
3
4
5
6
7
8
9
10
11
12
13
case column.type
when :string
  return :password  if method.to_s =~ /password/
  return :country   if method.to_s =~ /country$/
  return :time_zone if method.to_s =~ /time_zone/
when :integer
  return :select    if method.to_s =~ /_id$/
  return :numeric
when :float, :decimal
  return :numeric
when :timestamp
  return :datetime
end

When the method matches a database column, #default_input_type will first use the column type and method name to try to match on a few special cases. For example a :string with ‘password’ in the method name would become a password field. Integer fields for foreign keys would become selects.

1
2
# Try look for hints in options hash. Quite common senario: Enum keys stored as string in the database.
return :select    if column.type == :string && options.key?(:collection)

Next #default_input_type checks for enumeration keys that are typically saved to the database as strings.

1
return column.type

If none of the previous checks matched, the method will just return the column type.

Virtual Columns

1
return :select  if self.reflection_for(method)

formtastic defines #reflection_for, which uses ActiveRecord’s reflect_on_association to see if a method is an association. If so, it returns a select field so you can populate the association directly from the form.

1
2
3
4
5
6
@@file_methods = [ :file?, :public_filename, :filename ]
 
# ...
 
file = @object.send(method) if @object.respond_to?(method)
return :file    if file && @@file_methods.any? { |m| file.respond_to?(m) }

Here formtastic is doing some checks to see if the method is one that is used to add a file upload. It calls the method to create a new instance of the object and then checks if it responds to any of the @@file_methods (file?, public_filename, filename by default).

1
2
return :select    if options.key?(:collection)
return :password  if method.to_s =~ /password/

If the field isn’t for an association or a file upload, formtastic then checks if it should display a select field for a collection or a password field.

1
return :string

If none of the checks have matched, then default_input_type will default to a string field.

Other than the reflections and file upload fields, default_input_type is just doing some pretty straight forward matching. It starts with the most specific matches and slowly broadens out until it returns either the database column type or a string.