class RPH::FormAssistant::FormBuilder

FormAssistant::FormBuilder

* provides several convenient helpers (see helpers.rb) and
  an infrastructure to easily add your own
* method_missing hook to wrap content "on the fly"
* optional: automatically attach labels to field helpers
* optional: format fields using partials (extremely extensible)

Usage:

<% form_for @project, :builder => RPH::FormAssistant::FormBuilder do |form| %>
  // fancy form stuff
<% end %>

- or -

<% form_assistant_for @project do |form| %>
  // fancy form stuff
<% end %>

- or -

# in config/intializers/form_assistant.rb
ActionView::Base.default_form_builder = RPH::FormAssistant::FormBuilder

Attributes

ignore_errors[RW]
ignore_labels[RW]
ignore_templates[RW]
template_root[RW]
fallback_template[RW]

used if no other template is available

Public Class Methods

assist(helper_name) click to toggle source
Calls superclass method
# File lib/form_assistant/form_builder.rb, line 204
def self.assist(helper_name)
  define_method(helper_name) do |field, *args|
    options          = (helper_name == 'check_box' ? args.shift : args.extract_options!) || {}
    label_options    = extract_options_for_label(field, options)
    template_options = extract_options_for_template(helper_name, options)
    extra_locals     = options.delete(:locals) || {}
    
    # build out the label element (if desired)
    label = label_options[:label] === false ? nil : self.label(field, label_options.delete(:text), label_options)

    # grab the tip, if any
    tip = options.delete(:tip)
    
    # is the field required?
    required = !!options.delete(:required)
    
    # ensure that we don't have any custom options pass through
    field_options = options.except(:label, :template, :tip, :required)
    
    # call the original render for the element
    super_args = helper_name == 'check_box' ? args.unshift(field_options) : args.push(field_options)
    element = super(field, *super_args)
    
    return element if template_options[:template] === false
    
    # return the helper with an optional label if templates are not to be used
    return render_element(element, field, helper_name, options, label_options[:label] === false) if self.class.ignore_templates
    
    # render the partial template from the desired template root
    render_partial_for(element, field, label, tip, template_options[:template], helper_name, required, extra_locals, args)
  end
end
view_path() click to toggle source
# File lib/form_assistant/form_builder.rb, line 60
def view_path
  if Rails.configuration.respond_to?(:view_path)
    return Rails.configuration.view_path
  else
    return Rails.configuration.paths['app/views'].first
  end
end

Public Instance Methods

column_type(field) click to toggle source
# File lib/form_assistant/form_builder.rb, line 313
def column_type(field)
  object.class.columns_hash[field.to_s].type rescue :string
end
fields_for_with_form_assistant(record_or_name_or_array, *args, &proc) click to toggle source

since fields_for() doesn't inherit the builder from form_for, we need to provide a means to set the builder automatically (works with nesting, too)

Usage: simply call fields_for() on the builder object

<% form_assistant_for @project do |form| %>
  <%= form.text_field :title %>
  <% form.fields_for :tasks do |task_fields| %>
    <%= task_fields.text_field :name %>
  <% end %>
<% end %>
# File lib/form_assistant/form_builder.rb, line 328
def fields_for_with_form_assistant(record_or_name_or_array, *args, &proc)
  options = args.extract_options!
  # hand control over to the original #fields_for()
  fields_for_without_form_assistant(record_or_name_or_array, *(args << options.merge!(:builder => self.class)), &proc)
end
input(field, *args) click to toggle source
# File lib/form_assistant/form_builder.rb, line 290
def input(field, *args)
  helper_name = case column_type(field)
    when :string
      field.to_s.include?('password') ? :password_field : :text_field
    when :text                      ; :text_area
    when :integer, :float, :decimal ; :text_field
    when :date                      ; :date_select
    when :datetime, :timestamp      ; :datetime_select
    when :time                      ; :time_select
    when :boolean                   ; :check_box
    else                            ; :text_field
  end
  
  send(helper_name, field, *args)
end
inputs(*args) click to toggle source
# File lib/form_assistant/form_builder.rb, line 306
def inputs(*args)
  options = args.extract_options!
  args.flatten.map do |field|
    input(field, options.dup)
  end.join('')
end
partial(name, options={}) click to toggle source

Renders a partial, passing the form object as a local variable named 'form' <%= form.partial 'shared/new', :locals => { :whatever => @whatever } %>

# File lib/form_assistant/form_builder.rb, line 284
def partial(name, options={})
  (options[:locals] ||= {}).update :form => self
  options.update :partial => name
  @template.render options
end
widget(*args, &block) click to toggle source
# File lib/form_assistant/form_builder.rb, line 257
def widget(*args, &block)
  options          = args.extract_options!
  fields           = args.shift || nil 
  field            = Array === fields ? fields.first : fields
  
  label_options    = extract_options_for_label(field, options)
  template_options = extract_options_for_template(self.fallback_template, options)
  label            = label_options[:label] === false ? nil : self.label(field, label_options.delete(:text), label_options)
  tip              = options.delete(:tip)
  locals           = options.delete(:locals)
  required         = !!options.delete(:required)

  if block_given?
    element = without_assistance do
      @template.capture(&block)
    end  
  else
    element = nil
  end    
  
  partial = render_partial_for(element, fields, label, tip, template_options[:template], 'widget', required, locals, args)
  RPH::FormAssistant::Rules.binding_required? ? @template.concat(partial, block.binding) : @template.concat(partial)
