Sonntag, 11. Januar 2015

Customize the Rails FormBuilder!

Tolerating as less as possible Ruby logic in the Ruby on Rails views is a basic pattern.
But instead of putting the logic into a Helpers method or Decorator class, moving it into a customized FormBuilder is worth a consideration.
Especially when it:
  1. is tied to a form
  2. is repeated
  3. generates tags (or even multi tag widgets)
Lots of Ruby on Rails views suffer from Ruby blocks with only logic inside (generating the over and over same HTML tag structure) or iterations like:
<% form_for @recipe do |f| %>
  <% (1..10).each do |rating| %>
    <%= f.radio_button :rating, rating %>
    <%= f.label "rating_#{rating}", rating %>
  <% end %>
<% end %>
What if the view could be refactored to:
<% form_for @recipe, builder: Forms::CollectionFormBuilder do |f| %>
  <%= f.labeled_radio_button_group 1..10, :rating %>
<% end %>
Please note the defined option builder:, pointing to the customized FormBuilder.
The better readability is obvious, aside from the less coding. It can be achieved by creating a new class (collection_form_builder.rb) in lib/forms:
module Forms
  class CollectionFormBuilder < ActionView::Helpers::FormBuilder
    def labeled_radio_button_group collection, method, options={}
      collection.inject(''.html_safe) { |html, value|
        checked = object.send(method).eql? value
        options[:id] = "#{method}_#{value}"
        html += @template.radio_button_tag("#{@object_name}[#{method}]", value, checked, options) + 
        @template.label_tag(options[:id], value)
      }   
    end 
  end 
end
by inheriting from ActionView::Helpers::FormBuilder. Thus some instance variables stated by FormBuilder#form_for are available:
  1. @object (the object assigned to form_for itself, like @recipe)
  2. @object_name (the objects name, like "recipe")
  3. @template (the current view (an instance of ActionView::Base); this object provides all methods available in your view)
  4. @options (the options assigned to form_for)
  5. @proc (the block assigned to form_for)
The @template object is coupled with concat (for output) and @proc (which provides the binding from your view). And it provides access to all standard ActionView::Base helper methods, including the custom helper methods (e.g. defined in ApplicationHelper). Furthermore partials can be rendered with @template:
@template.render partial: "fancy_widget", 
                 locals:  { object: @object }
Besides overwriting existing FormBuilder methods can make sense:
module Forms
  class LabeledFormBuilder < ActionView::Helpers::FormBuilder
    def check_box method, options={}, checked_value="1", unchecked_value="0"
      text = options.delete :text
      @template.content_tag(:label) {
        super(method, options, checked_value, unchecked_value) +
        (text or checked_value).to_s.html_safe
      }
    end
  end 
end
which generates the typical check box tag embraced by a label tag:
<% form_for @recipe, builder: Forms::LabeledFormBuilder do |f| %>
  <%= f.check_box :published, text: 'Published', id: nil %>
<% end %>
resulting in HTML:

Further articles of interest:

Supported by Ruby 2.1.1 and Ruby on Rails 4.1.8