Skip to content

Commit

Permalink
DRYer fieldset legends; check if first inputs block argument is a val…
Browse files Browse the repository at this point in the history
…id legend - the options (:name/:title) way works still, and got higher priority. Closes #106.
  • Loading branch information
grimen authored and justinfrench committed Nov 18, 2009
1 parent 269e530 commit 3cad250
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 30 deletions.
6 changes: 3 additions & 3 deletions README.textile
Expand Up @@ -148,7 +148,7 @@ If you want to customize the label text, or render some hint text below the fiel

<pre>
<% 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 %>
Expand Down Expand Up @@ -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._

<pre>
<% semantic_form_for @post do |form| %>
<% form.inputs :title => :post_details do %> # => :title => "Post details"
<% form.inputs :post_details do %> # => :title => "Post details"
# ...
<% end %>
# ...
Expand Down
88 changes: 76 additions & 12 deletions lib/formtastic.rb
Expand Up @@ -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}}']
Expand Down Expand Up @@ -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| %>
Expand All @@ -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,
Expand All @@ -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:
Expand All @@ -185,6 +200,12 @@ def input(method, options = {})
# <li class="select">...</li>
# </ol>
# </fieldset>
# <fieldset class="inputs">
# <legend><span>Extra</span></legend>
# <ol>
# <li class="datetime">...</li>
# </ol>
# </fieldset>
# </form>
#
# === Nested attributes
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Expand Down
42 changes: 27 additions & 15 deletions spec/inputs_spec.rb
Expand Up @@ -174,44 +174,53 @@
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
end
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
Expand All @@ -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)
Expand Down Expand Up @@ -352,20 +360,24 @@
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
output_buffer.should have_tag('form > fieldset#my-id.inputs')
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

Expand Down
3 changes: 3 additions & 0 deletions spec/spec_helper.rb
Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 3cad250

Please sign in to comment.