Daily Code Reading #28 – Formtastic input

Today I’m tackling formtastic‘s SemanticFormBuilder#input which is used by just about every other part of formtastic to generate the form fields.

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 input(method, options = {})
      if options.key?(:selected) || options.key?(:checked) || options.key?(:default)
        ::ActiveSupport::Deprecation.warn(
          "The :selected, :checked (and :default) options are deprecated in Formtastic and will be removed from 1.0. " <<
          "Please set default values in your models (using an after_initialize callback) or in your controller set-up. " <<
          "See http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html for more information.", caller)
      end
 
      options[:required] = method_required?(method) unless options.key?(:required)
      options[:as]     ||= default_input_type(method, options)
 
      html_class = [ options[:as], (options[:required] ? :required : :optional) ]
      html_class << 'error' if @object && @object.respond_to?(:errors) && !@object.errors[method.to_sym].blank?
 
      wrapper_html = options.delete(:wrapper_html) || {}
      wrapper_html[:id]  ||= generate_html_id(method)
      wrapper_html[:class] = (html_class << wrapper_html[:class]).flatten.compact.join(' ')
 
      if options[:input_html] && options[:input_html][:id]
        options[:label_html] ||= {}
        options[:label_html][:for] ||= options[:input_html][:id]
      end
 
      input_parts = @@inline_order.dup
      input_parts = input_parts - [:errors, :hints] if options[:as] == :hidden
 
      list_item_content = input_parts.map do |type|
        send(:"inline_#{type}_for", method, options)
      end.compact.join("\n")
 
      return template.content_tag(:li, Formtastic::Util.html_safe(list_item_content), wrapper_html)
    end
 
  end
end

Review

Required field

1
options[:required] = method_required?(method) unless options.key?(:required)

There are two ways to make a field required:

  1. Set options[:required] = true in the caller, which will bypass this line of code, or
  2. Based on the response from #method_required?. According to #method_required?‘s docs it uses the ValidationRefections plugin to automatically check the model for a validation or it checks the all_fields_required_by_default option.

Changing the field type

1
options[:as]     ||= default_input_type(method, options)

formtastic will try to guess which form field to use based on the content, this is what the #default_input_type method is doing here. Since it’s using the conditional assignment (||=), the caller can define options[:as] to override the default field.

CSS Classes

1
2
html_class = [ options[:as], (options[:required] ? :required : :optional) ]
html_class << 'error' if @object && @object.respond_to?(:errors) && !@object.errors[method.to_sym].blank?

The next section creates an array of css classes to use based on the:

  • field type
  • if the field is required or optional
  • if the object has errors

Wrapper HTML

1
2
3
wrapper_html = options.delete(:wrapper_html) || {}
wrapper_html[:id]  ||= generate_html_id(method)
wrapper_html[:class] = (html_class << wrapper_html[:class]).flatten.compact.join(' ')

Here #input is building up an options hash for the li that wraps the field. Pretty standard, nothing too fancy.

Input HTML id

1
2
3
4
if options[:input_html] && options[:input_html][:id]
  options[:label_html] ||= {}
  options[:label_html][:for] ||= options[:input_html][:id]
end

Since formtastic lets the caller override the id of the field, it has to be sure that the generated label references the field correctly. I personally have seen a lot of labels in Redmine not referencing the fields correctly, so this little bit of insurance is nice.

Input rendering order

1
2
input_parts = @@inline_order.dup
input_parts = input_parts - [:errors, :hints] if options[:as] == :hidden

formtastic lets the application define the order that the different parts of a form input are rendering in. These include:

  • the input itself
  • text hints about the input
  • errors

This section of code copies that this order so it can remove the errors and hints if the field is hidden. This prevents showing help text or the errors on a field that is hidden from the user.

List item content

1
2
3
list_item_content = input_parts.map do |type|
  send(:"inline_#{type}_for", method, options)
end.compact.join("\n")

Now that #input has setup all of the options it needs, it iterates over the input_parts and renders each part. For a non-hidden, default ordered field, this would get expanded out to:

1
2
3
4
5
6
parts = []
parts << inline_input_for(method, options)
parts << inline_hints_for(method, options)
parts << inline_errors_for(method, options)
 
list_item_content = parts.compact.join("\n")

I’ll be taking a closer look at #inline_inputs_for later this week but for now it’s enough to know that it generates the actual HTML input element.

List item wrapper

1
return template.content_tag(:li, Formtastic::Util.html_safe(list_item_content), wrapper_html)

Finally, #input wraps the list item content from above in a li. This li is then returned to the #inputs method from yesterday and gets wrapped inside of the form’s ul.

formtastic‘s #input method has shown how formtastic can give developers a bunch of styling and helpers from free. I also touched on how it uses #default_input_type to “guess” what field to render based on the :as symbol. Tomorrow I’ll take a look at what #default_input_type uses to guess the field type.