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 = {})
#
...
#
#
+ #
#
#
# === 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)