SOLVED: Note to future self:
This question occurred to me as I was answering a different question. Given a has_many :through relationship, where there are additional attributes in the :through model, how would you construct a form so you could update those attributes?
#foo.rb
class Foo < ActiveRecord::Base
has_many :bars
has_many :bazzes, :through => :bars
accepts_nested_attributes_for :bazzes
end
class Foo < ActiveRecord::Base
attr_accessible :name, :bars_attributes
has_many :bars
has_many :bazs, :through => :bars, :dependent => :destroy
accepts_nested_attributes_for :bars
end
#bar.rb
class Bar < ActiveRecord::Base
belongs_to :foo
belongs_to :baz
# also defines the 'blarg' attribute
end
class Bar < ActiveRecord::Base
attr_accessible :baz_id, :blarged, :foo_id, :baz_attributes, :baz
belongs_to :baz
belongs_to :foo
accepts_nested_attributes_for :baz
end
#baz.rb
class Baz < ActiveRecord::Base
has_many :bars
has_many :foos, :through => :bars
end
class Baz < ActiveRecord::Base
attr_accessible :title
has_many :bars
has_many :foos, :through => :bars, :dependent => :destroy
end
So in a form_for @foo, in fields_for :bazzes, how would I define a checkbox to set the 'blarg' attribute to true or false? Does the nested @foo.baz just have a magical #blarg attribute, even though that attribute is defined in the linking model Bar?
I've gone so far as to scaffold this out, because I really would like to know the answer! Aside from having the plural of Baz wrong, my code above stands, and works as far as having multiple baz objects attached to a foo through a bar, and being able to create and modify these in a nested form. This part of Rails has only gotten easier to use in the latest version. What I cannot seem to do is in any way change the Bar when saving the association of Baz from a nested Foo form.
Here's my view:
<%= form_for(@foo) do |f| %>
<% if @foo.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@foo.errors.count, "error") %> prohibited this foo from being saved:</h2>
<ul>
<% @foo.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<!-- this all works swimmingly -->
<%= f.fields_for :bazs do |b| %>
<div class="field">
<%= b.label :title, "Baz" %><br />
<%= b.text_field :title %>
<!-- here be the error -->
<%= b.check_box :blarged %>
<!-- what do I do to set bar#blarged= in the context of h/m/t? -->
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
<%= form_for(@foo) do |f| %>
<% if @foo.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@foo.errors.count, "error") %> prohibited this foo from being saved:</h2>
<ul>
<% @foo.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<%= f.fields_for :bars do |b| %>
<div class="field">
<%= b.fields_for :baz do |z| %>
<%= z.label :title, "Baz" %><br />
<%= z.text_field :title %>
<% end %>
<%= b.check_box :blarged %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Trying this in console, I can get the list of bazs associated with a foo, and if I inspect one of those, I can see that it has a foo, but I can't seem to do anything to get the bar that stands between them and get or set any of its properties.
Loading development environment (Rails 3.2.7)
1.9.2p320 :001 > f = Foo.last
Foo Load (0.1ms) SELECT "foos".* FROM "foos" ORDER BY "foos"."id" DESC LIMIT 1
=> #<Foo id: 3, name: "I'm a Foo", created_at: "2012-08-09 16:37:38", updated_at: "2012-08-09 16:37:38">
1.9.2p320 :002 > f.bars
Bar Load (0.3ms) SELECT "bars".* FROM "bars" WHERE "bars"."foo_id" = 3
=> [#<Bar id: 1, blarged: nil, foo_id: 3, baz_id: 1, created_at: "2012-08-09 16:37:38", updated_at: "2012-08-09 16:37:38">, #<Bar id: 2, blarged: nil, foo_id: 3, baz_id: 2, created_at: "2012-08-09 16:37:38", updated_at: "2012-08-09 16:37:38">]
1.9.2p320 :004 > f.bazs
Baz Load (0.2ms) SELECT "bazs".* FROM "bazs" INNER JOIN "bars" ON "bazs"."id" = "bars"."baz_id" WHERE "bars"."foo_id" = 3
=> [#<Baz id: 1, title: "Here's a baz", created_at: "2012-08-09 16:37:38", updated_at: "2012-08-09 16:37:38">, #<Baz id: 2, title: "Here's another", created_at: "2012-08-09 16:37:38", updated_at: "2012-08-09 16:37:38">]
1.9.2p320 :005 > f.bazs.first.bar
NoMethodError: undefined method `bar' for #<Baz:0x007ff89a9ba7b8>
from /Users/waltd/.rvm/gems/ruby-1.9.2-p320/gems/activemodel-3.2.7/lib/active_model/attribute_methods.rb:407:in `method_missing'
from /Users/waltd/.rvm/gems/ruby-1.9.2-p320/gems/activerecord-3.2.7/lib/active_record/attribute_methods.rb:149:in `method_missing'
from (irb):5
from /Users/waltd/.rvm/gems/ruby-1.9.2-p320/gems/railties-3.2.7/lib/rails/commands/console.rb:47:in `start'
from /Users/waltd/.rvm/gems/ruby-1.9.2-p320/gems/railties-3.2.7/lib/rails/commands/console.rb:8:in `start'
from /Users/waltd/.rvm/gems/ruby-1.9.2-p320/gems/railties-3.2.7/lib/rails/commands.rb:41:in `<top (required)>'
from script/rails:6:in `require'
from script/rails:6:in `<main>'
1.9.2p320 :006 >
It seems to me as though if I use the magical h/m/t and nested form behavior, I don't get access to the specific bar being used to bridge one foo to one baz. If I manually inspect a foo's bars, I can see the extra attribute (but I would have to rewrite my form and controller and not use the magic). What's the trick I need?
The trick was to not try to build a bridge in mid-air. The foo could find a baz, but that found baz did not seem to have any notion of which bar was standing between the two. Nesting my forms one layer at a time did the trick. fields_for bars then contained fields_for baz.
Walter