diff --git a/README.textile b/README.textile index 4192a72a6..0a728fa18 100644 --- a/README.textile +++ b/README.textile @@ -148,7 +148,7 @@ If you want to customize the label text, or render some hint text below the fiel
   <% semantic_form_for @post do |form| %>
-    <% form.inputs :name => "Basic", :id => "basic" do %>
+    <% form.inputs "Basic", :id => "basic" do %>
       <%= form.input :title %>
       <%= form.input :body %>
     <% end %>
@@ -324,11 +324,11 @@ Formtastic supports localized *labels*, *hints*, *legends*, *actions* using the
 
 *4. Localized titles (a.k.a. legends):*
 
-_Note: Slightly different because Formtastic can't guess how you group fields in a form._
+_Note: Slightly different because Formtastic can't guess how you group fields in a form. Legend text can be set with first (as in the sample below) specified value, or :name/:title options - depending on what flavor is preferred._
 
 
   <% semantic_form_for @post do |form| %>
-    <% form.inputs :title => :post_details do %>   # => :title => "Post details"
+    <% form.inputs :post_details do %>   # => :title => "Post details"
       # ...
     <% end %>
     # ...
diff --git a/lib/formtastic.rb b/lib/formtastic.rb
index 6cde3cc13..e499df404 100644
--- a/lib/formtastic.rb
+++ b/lib/formtastic.rb
@@ -22,6 +22,8 @@ class SemanticFormBuilder < ActionView::Helpers::FormBuilder
                    :required_string, :optional_string, :inline_errors, :label_str_method, :collection_label_methods,
                    :inline_order, :file_methods, :priority_countries, :i18n_lookups_by_default, :default_commit_button_accesskey 
 
+    RESERVED_COLUMNS = [:created_at, :updated_at, :created_on, :updated_on, :lock_version, :version]
+    
     I18N_SCOPES = [ '{{model}}.{{action}}.{{attribute}}',
                     '{{model}}.{{attribute}}',
                     '{{attribute}}']
@@ -136,11 +138,16 @@ def input(method, options = {})
     #     <%= form.inputs %>
     #   <% end %>
     #
+    #   With a few arguments:
+    #   <% semantic_form_for @post do |form| %>
+    #     <%= form.inputs "Post details", :title, :body %>
+    #   <% end %>
+    #
     # === Options
     #
-    # All options (with the exception of :name) are passed down to the fieldset as HTML
-    # attributes (id, class, style, etc).  If provided, the :name option is passed into a
-    # legend tag inside the fieldset (otherwise a legend is not generated).
+    # All options (with the exception of :name/:title) are passed down to the fieldset as HTML
+    # attributes (id, class, style, etc).  If provided, the :name/:title option is passed into a
+    # legend tag inside the fieldset.
     #
     #   # With a block:
     #   <% semantic_form_for @post do |form| %>
@@ -154,6 +161,11 @@ def input(method, options = {})
     #     <%= form.inputs :title, :body, :name => "Create a new post", :style => "border:1px;" %>
     #   <% end %>
     #
+    #   # ...or the equivalent:
+    #   <% semantic_form_for @post do |form| %>
+    #     <%= form.inputs "Create a new post", :title, :body, :style => "border:1px;" %>
+    #   <% end %>
+    #
     # === It's basically a fieldset!
     #
     # Instead of hard-coding fieldsets & legends into your form to logically group related fields,
@@ -168,6 +180,9 @@ def input(method, options = {})
     #       <%= f.input :created_at %>
     #       <%= f.input :user_id, :label => "Author" %>
     #     <% end %>
+    #     <% f.inputs "Extra" do %>
+    #       <%= f.input :update_at %>
+    #     <% end %>
     #   <% end %>
     #
     #   # Output:
