diff options
-rw-r--r-- | actionpack/CHANGELOG | 2 | ||||
-rw-r--r-- | actionpack/lib/action_view/helpers/form_helper.rb | 60 | ||||
-rw-r--r-- | actionpack/lib/action_view/helpers/prototype_helper.rb | 13 | ||||
-rw-r--r-- | actionpack/test/template/form_helper_test.rb | 75 |
4 files changed, 127 insertions, 23 deletions
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index f61908a820..27bbb0d180 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Add :builder => option to form_for and friends. [Nicholas Seckar, Rick Olson] + * Fix controller resolution to avoid accidentally inheriting a controller from a parent module. [Nicholas Seckar] * Set sweeper's @controller to nil after a request so that the controller may be collected between requests. [Nicholas Seckar] diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index c896ace862..75c09dfe87 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -97,12 +97,36 @@ module ActionView # # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base. # Like collection_select and datetime_select. + # + # You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers, + # then use your custom builder like so: + # + # <% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %> + # <%= f.text_field :first_name %> + # <%= f.text_field :last_name %> + # <%= text_area :person, :biography %> + # <%= check_box_tag "person[admin]", @person.company.admin? %> + # <% end %> + # + # In many cases you will want to wrap the above in another helper, such as: + # + # def labelled_form_for(name, object, options, &proc) + # form_for(name, object, options.merge(:builder => LabellingFormBuiler), &proc) + # end + # def form_for(object_name, object, options = {}, &proc) - url_for_options = options[:url] - additional_options = options.reject { |k, v| ![ :method, :multipart ].include?(k) } - concat(form_tag(url_for_options, additional_options), proc.binding) - fields_for(object_name, object, &proc) - concat(end_form_tag, proc.binding) + raise ArgumentError, "form_for requires a block!" unless proc + + url_options = options.delete :url + form_tag_selector = options.delete(:form_tag_selector) || :form_tag + form_options = {} + [:method, :multipart].each { |key| form_options[key] = options.delete(key) if options.key? key } + + fields_for(object_name, object, options.merge(:proc => proc)) do |builder| + concat send(form_tag_selector, url_options, form_options), proc.binding + builder.build_form(url_options, form_options) { proc.call builder } + concat end_form_tag, proc.binding + end end # Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes @@ -119,8 +143,11 @@ module ActionView # # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base. # Like collection_select and datetime_select. - def fields_for(object_name, object, &proc) - form_builder = FormBuilder.new(object_name, object, self, proc) + def fields_for(object_name, object, options = {}, &proc) + raise ArgumentError, "fields_for requires a block!" unless proc + + builder = options[:builder] || FormBuilder + form_builder = builder.new(object_name, object, self, options, options[:proc] || proc) proc.call(form_builder) end @@ -350,13 +377,20 @@ module ActionView end class FormBuilder - def initialize(object_name, object, template, proc) - @object_name, @object, @template, @proc = object_name, object, template, proc + # The methods which wrap a form helper call. + class_inheritable_accessor :field_helpers + self.field_helpers = (FormHelper.instance_methods - ['form_for']) + + def initialize(object_name, object, template, options, proc) + @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc end - - (FormHelper.instance_methods - [ :check_box, :radio_button ]).each do |selector| - next if selector == "form_for" - + + # An empty stub that can be overriden to wrap a form created by form_for. + def build_form(url_options, form_options, &proc) + proc.call self + end + + (field_helpers - %w(check_box radio_button)).each do |selector| src = <<-end_src def #{selector}(method, options = {}) @template.send(#{selector.inspect}, @object_name, method, options.merge(:object => @object)) diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index 27dc9c93cf..2cd7550c65 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -170,18 +170,11 @@ module ActionView tag("form", options[:html], true) end - def form_remote_for(object_name, object, options = {}, &proc) - concat(form_remote_tag(options), proc.binding) - fields_for(object_name, object, &proc) - concat(end_form_tag, proc.binding) - end - # Works like form_remote_tag, but uses form_for semantics. - def form_remote_for(object_name, object, options = {}, &proc) - concat(form_remote_tag(options), proc.binding) - fields_for(object_name, object, &proc) - concat(end_form_tag, proc.binding) + def remote_form_for(object_name, object, options = {}, &proc) + form_for(object_name, object, options.merge(:form_for_select => :form_remote_tag), &proc) end + alias_method :form_remote_for, :remote_form_for # Returns a button input tag that will submit form using XMLHttpRequest # in the background instead of regular reloading POST arrangement. diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index a38e3c6728..6cc4b50d67 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -234,6 +234,10 @@ class FormHelperTest < Test::Unit::TestCase assert_dom_equal expected, _erbout end + def test_form_builder_does_not_have_form_for_method + assert ! ActionView::Helpers::FormBuilder.instance_methods.include?('form_for') + end + def test_form_for_and_fields_for _erbout = '' @@ -256,4 +260,75 @@ class FormHelperTest < Test::Unit::TestCase assert_dom_equal expected, _erbout end + + class LabellingBulider < ActionView::Helpers::FormBuilder + (field_helpers - %w(hidden_field)).each do |selector| + src = <<-END_SRC + def #{selector}(field, *args, &proc) + "<label for='\#{field}'>\#{field.to_s.humanize}:</label> " + super + "<br/>" + end + END_SRC + class_eval src, __FILE__, __LINE__ + end + end + + def test_form_for_with_labelled_builder + _erbout = '' + + form_for(:post, @post, :builder => LabellingBulider) do |f| + _erbout.concat f.text_field(:title) + _erbout.concat f.text_area(:body) + _erbout.concat f.check_box(:secret) + end + + expected = + "<form action='http://www.example.com' method='post'>" + + "<label for='title'>Title:</label> <input name='post[title]' size='30' type='text' id='post_title' value='Hello World' /><br/>" + + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea><br/>" + + "<label for='secret'>Secret:</label> <input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + + "<input name='post[secret]' type='hidden' value='0' /><br/>" + + "</form>" + + assert_dom_equal expected, _erbout + end + + # Perhaps this test should be moved to prototype helper tests. + def test_remote_form_for_with_labelled_builder + self.extend ActionView::Helpers::PrototypeHelper + _erbout = '' + + remote_form_for(:post, @post, :builder => LabellingBulider) do |f| + _erbout.concat f.text_field(:title) + _erbout.concat f.text_area(:body) + _erbout.concat f.check_box(:secret) + end + + expected = + "<form action='http://www.example.com' method='post'>" + + "<label for='title'>Title:</label> <input name='post[title]' size='30' type='text' id='post_title' value='Hello World' /><br/>" + + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea><br/>" + + "<label for='secret'>Secret:</label> <input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + + "<input name='post[secret]' type='hidden' value='0' /><br/>" + + "</form>" + + assert_dom_equal expected, _erbout + end + + def test_fields_for_with_labelled_builder + _erbout = '' + + fields_for(:post, @post, :builder => LabellingBulider) do |f| + _erbout.concat f.text_field(:title) + _erbout.concat f.text_area(:body) + _erbout.concat f.check_box(:secret) + end + + expected = + "<label for='title'>Title:</label> <input name='post[title]' size='30' type='text' id='post_title' value='Hello World' /><br/>" + + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea><br/>" + + "<label for='secret'>Secret:</label> <input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + + "<input name='post[secret]' type='hidden' value='0' /><br/>" + + assert_dom_equal expected, _erbout + end end |