How to create new contacts from nested attributes form
I'm building my first semi "real world" rails app (an Open House management app for real estate). I am currently stuck on a "accepts nested attributes" problem. Here are the details... (I've provided all code below)
An open house can have many contacts (attendees), and a contact can attend many open houses. So, I have a join model called Signin. When someone "signs in" to an open house via the Open House > Sign In form, I would like a "sign in" instance to be created along with a contact via nested attributes. Note: Since this is a "has many through", the "Sign in" and "contact" models are not the traditional "parent/child" relationship.
When attempting to submit my Signin form, the form error says "Contact must exist" and the log reports "Unpermitted parameter: :contacts". (I do have strong parameter sit, but perhaps incorrectly).
At this point, I just feel like I've tried everything, but I can't get the form to create a Contact as well as a Signin instance.
The relevant models:
class OpenHouse < ApplicationRecord
belongs_to :listing
has_many :signins, inverse_of: :open_house
has_many :contacts, through: :signins
class Signin < ApplicationRecord
belongs_to :open_house
belongs_to :contact
accepts_nested_attributes_for :contact
class Contact < ApplicationRecord
has_many :signins
has_many :open_houses, through: :signins
Routes: (Signins are nested within each open house to allow easy sharing of sign in form)
resources :open_houses do
resources :signins
end
resources :contacts
The Signin Controller:
class SigninsController < ApplicationController
def new
@open_house = OpenHouse.find(params[:open_house_id])
@signin = @open_house.signins.build
end
def create
@open_house = OpenHouse.find(params[:open_house_id])
@signin = @open_house.signins.new(signin_params)
if @signin.save
redirect_to @signin, notice: 'You are now signed in'
else
render :new
end
end
def show
@signin = Signin.find(params[:id])
end
private
def signin_params
params.require(:signin).permit( contacts_attributes: [:name, :email, :phone])
end
end
Signin Form:
<%= form_with model: [ @open_house, @open_house.signins.build ], local: true do |f| %>
<% if @signin.errors.any? %>
- <%= msg %>
<% @signin.errors.full_messages.each do |msg| %>
<% end %>
<% end %>
<%= f.fields_for :contacts do |c| %>
<%= c.label :name %>
<%= c.text_field :name, placeholder: "Your name", class: "form-control" %>
<%= c.label :email %>
<%= c.text_field :email, placeholder: "Your email", class: "form-control" %>
<%= c.label :phone %>
<%= c.text_field :phone, placeholder: "Your phone", class: "form-control" %>
<% end %>
<%= f.submit 'Sign In', class: "btn btn-primary" %>
<% end %>
You probably just need to add contact_id: [] to the signin_params.
Need to check your migrations as well and make sure you have your associations set correctly.
Suggest reading the Rails Guides on has_many_through
https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
You should also rename signin to viewings or something else. I would say the vast majority of people reading your code would think that the signin controller is for users signing in and not for what it actually does.
Sorry for the formatting of my post, I’m on a mobile device.
Thanks Red, I've added the ID back in (mistakenly pulled it out as I was experimenting). I'll definitely reconsider my :signin model name as well. I am now running into an errors that says: Could not find the inverse association for contact (:signin in Contact). I'm wondering if this comes back to an incorrect Inverse_of declaration (Thoughtbot wrote on it here: https://thoughtbot.com/blog/accepts-nested-attributes-for-with-has-many-through). But I just can't seem to get it working.
Hey Seth,
You most likely don't need to use inverse_of, a correctly setup has_many_though association will give you access to things like Contact.signins or OpenHouses.contacts etc... Try:
Signin Model:
belongs_to :contact
belongs_to :open_house
Contact Model:
has_many :signins
has_many :open_houses, through: signins
OpenHouse Model:
has_many :signins
has_many :contacts, through: signins
Then check your signin migration, and makes sure you have:
t.belongs_to :contact, index: true
t.belongs_to :open_house, index: true
Let me know if you need any more help.
Red
Thanks Red, I finally found my problem and solved it! You are right, all the inverse_of stuff was unnecessary (I removed). Everything boiled down to my f.fields_for :contact line. I changed that to:
<%= f.fields_for :contact, Contact.new do |c| %>
...and voila, everything worked! I feel like the Contact instantiation should be in the controller though.. it's just that I can't seem to get the Contact instantiated from the :new action with everything I tried. 🤷🏼♂️
Is that the nested_attributes bit? Without seeing your code it's hard to say for sure, but you quite probably don't need to use nested_attributes, they'll just be adding unnecessary complexity.
I'm happy to look at your repo if you add redhendery to it on Github.
Hey Red, I am running into a new challenge that I thought you would understand well. Picking up the discussion above, now I would like to update my form action to create a new contact (if a contact doesn't exist ... as defined by the email field) or find and update that contact with the provided form details. Through some research, I discovered:
autosave_associated_records_for_
In my join model I now have:
def autosave_associated_records_for_lead
if new_lead = Lead.find_by_email(lead.email)
self.lead = new_lead
else
new_lead.save!
end
end
The problem I'm running into is that this doesn't update the contact, if the contact exists. Any chance you could help me solve that last problem?
In the signin_params method, you need to use contacts_attributes instead of contacts. Here's the corrected code:
private
def signin_params
params.require(:signin).permit(contacts_attributes: [:name, :email, :phone])
end
In the form, you need to use fields_for with the correct association. Since your Signin model belongs_to :contact, you should use fields_for :contact instead of :contacts. Here's the corrected code:
<%= form_with model: [@open_house, @open_house.signins.build], local: true do |f| %>
<% if @signin.errors.any? %>
<% @signin.errors.full_messages.each do |msg| %>
<%= msg %>
<% end %>
<% end %>
<%= f.fields_for :contact do |c| %>
<%= c.label :name %>
<%= c.text_field :name, placeholder: "Your name", class: "form-control" %>
<%= c.label :email %>
<%= c.text_field :email, placeholder: "Your email", class: "form-control" %>
<%= c.label :phone %>
<%= c.text_field :phone, placeholder: "Your phone", class: "form-control" %>
<% end %>
<%= f.submit 'Sign In', class: "btn btn-primary" %>
<% end %>