Nested form with has_many :through -- how to modify the "through" attributes

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

#bar.rb
class Bar < ActiveRecord::Base
  belongs_to :foo
  belongs_to :baz
  # also defines the 'blarg' attribute
end

#baz.rb
class Baz < ActiveRecord::Base
  has_many :bars
  has_many :foos, :through => :bars
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?

Thanks,

Walter

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 %>

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?

Thanks in advance,

Walter

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

Walter Davis wrote in post #1071859:

SOLVED: Note to future self:

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

Thank you very much i appreciate it!!!