%w(error rules builder collector helpers field_errors).each do |f| require File.join(File.dirname(__FILE__), 'form_assistant', f) end # Developed by Ryan Heath (http://rpheath.com) module RPH # The idea is to make forms extremely less painful and a lot more DRY module FormAssistant # 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 class FormBuilder < ActionView::Helpers::FormBuilder include RPH::FormAssistant::Helpers cattr_accessor :ignore_templates cattr_accessor :ignore_labels cattr_accessor :ignore_errors cattr_accessor :template_root # used if no other template is available attr_accessor_with_default :fallback_template, 'field' # if set to true, none of the templates will be used; # however, labels can still be automatically attached # and all FormAssistant helpers are still avaialable self.ignore_templates = false # if set to true, labels will become nil everywhere (both # with and without templates) self.ignore_labels = false # set to true if you'd rather use #error_messages_for() self.ignore_errors = false # sets the root directory where templates will be searched # note: the template root should be nested within the # configured view path (which defaults to app/views) self.template_root = File.join(Rails.configuration.view_path, 'forms') # override the field_error_proc so that it no longer wraps the field # with
...
, but just returns the field ActionView::Base.field_error_proc = Proc.new { |html_tag, instance| html_tag } private # render(:partial => '...') doesn't want the full path of the template def self.template_root(full_path = false) full_path ? @@template_root : @@template_root.gsub(Rails.configuration.view_path + '/', '') end # get the error messages (if any) for a field def error_message_for(field) return nil unless has_errors?(field) errors = if RPH::FormAssistant::Rules.has_I18n_support? full_messages_for(field) else errors = object.errors[field] [[field.to_s.humanize, (errors.is_a?(Array) ? errors.to_sentence : errors).to_s].join(' ')] end RPH::FormAssistant::FieldErrors.new(errors) end # Returns full error messages for given field (uses I18n) 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 # returns true if a field is invalid def has_errors?(field) !(object.nil? || object.errors[field].blank?) end # checks to make sure the template exists def template_exists?(template) File.exists?(File.join(self.class.template_root(true), "_#{template}.html.erb")) end protected # renders the appropriate partial located in the template root 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 # render the element with an optional label (does not use the templates) 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 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 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 public 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 self.class.ignore_labels, self.class.ignore_templates = true, true result = yield self.class.ignore_labels, self.class.ignore_templates = ignore_labels, ignore_templates result end def widget(*args, &block) options = args.extract_options! field = args.shift || nil 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) required = !!options.delete(:required) element = without_assistance do @template.capture(&block) end partial = render_partial_for(element, field, label, tip, template_options[:template], 'widget', required, {}, args) RPH::FormAssistant::Rules.binding_required? ? @template.concat(partial, block.binding) : @template.concat(partial) end # redefining all traditional form helpers so that they # behave the way FormAssistant thinks they should behave send(:form_helpers).each do |helper_name| define_method(helper_name) do |field, *args| options = 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 element = super(field, *(args << field_options)) 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 # Renders a partial, passing the form object as a local # variable named 'form' # <%= form.partial 'shared/new', :locals => { :whatever => @whatever } %> def partial(name, options={}) (options[:locals] ||= {}).update :form => self options.update :partial => name @template.render options end # 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 %> 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 # used to intercept #fields_for() and set the builder alias_method_chain :fields_for, :form_assistant end # methods that mix into ActionView::Base module ActionView private # used to ensure that the desired builder gets set before calling #form_for() def form_for_with_builder(record_or_name_or_array, builder, *args, &proc) options = args.extract_options! # hand control over to the original #form_for() form_for(record_or_name_or_array, *(args << options.merge!(:builder => builder)), &proc) end # determines if binding is needed for #concat() # (Rails 2.2.0 and greater no longer requires the binding) def binding_required RPH::FormAssistant::Rules.binding_required? end public # easy way to make use of FormAssistant::FormBuilder # # <% form_assistant_for @project do |form| %> # // fancy form stuff # <% end %> def form_assistant_for(record_or_name_or_array, *args, &proc) form_for_with_builder(record_or_name_or_array, RPH::FormAssistant::FormBuilder, *args, &proc) end # (borrowed the #fieldset() helper from Chris Scharf: # http://github.com/scharfie/slate/tree/master/app/helpers/application_helper.rb) # # <% fieldset 'User Registration' do %> # // fields # <% end %> def fieldset(legend, &block) locals = { :legend => legend, :fields => capture(&block) } partial = render(:partial => "#{RPH::FormAssistant::FormBuilder.template_root}/fieldset.html.erb", :locals => locals) # render the fields binding_required ? concat(partial, block.binding) : concat(partial) end end end end