aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/CHANGELOG2
-rw-r--r--actionpack/lib/action_view/helpers/form_helper.rb60
-rw-r--r--actionpack/lib/action_view/helpers/prototype_helper.rb13
-rw-r--r--actionpack/test/template/form_helper_test.rb75
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