Skip to content

Commit

Permalink
Add support for nested forms (http://ryandaigle.com/articles/2009/2/1…
Browse files Browse the repository at this point in the history
…/what-s-new-in-edge-rails-nested-attributes).

Signed-off-by: Justin French <justin@indent.com.au>
  • Loading branch information
sprsquish authored and justinfrench committed Feb 16, 2009
1 parent ee26f18 commit 085e22f
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 71 deletions.
13 changes: 13 additions & 0 deletions README.textile
Expand Up @@ -194,6 +194,19 @@ You don't even have to specify the field list (Formtastic will simply render and

Pretty soon we won't have to write any code at all ;)

h2. Nested forms (Rails 2.3+)

Nested forms are supported:

<pre>
<% semantic_form_for @post do |post| %>
<%= post.semantic_fields_for :author do |author| %>
<%= author.inputs %>
<%= end %>
<%= post.buttons %>
<% end %>
</pre>

h2. Extending Formtastic

Adding functionality to Formtastic can be done by extending SemanticFormBuilder and configuring formtastic's builder.
Expand Down
91 changes: 71 additions & 20 deletions lib/formtastic.rb
@@ -1,3 +1,17 @@
# Override the default ActiveRecordHelper behaviour of wrapping the input.
# This gets taken care of semantically by adding an error class to the LI tag
# containing the input.
module ActionView
module Helpers
class InstanceTag
alias_method :error_wrapping_with_wrapping, :error_wrapping
def error_wrapping(html_tag, has_error)
html_tag
end
end
end
end

module Formtastic #:nodoc:

class SemanticFormBuilder < ActionView::Helpers::FormBuilder
Expand Down Expand Up @@ -64,7 +78,7 @@ def input(method, options = {})
html_class = [
options[:as].to_s,
(options[:required] ? 'required' : 'optional'),
(@object.errors.on(method) ? 'error' : nil)
(@object.errors.on(method.to_s) ? 'error' : nil)
].compact.join(" ")

html_id = "#{@object_name}_#{method}_input"
Expand Down Expand Up @@ -213,8 +227,44 @@ def commit_button(value = save_or_create_commit_button_text, options = {})
template.content_tag(:li, template.submit_tag(value), :class => "commit")
end


# A thin wrapper around #fields_for to set :builder => Formtastic::SemanticFormBuilder
# for nesting forms:
#
# # Example:
# <% semantic_form_for @post do |post| %>
# <% post.semantic_fields_for :author do |author| %>
# <% author.inputs :name %>
# <% end %>
# <% end %>
#
# # Output:
# <form ...>
# <fieldset class="inputs">
# <ol>
# <li class="string"><input type='text' name='post[author][name]' id='post_author_name' /></li>
# </ol>
# </fieldset>
# </form>
def semantic_fields_for(record_or_name_or_array, *args, &block)
opts = args.extract_options!
opts.merge!(:builder => Formtastic::SemanticFormBuilder)
args.push(opts)
fields_for(record_or_name_or_array, *args, &block)
end

protected

# Ensure :object => @object is set before sending the options down to the Rails layer.
# Also remove any Formtastic-specific options
def set_options(options)
opts = options.dup
[:value_method, :label_method, :collection, :required, :label, :as, :hint].each do |key|
opts.delete(key)
end
opts.merge(:object => @object)
end

def save_or_create_commit_button_text #:nodoc:
prefix = @object.new_record? ? "Create" : "Save"
"#{prefix} #{@object_name.to_s.humanize}"
Expand Down Expand Up @@ -295,12 +345,12 @@ def select_input(method, options)
options[:collection] ||= find_parent_objects_for_column(method)
options[:label_method] ||= options[:collection].first.respond_to?(:to_label) ? :to_label : :to_s
options[:value_method] ||= :id

choices = options[:collection].map { |o| [o.send(options[:label_method]), o.send(options[:value_method])] }
input_label(method, options) + template.select(@object_name, method, choices, options)
input_label(method, options) + template.select(@object_name, method, choices, set_options(options))
end


# Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
# items, one for each possible choice in the belongs_to association. Each li contains a
# label and a radio input.
Expand Down Expand Up @@ -354,15 +404,15 @@ def radio_input(method, options)
options[:collection].map { |c|
template.content_tag(:li,
template.content_tag(:label,
"#{template.radio_button(@object_name, method, c.id)} #{c.send(options[:label_method])}",
"#{template.radio_button(@object_name, method, c.id, set_options(options))} #{c.send(options[:label_method])}",
:for => "#{@object_name}_#{method}_#{c.id}"
)
)
}
)
)
end

# Outputs a label and a password input, nothing fancy.
def password_input(method, options)
input_label(method, options) +
Expand All @@ -372,7 +422,7 @@ def password_input(method, options)

