Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add support for nested object forms to ActiveRecord and the helpers i…
…n ActionPack

Signed-Off-By: Michael Koziarski <michael@koziarski.com>

[#1202 state:committed]
  • Loading branch information
alloy authored and NZKoz committed Feb 1, 2009
1 parent a02d752 commit ec8f045
Show file tree
Hide file tree
Showing 21 changed files with 1,687 additions and 58 deletions.
12 changes: 12 additions & 0 deletions actionpack/CHANGELOG
@@ -1,5 +1,17 @@
*2.3.0 [Edge]*

* Make the form_for and fields_for helpers support the new Active Record nested update options. #1202 [Eloy Duran]

<% form_for @person do |person_form| %>
...
<% person_form.fields_for :projects do |project_fields| %>
<% if project_fields.object.active? %>
Name: <%= project_fields.text_field :name %>
<% end %>
<% end %>
<% end %>


* Added grouped_options_for_select helper method for wrapping option tags in optgroups. #977 [Jon Crawford]

* Implement HTTP Digest authentication. #1230 [Gregg Kellogg, Pratik Naik] Example :
Expand Down
196 changes: 187 additions & 9 deletions actionpack/lib/action_view/helpers/form_helper.rb
Expand Up @@ -269,10 +269,12 @@ def apply_form_for_options!(object_or_array, options) #:nodoc:
options[:url] ||= polymorphic_path(object_or_array)
end

# Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes
# fields_for suitable for specifying additional model objects in the same form:
# Creates a scope around a specific model object like form_for, but
# doesn't create the form tags themselves. This makes fields_for suitable
# for specifying additional model objects in the same form.
#
# === Generic Examples
#
# ==== Examples
# <% form_for @person, :url => { :action => "update" } do |person_form| %>
# First name: <%= person_form.text_field :first_name %>
# Last name : <%= person_form.text_field :last_name %>
Expand All @@ -282,20 +284,166 @@ def apply_form_for_options!(object_or_array, options) #:nodoc:
# <% end %>
# <% end %>
#
# ...or if you have an object that needs to be represented as a different parameter, like a Client that acts as a Person:
# ...or if you have an object that needs to be represented as a different
# parameter, like a Client that acts as a Person:
#
# <% fields_for :person, @client do |permission_fields| %>
# Admin?: <%= permission_fields.check_box :admin %>
# <% end %>
#
# ...or if you don't have an object, just a name of the parameter
# ...or if you don't have an object, just a name of the parameter:
#
# <% fields_for :person do |permission_fields| %>
# Admin?: <%= permission_fields.check_box :admin %>
# <% end %>
#
# Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base,
# like FormOptionHelper#collection_select and DateHelper#datetime_select.
# Note: This also works for the methods in FormOptionHelper and
# DateHelper that are designed to work with an object as base, like
# FormOptionHelper#collection_select and DateHelper#datetime_select.
#
# === Nested Attributes Examples
#
# When the object belonging to the current scope has a nested attribute
# writer for a certain attribute, fields_for will yield a new scope
# for that attribute. This allows you to create forms that set or change
# the attributes of a parent object and its associations in one go.
#
# Nested attribute writers are normal setter methods named after an
# association. The most common way of defining these writers is either
# with +accepts_nested_attributes_for+ in a model definition or by
# defining a method with the proper name. For example: the attribute
# writer for the association <tt>:address</tt> is called
# <tt>address_attributes=</tt>.
#
# Whether a one-to-one or one-to-many style form builder will be yielded
# depends on whether the normal reader method returns a _single_ object
# or an _array_ of objects.
#
# ==== One-to-one
#
# Consider a Person class which returns a _single_ Address from the
# <tt>address</tt> reader method and responds to the
# <tt>address_attributes=</tt> writer method:
#
# class Person
# def address
# @address
# end
#
# def address_attributes=(attributes)
# # Process the attributes hash
# end
# end
#
# This model can now be used with a nested fields_for, like so:
#
# <% form_for @person, :url => { :action => "update" } do |person_form| %>
# ...
# <% person_form.fields_for :address do |address_fields| %>
# Street : <%= address_fields.text_field :street %>
# Zip code: <%= address_fields.text_field :zip_code %>
# <% end %>
# <% end %>
#
# When address is already an association on a Person you can use
# +accepts_nested_attributes_for+ to define the writer method for you:
#
# class Person < ActiveRecord::Base
# has_one :address
# accepts_nested_attributes_for :address
# end
#
# If you want to destroy the associated model through the form, you have
# to enable it first using the <tt>:allow_destroy</tt> option for
# +accepts_nested_attributes_for+:
#
# class Person < ActiveRecord::Base
# has_one :address
# accepts_nested_attributes_for :address, :allow_destroy => true
# end
#
# Now, when you use a form element with the <tt>_delete</tt> parameter,
# with a value that evaluates to +true+, you will destroy the associated
# model (eg. 1, '1', true, or 'true'):
#
# <% form_for @person, :url => { :action => "update" } do |person_form| %>
# ...
# <% person_form.fields_for :address do |address_fields| %>
# ...
# Delete: <%= address_fields.check_box :_delete %>
# <% end %>
# <% end %>
#
# ==== One-to-many
#
# Consider a Person class which returns an _array_ of Project instances
# from the <tt>projects</tt> reader method and responds to the
# <tt>projects_attributes=</tt> writer method:
#
# class Person
# def projects
# [@project1, @project2]
# end
#
# def projects_attributes=(attributes)
# # Process the attributes hash
# end
# end
#
# This model can now be used with a nested fields_for. The block given to
# the nested fields_for call will be repeated for each instance in the
# collection:
#
# <% form_for @person, :url => { :action => "update" } do |person_form| %>
# ...
# <% person_form.fields_for :projects do |project_fields| %>
# <% if project_fields.object.active? %>
# Name: <%= project_fields.text_field :name %>
# <% end %>
# <% end %>
# <% end %>
#
# It's also possible to specify the instance to be used:
#
# <% form_for @person, :url => { :action => "update" } do |person_form| %>
# ...
# <% @person.projects.each do |project| %>
# <% if project.active? %>
# <% person_form.fields_for :projects, project do |project_fields| %>
# Name: <%= project_fields.text_field :name %>
# <% end %>
# <% end %>
# <% end %>
# <% end %>
#
# When projects is already an association on Person you can use
# +accepts_nested_attributes_for+ to define the writer method for you:
#
# class Person < ActiveRecord::Base
# has_many :projects
# accepts_nested_attributes_for :projects
# end
#
# If you want to destroy any of the associated models through the
# form, you have to enable it first using the <tt>:allow_destroy</tt>
# option for +accepts_nested_attributes_for+:
#
# class Person < ActiveRecord::Base
# has_many :projects
# accepts_nested_attributes_for :projects, :allow_destroy => true
# end
#
# This will allow you to specify which models to destroy in the
# attributes hash by adding a form element for the <tt>_delete</tt>
# parameter with a value that evaluates to +true+
# (eg. 1, '1', true, or 'true'):
#
# <% form_for @person, :url => { :action => "update" } do |person_form| %>
# ...
# <% person_form.fields_for :projects do |project_fields| %>
# Delete: <%= project_fields.check_box :_delete %>
# <% end %>
# <% end %>
def fields_for(record_or_name_or_array, *args, &block)
raise ArgumentError, "Missing block" unless block_given?
options = args.extract_options!
Expand Down Expand Up @@ -760,7 +908,11 @@ def fields_for(record_or_name_or_array, *args, &block)

case record_or_name_or_array
when String, Symbol
name = "#{object_name}#{index}[#{record_or_name_or_array}]"
if nested_attributes_association?(record_or_name_or_array)
return fields_for_with_nested_attributes(record_or_name_or_array, args, block)
else
name = "#{object_name}#{index}[#{record_or_name_or_array}]"
end
when Array
object = record_or_name_or_array.last
name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
Expand Down Expand Up @@ -802,11 +954,37 @@ def submit(value = "Save changes", options = {})
def objectify_options(options)
@default_options.merge(options.merge(:object => @object))
end

def nested_attributes_association?(association_name)
@object.respond_to?("#{association_name}_attributes=")
end

def fields_for_with_nested_attributes(association_name, args, block)
name = "#{object_name}[#{association_name}_attributes]"
association = @object.send(association_name)

if association.is_a?(Array)
children = args.first.respond_to?(:new_record?) ? [args.first] : association

children.map do |child|
child_name = "#{name}[#{ child.new_record? ? new_child_id : child.id }]"
@template.fields_for(child_name, child, *args, &block)
end.join
else
@template.fields_for(name, association, *args, &block)
end
end

def new_child_id
value = (@child_counter ||= 1)
@child_counter += 1
"new_#{value}"
end
end
end

class Base
cattr_accessor :default_form_builder
self.default_form_builder = ::ActionView::Helpers::FormBuilder
end
end
end

3 comments on commit ec8f045

@tamersalama
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great Feature! Thanks Guys.

I hope I’m not making a fool of myself here :)

It seems that adding the foreign key presence validation to the child instance creates a circular dependency (the child validation is done before the parent id is propagated to the child).


class Person < ActiveRecord::Base
  has_many :children
  accepts_nested_attributes_for :children
end

class Child < ActiveRecord::Base
  belongs_to :person
  
  #Validating that a toy belongs to a person
  validates_presence_of :person_id
end

  p = Person.new(:name => "Smith", :children_attributes => {"new_1" => {:name => "John"}})
  p.valid? #=> false
  p.save  #false
  p.children.first.errors.on(:person_id) #=> "can't be blank"

Is it just late or does it need a work-around?

@alloy
Copy link
Contributor Author

@alloy alloy commented on ec8f045 Feb 11, 2009

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tamersalama: No you’re not making a fool out of yourself :)
Please file a ticket on lighthouse: http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets

@tamersalama
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A ticket has been created:

http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/1943

Please sign in to comment.