Permalink
Browse files

Toying with fields_for a has_many association and finding a way to ha…

…ve NestedParams build new records from those generated forms.

In the case of a new record we simply generate a fictive id, but since we don't need it afterwards there's no point in keeping
a counter around, thus we simply generate an id like 'new_12345' where the number is the #object_id of the record.
We can then catch these new records in the NestedParams writer method and build new records.
  • Loading branch information...
alloy committed Aug 19, 2008
1 parent c1cd32b commit 9f711f2ae148eca7b95864f1c537c99859d2b051
@@ -0,0 +1,23 @@
+class NestedParamsFormBuilder < ActionView::Helpers::FormBuilder
+ def fields_for(record_or_name_or_array, *args, &block)
+ if record_or_name_or_array.is_a?(String) || record_or_name_or_array.is_a?(Symbol)
+ # Is this reflecting bad speed wise? And is there an alternative?
+ if reflection = @object.class.reflect_on_all_associations.detect { |r| r.name == record_or_name_or_array.to_sym }
+ if reflection.macro == :has_many
+ record = args.first
+ # In the case of a new object use a fictive id which is composited with "new_" and the object_id.
+ name = "#{object_name}[#{record_or_name_or_array}][#{ record.new_record? ? "new_#{record.object_id}" : record.id}]"
+ # TODO: We also need to pass the options along without the record instance.
+ return @template.fields_for(name, args.first, &block)
+ end
+ end
+ end
+ super
+ end
+
+ private
+
+ def log(*args)
+ Rails.logger.info args.map { |obj| obj.inspect }.join(', ')
+ end
+end
@@ -1,3 +1,5 @@
+load 'nested_params_form_builder.rb'
+
class ProjectsController < ApplicationController
def index
@projects = Project.find(:all)
@@ -24,10 +26,11 @@ def create
def edit
@project = Project.find(params[:id])
+ # add an extra new record for debugging purposes
+ @project.tasks.build
end
def update
- #params[:project][:tasks] ||= []
@project = Project.find(params[:id])
if @project.update_attributes(params[:project])
flash[:notice] = "Successfully updated project."
@@ -2,16 +2,18 @@
<%= error_messages_for :project %>
-<% form_for @project do |f| -%>
+<% form_for @project, :builder => NestedParamsFormBuilder do |f| -%>
+
<p>
<%= f.label :name, "Project:" %>
<%= f.text_field :name %>
</p>
<div id="tasks">
- <%= render :partial => 'task', :collection => @project.tasks %>
+ <%= render :partial => 'task', :collection => @project.tasks, :locals => { :pf => f } %>
+ </div>
</div>
<p>
- <%= add_task_link "Add a task" %>
+ <%#= add_task_link "Add a task" %>
</p>
<p>
<%= f.submit "Submit" %>
@@ -1,11 +1,9 @@
<div class="task">
-<% fields_for 'project[tasks]', task, :index => '' do |f| -%>
- <%= error_messages_for :task, :object => task %>
- <%= f.hidden_field :id unless task.new_record? %>
+<% pf.fields_for :tasks, task do |f| %>
+ <%#= error_messages_for :task, :object => task %>
<p>
<%= f.label :name, "Task:" %>
<%= f.text_field :name %>
<%= link_to_function "remove", "$(this).up('.task').remove()" %>
</p>
-<% end -%>
-</div>
+<% end %>
View
@@ -32,7 +32,7 @@
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
# Add additional load paths for your own custom dirs
- # config.load_paths += %W( #{RAILS_ROOT}/extras )
+ config.load_paths += %W( #{RAILS_ROOT}/app/concerns )
# Force all environments to use the same logger level
# (by default production uses :info, the others :debug)
@@ -49,6 +49,9 @@ body {
.fieldWithErrors {
display: inline;
}
+.fieldWithErrors input {
+ border: 1px solid red;
+}
#errorExplanation {
width: 400px;
@@ -50,13 +50,18 @@ def define_nested_params_for_has_many_association(attr)
class_eval do
define_method("#{attr}_with_nested_params=") do |value|
if value.is_a? Hash
- # For existing records
+ # For existing records and new records that are marked by an id that starts with 'new_'
value.each do |id, attributes|
- send(attr).detect { |x| x.id == id.to_i }.attributes = attributes
+ if id.starts_with? 'new_'
+ send(attr).build attributes
+ else
+ # Find the record for this id and assign the attributes
+ send(attr).detect { |x| x.id == id.to_i }.attributes = attributes
+ end
end
else
if value.is_a?(Array) && value.all? { |x| x.is_a?(Hash) }
- # For new records
+ # For an array full of new record hashes
value.each do |attributes|
send(attr).build attributes
end
@@ -63,6 +63,22 @@
@visitor.reload
@visitor.artists.map(&:name).sort.should == %w{ jack joe }
end
+
+ it "should update existing records and add new ones that have an id that start with the string 'new_'" do
+ before = Artist.count
+
+ @visitor.update_attributes({
+ :artists => {
+ @artist1.id.to_s => { :name => 'joe' },
+ "new_12345" => { :name => 'jill' },
+ @artist2.id.to_s => { :name => 'jack' }
+ }
+ })
+ @visitor.reload
+
+ Artist.count.should.be before + 1
+ @visitor.artists.map(&:name).sort.should == %w{ jack jill joe }
+ end
end
describe "NestedParams, on a has_one association" do

0 comments on commit 9f711f2

Please sign in to comment.