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] = true
in 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_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.