Vinicius Martinson
Vinicius Martinson

Reputation: 105

Nested forms with has_one :through

I'm having some trouble to implement a nested form with a has_one :through association.

Models

# model: member.rb

belongs_to :user
has_one :academic

# model: user.rb

has_one :member
has_one :academic, through: :member

accepts_nested_attributes_for :member, reject_if: :all_blank 
accepts_nested_attributes_for :academic, reject_if: :all_blank

# model: academic.rb

belongs_to :member
belongs_to :user 

Controller

# users_controller.rb

def new
  @user = User.new
  @user.build_member
  @user.build_academic  
end

I also have tried with:

@user.member.build_academic

View

# new.html.erb

<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |ff| %>

  <%= ff.text_field :email %>

# member belongs to user so I can call a fields_for
<% ff.fields_for :member do |f| %>

  <%= f.text_field :name %>

# this part is not shown. What is wrong with my association?
<% f.fields_for :academic do |a| %>

  <%= a.text_field :major %>

<% end %>
 <% end %>
  <% end %>

I've taken a look into the Rails documentation. The first fields_for is shown in the page (:member), but the second one (:academic), which has the has_one :through association, is not shown in the page.

Any help will be appreciated. Thank you.

Upvotes: 2

Views: 1451

Answers (2)

Vinicius Martinson
Vinicius Martinson

Reputation: 105

For future reference or for anyone that has the same problem, I found a way around to fix this problem, in case you have same structure.

I could fix this by going to the view, before the form, and writing:

<% resource.member.build_academic %>

In my case resource is User, set by devise, a Rails gem used for authentication.

And you don't need to reference any :through or whatsoever in your model.

It is not the most efficient way, but I haven't found any other solution. Hope it helps.

Upvotes: 1

Richard Peck
Richard Peck

Reputation: 76774

Through

If you want to build your data through a relation, you have to pass the associated data as it's constructed:

#app/models/member.rb
class Member < ActiveRecord::Base
   belongs_to :user
   has_one :academic
   
   accepts_nested_attributes_for :academic
end

#app/models/user.rb
class User < ActiveRecord::Base
   has_one :member
   has_one :academic, through: :member

   accepts_nested_attributes_for :member
end

#app/models/academic.rb
class Academic < ActiveRecord::Base
   belongs_to :member
   belongs_to :user
end

This will allow you to do the following:

#app/controllers/members_controller.rb
class MembersController < ApplicationController
   def new
      @member = Member.new
      @member.build_member.build_academic
   end

   def create
      @member = Member.new member_params
      @member.save
   end

   private

   def member_params
      params.require(:member).permit(:x, :y, :z, academic_attributes: [:some, :attributes, member_attributes:[...]])
   end
end

This would permit the following:

#app/views/users/new.html.erb
<%= form_for @user do |f| %>
   <%= f.fields_for :member do |m| %>
      <%= f.text_field :name %>
      ...
      <%= m.fields_for :academic do |a| %>
           <% a.text_field :name %>
           ...
      <% end %> 
   <% end %>
   <%= f.submit %>
<% end %>

This works to build a new member object, and a new academic object from the user. Although not strictly what you're asking, it looks like it could benefit you in some form.


Associations

If you want to do what you're asking (IE build_member and build_academic exclusively), you'll need to get rid of the has_one :through relationship...

#app/models/user.rb
class User < ActiveRecord::Base
   has_many :memberships
   has_many :academics, through: :memberships
end

#app/models/membership.rb
class Membership < ActiveRecord::Base
   belongs_to :user
   belongs_to :academic
end

#app/models/academic.rb
class Academic < ActiveRecord::Base
   has_many :memberships
   has_many :users, through: :memberships
end

The problem is you're basically trying to build a relationship for a direct association (member) and an indirect relationship (academic).

If you want to build both exclusively, you have to make them have a direct association with your main model. The above should allow you to do the following:

#app/controllers/users_controller.rb
class UsersController < ApplicationController
   def new 
       @user = User.new
       @user.members.build.academics.build
   end
end

Much like my top example, this will pass nested data through your form - if you wanted to have it completely exclusively, do this:

#app/models/user.rb
class User < ActiveRecord::Base
   has_one :member
   has_one :academic
   has_and_belongs_to_many :academics
end

#app/models/member.rb
class Member < ActiveRecord::Base
   belongs_to :user
end

#app/models/academic.rb
class Academic < ActiveRecord::Base
   belongs_to :user
   has_and_belongs_to_many :users
end

This will allow the following:

#app/controllers/users_controller.rb
class UsersController < ApplicationController
   def new
      @user = User.new
      @user.build_member
      @user.build_academic
   end

   def create
      @user = User.new user_params
      @user.save
   end

   private

   def user_params
      params.require(:user).permit(:user, :params, member_attributes: [], academic_attributes:[])
   end
end

Upvotes: 3

Related Questions