@@ -185,6 +200,12 @@ def input(method, options = {})
     #         
  • ...
  • # # + #
    + # Extra + #
      + #
    1. ...
    2. + #
    + #
    # # # === Nested attributes @@ -232,17 +253,19 @@ def inputs(*args, &block) if html_options[:for] inputs_for_nested_attributes(args, html_options, &block) elsif block_given? - field_set_and_list_wrapping(html_options, &block) + field_set_and_list_wrapping(*(args << html_options), &block) else if @object && args.empty? - args = @object.class.reflections.map { |n,_| n if _.macro == :belongs_to } - args += @object.class.content_columns.map(&:name) - args -= %w[created_at updated_at created_on updated_on lock_version version] + args = self.association_columns(:belongs_to) + args += self.content_columns + args -= RESERVED_COLUMNS args.compact! end - contents = args.map { |method| input(method.to_sym) } - - field_set_and_list_wrapping(html_options, contents) + legend = args.shift if args.first.is_a?(::String) + contents = args.collect { |method| input(method.to_sym) } + args.unshift(legend) if legend.present? + + field_set_and_list_wrapping(*(args << html_options), contents) end end alias :input_field_set :inputs @@ -396,6 +419,30 @@ def inline_errors_for(method, options=nil) #:nodoc: protected + # Collects content columns (non-relation columns) for the current form object class. + # + def content_columns + if @object.present? + @object.class.name.constantize.content_columns.collect { |c| c.name.to_sym }.compact + else + @object_name.to_s.classify.constantize.content_columns.collect { |c| c.name.to_sym }.compact rescue [] + end + end + + def association_columns(*by_associations) + if @object.present? + @object.class.reflections.collect do |name, _| + if by_associations.present? + name if by_associations.include?(_.macro) + else + name + end + end.compact + else + [] + end + end + # Prepare options to be sent to label # def options_for_label(options) @@ -1050,12 +1097,29 @@ def required_or_optional_string(required) #:nodoc: # # f.inputs :name => 'Task #%i', :for => :tasks # + # or the shorter equivalent: + # + # f.inputs 'Task #%i', :for => :tasks + # # And it will generate a fieldset for each task with legend 'Task #1', 'Task #2', # 'Task #3' and so on. # - def field_set_and_list_wrapping(html_options, contents='', &block) #:nodoc: + # Note: Special case for the inline inputs (non-block): + # f.inputs "My little legend", :title, :body, :author # Explicit legend string => "My little legend" + # f.inputs :my_little_legend, :title, :body, :author # Localized (118n) legend with I18n key => I18n.t(:my_little_legend, ...) + # f.inputs :title, :body, :author # First argument is a column => (no legend) + # + def field_set_and_list_wrapping(*args, &block) #:nodoc: + contents = args.last.is_a?(::Hash) ? '' : args.pop.flatten + html_options = args.extract_options! + html_options[:name] ||= html_options.delete(:title) - html_options[:name] = localized_string(html_options[:name], html_options[:name], :title) if html_options[:name].is_a?(Symbol) + + valid_name_classes = [::String, ::Symbol] + valid_name_classes.delete(::Symbol) if !block_given? && (args.first.is_a?(::Symbol) && self.content_columns.include?(args.first)) + + html_options[:name] ||= args.shift if valid_name_classes.any? { |valid_name_class| args.first.is_a?(valid_name_class) } + html_options[:name] = localized_string(html_options[:name], html_options[:name], :title) if html_options[:name].is_a?(::Symbol) legend = html_options.delete(:name).to_s legend %= parent_child_index(html_options[:parent]) if html_options[:parent] diff --git a/spec/inputs_spec.rb b/spec/inputs_spec.rb index 9c66d0038..ddd411d93 100644 --- a/spec/inputs_spec.rb +++ b/spec/inputs_spec.rb @@ -174,9 +174,12 @@ describe 'and is a string' do before do @legend_text = "Advanced options" - @legend_text_using_title = "Advanced options 2" + @legend_text_using_name = "Advanced options 2" + @legend_text_using_title = "Advanced options 3" semantic_form_for(@new_post) do |builder| - builder.inputs :name => @legend_text do + builder.inputs @legend_text do + end + builder.inputs :name => @legend_text_using_name do end builder.inputs :title => @legend_text_using_title do end @@ -184,34 +187,40 @@ end it 'should render a fieldset with a legend inside the form' do - output_buffer.should have_tag("form fieldset legend", /#{@legend_text}/) - output_buffer.should have_tag("form fieldset legend", /#{@legend_text_using_title}/) + output_buffer.should have_tag("form fieldset legend", /^#{@legend_text}$/) + output_buffer.should have_tag("form fieldset legend", /^#{@legend_text_using_name}$/) + output_buffer.should have_tag("form fieldset legend", /^#{@legend_text_using_title}$/) end end describe 'and is a symbol' do before do @localized_legend_text = "Localized advanced options" - @localized_legend_text_using_title = "Localized advanced options 2" + @localized_legend_text_using_name = "Localized advanced options 2" + @localized_legend_text_using_title = "Localized advanced options 3" ::I18n.backend.store_translations :en, :formtastic => { :titles => { :post => { :advanced_options => @localized_legend_text, - :advanced_options_2 => @localized_legend_text_using_title + :advanced_options_using_name => @localized_legend_text_using_name, + :advanced_options_using_title => @localized_legend_text_using_title } } } semantic_form_for(@new_post) do |builder| - builder.inputs :name => :advanced_options do + builder.inputs :advanced_options do + end + builder.inputs :name => :advanced_options_using_name do end - builder.inputs :title => :advanced_options_2 do + builder.inputs :title => :advanced_options_using_title do end end end it 'should render a fieldset with a localized legend inside the form' do - output_buffer.should have_tag("form fieldset legend", /#{@localized_legend_text}/) - output_buffer.should have_tag("form fieldset legend", /#{@localized_legend_text_using_title}/) + output_buffer.should have_tag("form fieldset legend", /^#{@localized_legend_text}$/) + output_buffer.should have_tag("form fieldset legend", /^#{@localized_legend_text_using_name}$/) + output_buffer.should have_tag("form fieldset legend", /^#{@localized_legend_text_using_title}$/) end end end @@ -238,9 +247,8 @@ describe 'without a block' do before do - ::Post.stub!(:reflections).and_return({:author => mock('reflection', :options => {}, :macro => :belongs_to), + ::Post.stub!(:reflections).and_return({:author => mock('reflection', :options => {}, :macro => :belongs_to), :comments => mock('reflection', :options => {}, :macro => :has_many) }) - ::Post.stub!(:content_columns).and_return([mock('column', :name => 'title'), mock('column', :name => 'body'), mock('column', :name => 'created_at')]) ::Author.stub!(:find).and_return([@fred, @bob]) @new_post.stub!(:title) @@ -352,12 +360,15 @@ 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")) + @legend_text_using_option = "Legendary Legend Text" + @legend_text_using_arg = "Legendary Legend Text 2" + concat(builder.inputs(:title, :body, :name => @legend_text_using_option, :id => "my-id")) + concat(builder.inputs(@legend_text_using_arg, :title, :body, :id => "my-id-2")) end end it 'should render a form with a fieldset containing two list items' do - output_buffer.should have_tag('form > fieldset.inputs > ol > li', :count => 2) + output_buffer.should have_tag('form > fieldset.inputs > ol > li', :count => 4) end it 'should pass the options down to the fieldset' do @@ -365,7 +376,8 @@ end 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/) + output_buffer.should have_tag('form > fieldset#my-id.inputs > legend', /^#{@legend_text_using_option}$/) + output_buffer.should have_tag('form > fieldset#my-id-2.inputs > legend', /^#{@legend_text_using_arg}$/) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b235a7f51..b8beb0aa3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -116,6 +116,7 @@ def new_author_path; "/authors/new"; end ::Author.stub!(:human_name).and_return('::Author') ::Author.stub!(:reflect_on_validations_for).and_return([]) ::Author.stub!(:reflect_on_association).and_return { |column_name| mock('reflection', :options => {}, :klass => Post, :macro => :has_many) if column_name == :posts } + ::Author.stub!(:content_columns).and_return([mock('column', :name => 'login'), mock('column', :name => 'created_at')]) # Sometimes we need a mock @post object and some Authors for belongs_to @new_post = mock('post') @@ -153,11 +154,13 @@ def new_author_path; "/authors/new"; end end end ::Post.stub!(:find).and_return([@freds_post]) + ::Post.stub!(:content_columns).and_return([mock('column', :name => 'title'), mock('column', :name => 'body'), mock('column', :name => 'created_at')]) @new_post.stub!(:title) @new_post.stub!(:body) @new_post.stub!(:published) @new_post.stub!(:publish_at) + @new_post.stub!(:created_at) @new_post.stub!(:secret) @new_post.stub!(:time_zone) @new_post.stub!(:category_name)