Permalink
Browse files

Added support to :for option in inputs. This way we can simplify nest…

…ed attributes declarations.

Signed-off-by: Justin French <justin@indent.com.au>
  • Loading branch information...
1 parent 7ee99d9 commit a904eb97881a0606a07f7b79d55260b715c1f5ff @josevalim josevalim committed with justinfrench Mar 7, 2009
Showing with 104 additions and 7 deletions.
  1. +60 −2 lib/formtastic.rb
  2. +44 −5 spec/formtastic_spec.rb
View
@@ -177,18 +177,60 @@ def input(method, options = {})
# </ol>
# </fieldset>
# </form>
+ #
+ # === Nested attributes
+ #
+ # As in Rails, you can use semantic_fields_for to nest attributes:
+ #
+ # <% semantic_form_for @post do |form| %>
+ # <%= form.inputs :title, :body %>
+ #
+ # <% form.semantic_fields_for :author, @bob do |author_form| %>
+ # <% author_form.inputs do %>
+ # <%= author_form.input :first_name, :required => false %>
+ # <%= author_form.input :last_name %>
+ # <% end %>
+ # <% end %>
+ # <% end %>
+ #
+ # But this does not look formtastic! This is equivalent:
+ #
+ # <% semantic_form_for @post do |form| %>
+ # <%= form.inputs :title, :body %>
+ # <% form.inputs :for => [ :author, @bob ] do |author_form| %>
+ # <%= author_form.input :first_name, :required => false %>
+ # <%= author_form.input :last_name %>
+ # <% end %>
+ # <% end %>
+ #
+ # And if you don't need to give options to your input call, you could do it
+ # in just one line:
+ #
+ # <% semantic_form_for @post do |form| %>
+ # <%= form.inputs :title, :body %>
+ # <%= form.inputs :first_name, :last_name, :for => @bob %>
+ # <% end %>
+ #
+ # Just remember that calling inputs generates a new fieldset to wrap your
+ # inputs. If you have two separate models, but, semantically, on the page
+ # they are part of the same fieldset, you should use semantic_fields_for
+ # instead (just as you would do with Rails' form builder).
+ #
def inputs(*args, &block)
html_options = args.extract_options!
html_options[:class] ||= "inputs"
- if block_given?
+ if fields_for_object = html_options.delete(:for)
+ inputs_for_nested_attributes(fields_for_object, args << html_options, &block)
+ elsif block_given?
field_set_and_list_wrapping(html_options, &block)
else
if args.empty?
- args = @object.class.reflections.map { |n,_| n }
+ args = @object.class.reflections.map { |n,_| n }
args += @object.class.content_columns.map(&:name)
end
contents = args.map { |method| input(method.to_sym) }
+
field_set_and_list_wrapping(html_options, contents)
end
end
@@ -253,6 +295,22 @@ def semantic_fields_for(record_or_name_or_array, *args, &block)
protected
+ # Deals with :for option when it's supplied to inputs methods.
+ # It should raise an error if a block with arity zero is given.
+ #
+ def inputs_for_nested_attributes(fields_for_object, inputs_args, &block)
+ fields_for_block = if block_given?
+ raise ArgumentError, 'You gave :for option with a block to inputs method, ' <<
+ 'but the block does not accept any argument.' if block.arity <= 0
+
+ proc { |f| f.inputs(*inputs_args){ block.call(f) } }
+ else
+ proc { |f| f.inputs(*inputs_args) }
+ end
+
+ semantic_fields_for(*Array(fields_for_object), &fields_for_block)
+ end
+
# Ensure :object => @object is set before sending the options down to the Rails layer.
# Also remove any Formtastic-specific options
def set_options(options)
View
@@ -2180,20 +2180,50 @@ def custom(arg1, arg2, options = {})
end
end
end
+
it 'should render a fieldset inside the form, with a class of "inputs"' do
output_buffer.should have_tag("form fieldset.inputs")
end
+
it 'should render an ol inside the fieldset' do
output_buffer.should have_tag("form fieldset.inputs ol")
end
+
it 'should render the contents of the block inside the ol' do
output_buffer.should have_tag("form fieldset.inputs ol", /hello/)
end
+
it 'should not render a legend inside the fieldset' do
output_buffer.should_not have_tag("form fieldset.inputs legend")
end
end
+ describe 'when a :for option is provided' do
+ it 'should render nested inputs' do
+ @bob.stub!(:column_for_attribute).and_return(mock('column', :type => :string, :limit => 255))
+
+ semantic_form_for(@new_post) do |builder|
+ builder.inputs :for => [:author, @bob] do |bob_builder|
+ concat(bob_builder.input :login)
+ end
+ end
+
+ output_buffer.should have_tag("form fieldset.inputs #post_author_login")
+ output_buffer.should_not have_tag("form fieldset.inputs #author_login")
+ end
+
+ it 'should raise an error if :for and block with no argument is given' do
+ semantic_form_for(@new_post) do |builder|
+ proc {
+ builder.inputs(:for => [:author, @bob]) do
+ #
+ end
+ }.should raise_error(ArgumentError, 'You gave :for option with a block to inputs method, ' <<
+ 'but the block does not accept any argument.')
+ end
+ end
+ end
+
describe 'when a :name option is provided' do
before do
@legend_text = "Advanced options"
@@ -2203,6 +2233,7 @@ def custom(arg1, arg2, options = {})
end
end
end
+
it 'should render a fieldset inside the form' do
output_buffer.should have_tag("form fieldset legend", /#{@legend_text}/)
end
@@ -2218,6 +2249,7 @@ def custom(arg1, arg2, options = {})
end
end
end
+
it 'should pass the options into the fieldset tag as attributes' do
output_buffer.should have_tag("form fieldset##{@id_option}")
output_buffer.should have_tag("form fieldset.#{@class_option}")
@@ -2243,7 +2275,6 @@ def custom(arg1, arg2, options = {})
end
describe 'with no args' do
-
before do
semantic_form_for(@new_post) do |builder|
concat(builder.inputs)
@@ -2281,11 +2312,9 @@ def custom(arg1, arg2, options = {})
it 'should render a select list item for author_id' do
output_buffer.should have_tag('form > fieldset.inputs > ol > li.select')
end
-
end
describe 'with column names as args' do
-
before do
semantic_form_for(@new_post) do |builder|
concat(builder.inputs(:title, :body))
@@ -2297,11 +2326,22 @@ def custom(arg1, arg2, options = {})
output_buffer.should have_tag('form > fieldset.inputs > ol > li.string')
output_buffer.should have_tag('form > fieldset.inputs > ol > li.text')
end
+ end
+ describe 'when a :for option is provided' do
+ it 'should render nested inputs' do
+ @bob.stub!(:column_for_attribute).and_return(mock('column', :type => :string, :limit => 255))
+
+ semantic_form_for(@new_post) do |builder|
+ concat(builder.inputs :login, :for => @bob)
+ end
+
+ output_buffer.should have_tag("form fieldset.inputs #post_author_login")
+ output_buffer.should_not have_tag("form fieldset.inputs #author_login")
+ end
end
describe 'with column names and an options hash as args' do
-
before do
semantic_form_for(@new_post) do |builder|
concat(builder.inputs(:title, :body, :name => "Legendary Legend Text", :id => "my-id"))
@@ -2319,7 +2359,6 @@ def custom(arg1, arg2, options = {})
it 'should use the special :name option as a text for the legend tag' do
output_buffer.should have_tag('form > fieldset#my-id.inputs > legend', /Legendary Legend Text/)
end
-
end
end

0 comments on commit a904eb9

Please sign in to comment.