# Outputs a label and a textarea, nothing fancy.
def text_input(method, options)
input_label(method, options) + template.text_area(@object_name, method)
input_label(method, options) + template.text_area(@object_name, method, set_options(options))
end


Expand All @@ -393,7 +443,7 @@ def numeric_input(method, options)
# Outputs label and file field
def file_input(method, options)
input_label(method, options) +
template.file_field(@object_name, method)
template.file_field(@object_name, method, set_options(options))
end


Expand Down Expand Up @@ -472,9 +522,10 @@ def date_or_datetime_input(method, options)
break if time_inputs.include?(input)
list_items_capture << template.hidden_field_tag("#{@object_name}[#{method}(#{position[input]}i)]", @object.send(method), :id => "#{@object_name}_#{method}_#{position[input]}i")
else
opts = set_options(options).merge({:prefix => @object_name, :field_name => "#{method}(#{position[input]}i)", :include_blank => options[:include_blank]})
list_items_capture << template.content_tag(:li,
template.content_tag(:label, input.to_s.humanize, :for => "#{@object_name}_#{method}_#{position[input]}i") +
template.send("select_#{input}".intern, @object.send(method), :prefix => @object_name, :field_name => "#{method}(#{position[input]}i)", :include_blank => options[:include_blank])
template.send("select_#{input}".intern, @object.send(method), opts)
)
end
end
Expand All @@ -489,7 +540,7 @@ def date_or_datetime_input(method, options)
# name (method name) and can be altered with the :label option.
def boolean_input(method, options)
input_label(method, options,
template.check_box(@object_name, method) +
template.check_box(@object_name, method, set_options(options)) +
label_text(method, options)
)
end
Expand Down Expand Up @@ -519,7 +570,7 @@ def boolean_select_input(method, options)
options[:false] ||= "No"

choices = [ [options[:true],true], [options[:false],false] ]
input_label(method, options) + template.select(@object_name, method, choices, options)
input_label(method, options) + template.select(@object_name, method, choices, set_options(options))
end

# Outputs a fieldset containing two radio buttons (with labels) for "true" and "false". The
Expand Down Expand Up @@ -568,16 +619,16 @@ def boolean_radio_input(method, options)
end

def inline_errors(method, options) #:nodoc:
errors = @object.errors.on(method).to_a
errors = @object.errors.on(method.to_s).to_a
unless errors.empty?
send("error_#{@@inline_errors}", errors) if [:sentence, :list].include?(@@inline_errors)
end
end

def error_sentence(errors) #:nodoc:
template.content_tag(:p, errors.to_sentence, :class => 'inline-errors')
end

def error_list(errors) #:nodoc:
list_elements = []
errors.each do |error|
Expand Down Expand Up @@ -628,7 +679,7 @@ def field_set_and_list_wrapping(field_set_html_options, contents = '', &block) #
# cases it will simplify (like the case of :integer, :float & :decimal to :numeric), or do
# something different (like :password and :select).
#
# If there is no column for the method (eg "virtual columns" with an attr_accessor), the
# If there is no column for the method (eg "virtual columns" with an attr_accessor), the
# default is a :string, a similar behaviour to Rails' scaffolding.
def default_input_type(object, method) #:nodoc:
# rescue if object does not respond to "column_for_attribute" method
Expand All @@ -644,7 +695,7 @@ def default_input_type(object, method) #:nodoc:
return :numeric if [:integer, :float, :decimal].include?(column.type)
return :password if column.type == :string && method.to_s =~ /password/
# otherwise assume the input name will be the same as the column type (eg string_input)
return column.type
return column.type
else
return :password if method.to_s =~ /password/
return :string
Expand All @@ -660,18 +711,19 @@ def find_parent_objects_for_column(column)
end

def default_string_options(method) #:nodoc:
# Use rescue to set column if @object does not have a column_for_attribute method
# Use rescue to set column if @object does not have a column_for_attribute method
# (eg if @object is not an ActiveRecord object)
begin
column = @object.column_for_attribute(method)
rescue NoMethodError
column = nil
end
if column.nil? || column.limit.nil?
opts = if column.nil? || column.limit.nil?
{ :size => DEFAULT_TEXT_FIELD_SIZE }
else
{ :maxlength => column.limit, :size => [column.limit, DEFAULT_TEXT_FIELD_SIZE].min }
end
set_options(opts)
end

end
Expand Down Expand Up @@ -746,5 +798,4 @@ def semantic_#{meth}(record_or_name_or_array, *args, &proc)
module_eval src, __FILE__, __LINE__
end
end

end

0 comments on commit 085e22f

Please sign in to comment.