end
without_assistance(options={}) { || ... } click to toggle source
# File lib/form_assistant/form_builder.rb, line 243
def without_assistance(options={}, &block)
  # TODO - allow options to only turn off templates and/or labels
  ignore_labels, ignore_templates = self.class.ignore_labels, self.class.ignore_templates
 
  begin
    self.class.ignore_labels, self.class.ignore_templates = true, true
    result = yield
  ensure  
    self.class.ignore_labels, self.class.ignore_templates = ignore_labels, ignore_templates
  end  

  result
end

Protected Instance Methods

extract_options_for_label(field, options={}) click to toggle source
# File lib/form_assistant/form_builder.rb, line 151
def extract_options_for_label(field, options={})
  label_options = {}

  # consider the global setting for labels and
  # allow for turning labels off on a per-helper basis
  # <%= form.text_field :title, :label => false %>
  if self.class.ignore_labels || options[:label] === false || field.blank?
    label_options[:label] = false
  else  
    # ensure that the :label option is a Hash from this point on
    options[:label] ||= {}
  
    # allow for a cleaner way of setting label text
    # <%= form.text_field :whatever, :label => 'Whatever Title' %>
    label_options.merge!(options[:label].is_a?(String) ? {:text => options[:label]} : options[:label])

    # allow for a more convenient way to set common label options
    # <%= form.text_field :whatever, :label_id => 'dom_id' %>
    # <%= form.text_field :whatever, :label_class => 'required' %>
    # <%= form.text_field :whatever, :label_text => 'Whatever' %>
    %w(id class text).each do |option|
      label_option = "label_#{option}".to_sym
      label_options.merge!(option.to_sym => options.delete(label_option)) if options[label_option]
    end
  
    # Ensure we have default label text
    # (since Rails' label() does not currently respect I18n)
    label_options[:text] ||= object.class.human_attribute_name(field.to_s)
  end
    
  label_options
end
extract_options_for_template(helper_name, options={}) click to toggle source
# File lib/form_assistant/form_builder.rb, line 184
def extract_options_for_template(helper_name, options={})
  template_options = {}
  
  if options.has_key?(:template) && options[:template].kind_of?(FalseClass)
    template_options[:template] = false
  else  
    # grab the template
    template = options.delete(:template) || helper_name.to_s
    template = self.fallback_template unless template_exists?(template)
    template_options[:template] = template
  end
    
  template_options
end
render_element(element, field, name, options, ignore_label = false) click to toggle source

render the element with an optional label (does not use the templates)

# File lib/form_assistant/form_builder.rb, line 131
def render_element(element, field, name, options, ignore_label = false)
  return element if ignore_label
  
  # need to consider if the shortcut label option was used
  # i.e. <%= form.text_field :title, :label => 'Project Title' %>
  text, label_options = if options[:label].is_a?(String)
    [options.delete(:label), {}]
  else
    [options[:label].delete(:text), options.delete(:label)]
  end
  
  # consider trailing labels
  if %w(check_box radio_button).include?(name)
    label_options[:class] = (label_options[:class].to_s + ' inline').strip
    element + label(field, text, label_options)
  else
    label(field, text, label_options) + element
  end
end
render_partial_for(element, field, label, tip, template, helper, required, extra_locals, args) click to toggle source

renders the appropriate partial located in the template root

# File lib/form_assistant/form_builder.rb, line 123
def render_partial_for(element, field, label, tip, template, helper, required, extra_locals, args)
  errors = self.class.ignore_errors ? nil : error_message_for(field)
  locals = (extra_locals || {}).merge(:element => element, :field => field, :builder => self, :object => object, :object_name => object_name, :label => label, :errors => errors, :tip => tip, :helper => helper, :required => required)

  @template.render :partial => "#{self.class.template_root}/#{template}.html.erb", :locals => locals
end

Private Instance Methods

error_message_for(fields) click to toggle source

get the error messages (if any) for a field

# File lib/form_assistant/form_builder.rb, line 81
def error_message_for(fields)
  errors = []
  fields = [fields] unless Array === fields

  fields.each do |field|
    next unless has_errors?(field)
    
    errors += if RPH::FormAssistant::Rules.has_I18n_support?
      full_messages_for(field)
    else
      human_field_name = field.to_s.humanize
      errors += [*object.errors[field]].map do |error|
        "#{human_field_name} #{error}"
      end  
    end
  end

  errors.empty? ? nil : RPH::FormAssistant::FieldErrors.new(errors)
end
full_messages_for(field) click to toggle source

Returns full error messages for given field (uses I18n)

# File lib/form_assistant/form_builder.rb, line 102
def full_messages_for(field)
  attr_name = object.class.human_attribute_name(field.to_s)

  object.errors[field].inject([]) do |full_messages, message|
    next unless message
    full_messages << attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + message
  end
end
has_errors?(field) click to toggle source

returns true if a field is invalid

# File lib/form_assistant/form_builder.rb, line 112
def has_errors?(field)
  !(object.nil? || object.errors[field].blank?)
end
template_exists?(template) click to toggle source

checks to make sure the template exists

# File lib/form_assistant/form_builder.rb, line 117
def template_exists?(template)
  File.exists?(File.join(self.class.template_root(true), "_#{template}.html.erb"))
end