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:
- Set
options[:required] = truein the caller, which will bypass this line of code, or - 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 theall_fields_required_by_defaultoption.
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.