public
Description: Rails plugin: a custom, template-based form builder that attempts to make your forms friendly (and maybe even fun!).
Homepage: http://rpheath.com/posts/376-form-assistant-plugin-updates
Clone URL: git://github.com/rpheath/form_assistant.git
form_assistant / lib / form_assistant.rb
100644 315 lines (263 sloc) 13.447 kb
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
%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 <div class="fieldWithErrors">...</div>, 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