Using form_with with a self-referential resource in Rails 5 - ruby-on-rails

I have a self-referential resource, Node, set up like this:
class Node < ApplicationRecord
belongs_to :parent, class_name: 'Node', foreign_key: 'node_id', optional: true
has_many :children, class_name: 'Node', foreign_key: 'node_id'
end
It works fine on the console, but I want to be able to navigate to a node and create children nodes from there, and I'm having trouble doing so.
For example, if I open /nodes/1, there should be a form there, and every node I create from that form should automatically populate node_id with the current node's id, 1.
How do I set up form_with and the nodes_controller so that it will allow me to achieve this?
The closest I got was by creating a special method (with the appropriate route):
def create_child
#node = Node.new(node_params)
#node.node_id = params[:id]
if #node.save
redirect_to node_path(params[:id])
end
end
private
def node_params
params.require(:node).permit(:name)
end
And then setting up the form like this:
<%= form_with url: "create_child" do |form| %>
<%= form.text_field :name %>
<%= form.submit %>
<% end %>
The request goes through, but then I get this error:
ActionController::ParameterMissing (param is missing or the value is empty: node)
But as far as I can tell the Node object should have been created in the create_child method.
Any ideas?

I've kind of solved it.
The form:
<%= form_with model: #node.children.build do |form| %>
<%= form.text_field :name %>
<%= form.hidden_field :node_id %>
<%= form.submit %>
<% end %>
And the controller:
def create
#node = Node.new(node_params)
if #node.save
redirect_to #node.parent
end
end
It's not perfect because it interferes with the displaying of the children nodes: if I put the form before the list of children, the list breaks. However, this is ok for my purposes, so I'll take it. Unless someone posts a better alternative I'll be accepting this as the answer.

Related

How do you persist an instantiated nested form object when creation fails?

I have models Software and Version. A Software has_many Version's and has_one :first_version
class Software < ApplicationRecord
has_many :versions
has_one :first_version, -> { order(created_at: :asc) },
class_name: "Version", dependent: :destroy
accepts_nested_attributes_for :versions
end
class Version < ApplicationRecord
belongs_to :software
end
I'm building the nested object in the new controller action.
class SoftwaresController < ApplicationController
def new
#software = current_account.softwares.build
#software.build_first_version
end
def create
#software = current_account.softwares.build(software_params)
if #software.save
redirect_to software_path(#software)
else
render :new
end
end
def software_params
params.require(:software).permit(
:name,
first_version_attributes: %i[id release_date],
)
end
end
form:
<%= simple_form_for :software do |f| %>
<%= f.input :name %>
<%= f.simple_fields_for :first_version do |v|%>
<%= v.input :release_date %>
<% end %>
<% end %>
With the above code, if something fails during creation, the nested object is persisted even though the object itself and it's parent do not have an id yet, and so errors are displayed under each field with invalid values.
At the same time, if I comment out the line where I build the nested object, the form does not break, just no nested fields are displayed. This is good.
Now, because the form is reused in the new and edit views and I don't want to let users edit the :first_version through this form nor rely on the view to render it conditionally if #software.new_record? I put the nested object in a global variable and point the nested form to that variable hoping that the same result will be achieved in the edit view because no global variable will exist.
def new
#software = current_account.softwares.build
#first_version = #software.build_first_version
end
form:
<%= simple_form_for :software do |f| %>
<%= f.input :name %>
<%= f.simple_fields_for #first_version do |v|%>
<%= v.input :release_date %>
<% end %>
<% end %>
Problem:
If something goes wrong during creation the object is no longer persisted and the view breaks due to #first_version being nil. So why is the nested object persisted when I do #parent.build_nested_object but not when #nested_object = #parent.build_nested_object ?
Solving the problem by creating more i_vars can lead to bugs. I think the best option is to disable the field based on a condition and change your view to the following.
<%= simple_form_for #software do |f| %>
<%= f.input :name %>
<%= f.simple_fields_for #software.first_version || #software.build_first_version do |v| %>
<%= v.input :release_date, disabled: (true if #software.first_version.id) %>
<% end %>
<% end %>
Using this view means that you can initialize only #software on your controller.
class SoftwaresController < ApplicationController
def new
#software = current_account.softwares.build
end
end

How to create triple nested objects in Rails with multiple-directional associations

An article called Triple nested Forms in Rails presents a good description of creating a form for saving three nested objects. The example given is of creating a Show that has_many Seasons, and each Season has_many Episodes. Also, Episode --> belongs_to --> Season --> belongs_to --> Show. 
Shows are created like this:
def new
#show = Show.new
#show.seasons.build.episodes.build
end
The form looks like this:
<%= form.fields_for :seasons do |s| %>
    <%= s.label :number %>
    <%= s.number_field :number %>    <%= s.fields_for :episodes do |e| %>
      <%= e.label :title %>
      <%= e.text_field :title %>
    <% end %> 
 <% end %>
<% end %>
This seems straightforward because all the associations run in one direction. I'm trying to do something that's similar, but more complicated. I have a Parent model where each Parent has multiple Children and each Child is enrolled in a School. After specifying that Children is the plural of Child, the association would have to be like this:
Parent has_many Children, accepts_nested_attributes_for :children
Child belongs_to Parent, belongs_to School, accepts_nested_attributes_for :school
School has_many Children, accepts_nested_attributes_for :children
Graphically, it would look like this:
Parent <-- belongs_to <-- Child --> belongs_to --> School
Each Parent is also associated with a User, like this:
User has_many :parents
The data on Parents, Children, and Schools is entered in the following form (generated using the Simple Form gem), where the schools are entered as a dropdown selector populated from the schools table:
#schools = School.all
<%= simple_form_for (#parent) do |f| %>
     <%= f.input :name, label: 'name' %>
     <%= f.simple_fields_for :children, #children do |child_form| %>
     <%= child_form.input :name, label: "Child Name" %>
         <%= child_form.simple_fields_for :school, #school do |school %>
     <%= school.collection_select :id, #schools, :id, :name, {}, {} %>
        <% end %>
     <% end %>
<% end %>
I set up the new controller method to create a Parent having three Children enrolled in an existing School. Then I tried to associate the Children with a School that already exists in the schools table with id = 1.
def new
#parent = Parent.new
# creating 3 children
#children = Array.new(3) {#parent.children.build}
#school = School.find(1)
#school.children.build
end
This throws an error 
Couldn't find School with ID=1 for Child with ID=
The error is located in the first line of the create method, which looks like this:
def create
#parent = Parent.new(parent_params.merge(:user => current_user))
if #parent.save
redirect_to root_path
else
render :new, :status => :unprocessable_entity
end
end
def parent_params
params.require(:parent).permit(:name, :child_attributes => [:id, :name, age, :school_attributes => [:id, :name]])
end
Since the error text states "Child with ID= ", the error must be thrown before ids for new Children are assigned. Why can a School with ID=1 not be found when it exists in the schools table? Or, does this mean that a School record has not been properly associated with an instance of Child before an attempt is made to save that instance? If so, how can I fix the association?
One of the most common missconceptions/misstakes with nested attributes is to think that you need it to do simple association assignment. You don't. You just need to pass an id to the assocation_name_id= setter.
If fact using nested attributes won't even do what you want at all. It won't create an assocation from an existing record when you do child.school_attributes = [{ id: 1 }] rather it will attempt to create a new record or update an existing school record.
You would only need to accept nested attributes for school if the user is creating a school at the same time. And in that case its probally a better idea to use Ajax rather than stuffing everything into one mega action.
<%= simple_form_for (#parent) do |f| %>
<%= f.input :name, label: 'name' %>
<%= f.simple_fields_for :children, #children do |child_form| %>
<%= child_form.input :name, label: "Child Name" %>
<%= child_form.associaton :school,
collection: #schools, label_method: :name %>
<% end %>
<% end %>
def parent_params
params.require(:parent).permit( :name,
child_attributes: [:id, :name, :age, :school_id]]
)
end

Nested Forms with find_or_create_by method

I've been stuck on this issue all day now and I'm fairly certain there is an easy fix that I am just not seeing due to my inexperience. A bit of background on what I'm trying to do before I discuss my problem. I have a model called Companies that can have many Locations. Similarly, a location can have multiple Companies. Because of this I created a has_many :through relationship.
class Company < ApplicationRecord
has_many :company_locations
has_many :locations, :through => :company_locations
accepts_nested_attributes_for :company_locations
accepts_nested_attributes_for :locations
end
class Location < ApplicationRecord
has_many :company_locations
has_many :companies, :through => :company_locations
end
class CompanyLocation < ApplicationRecord
belongs_to :company
belongs_to :location
end
Because of this structure, when a Company's location is created/updated I want to check whether this location (by name) exists. If it does, I use form the association between the Company and that Location. If it does not, the location is created and then the association is created. It is my understanding that the best way to do this is through a find_or_create_by method. However, the various ways I have tried do not seem to be creating this functionality.
Right now just to get something working my view for new Companies is this:
<h1> Add Company </h1>
<%= form_for :company, url: companies_path do |f| %>
<p>
<%= f.label :name %><br>
<%= f.text_field :name%>
</p>
<p>
<%= f.label :website %><br>
<%= f.text_field :website%>
</p>
<p>
<%= f.label :description %><br>
<%= f.text_area :description %>
</p>
<%=fields_for :locations do |location_form|%>
<%= location_form.label :name, 'Location' %>
<%= location_form.text_field :name %>
<%end%>
<p>
<%= f.submit %>
</p>
<% end %>
Now inside the create action in my Companies controller is where I am experiencing difficulties. As said before, I want to check if the location that is being added to the company already exists or not. Because of this, I am using a find_or_create_by method. However, I cannot seem to figure out how to properly handle the strong params/slice the params in a way to make this work without error.
def new
#company = Company.new
#company_locations = #company.company_locations.build
#location = #company_locations.build_location
end
def create
#company = Company.new(company_params)
#location = Location.find_or_create_by(name: (company_params.slice(:location_attributes[0][:name)))
#company.locations << #location
#company.save
redirect_to #company
end
private
def company_params
params.require(:company).permit(:name,:website, :description, location_attributes: [:name])
end
Currently, I'm getting an error saying 'no implicit conversion of Symbol into Integer' which leads me to believe that I am accessing the hash wrong, however, any other method I have tried results in the a Location being created with "NULL" set as the name. I'm really stumped on this one, and to completely honest I'm not sure I am approaching this nested form correctly. In the future, I hope to use JQuery/Javascript/Cocoon to be able to dynamically add fields in the form to add more locations at once. I've been trying to follow other Stack Overflow posts and forums to no avail. Any help/guidance is much appreciated! Thank you.
UPDATE
Still stuck on this one. Here is the params hash from the server log for an example company:
{"utf8"=>"✓",
"authenticity_token"=>"xpDOq6D5YRZ3VUXLBLgu8SfRIkRXgMQHXIRUtArNp1smtXShB/i54fQQVEHgqy64kdj1R+u0t/JVihLCXQVZpg==",
"company"=>{"name"=>"Google", "website"=>"www.google.com", "description"=>"Google is a search engine."},
"locations"=>{"name"=>"Mountain View"},
"commit"=>"Save Company"}
UPDATE 2
Params hash after Pavan's suggestions.
{"utf8"=>"✓",
"authenticity_token"=>"xJHZVYSfHmjR3BOMS49yRzwD35NV5F7uyCou8yOmtKAktGNfI57Gn1KZAgavnHIOigoIkOnQLRvBJGiFdG5KXQ==",
"company"=>{"name"=>"Logitech", "website"=>"www.logitech.com", "description"=>"This is logitech"},
"locations"=>{"name"=>"Chicago"},
"commit"=>"Save Company"}
UPDATE 3
Changed the form for tag to:
<%= form_for #company, url: companies_path do |f| %>
This is the new updated params hash:
{"utf8"=>"✓",
"authenticity_token"=>"UiiwNXzOiDqZd0Vv1dDu4jkyRQU4e9LKLixqKH+rvKCyDQo/289QzRoyVOUxw+6rjzuSBoRPoT8nIixeKGNCXQ==",
"company"=>{"name"=>"Seagate", "website"=>"www.seagate.com", "description"=>"This is seagate"},
"locations"=>{"name"=>"Los Angeles "},
"commit"=>"Create Company"}
I see a couple of mistakes
You need to build the associations correctly
def new
#company = Company.new
#company_locations = #company.company_locations.build
#location = #company.locations.build #since you have defined nested attributes in company
end
Change location_attributes to locations_attributes
def company_params
params.require(:company).permit(:name,:website, :description, locations_attributes: [:name])
end
And try using company_params.slice(:locations_attributes[0][:name])

Rails: How do I submit multiple objects into strong params?

I am making a goal tracking app. Right now outcome, purpose, action, priority, resources, and direction are all things which are part of Outcome in the database. However, I want to make purpose and action their own model objects. What I am confused about is how do I submit Outcome, Purpose, and Action, which will be 3 separate model objects, in a single HTTP request?
Should I just use multiple strong params in my controller?
app/view/outcomes/new.html.erb
You need to have model associations of outcomes with purpose and action.
Then you will need to create nested form. So that outform form can wrap purpose and action model attributes.
As you want to have different models for actions and purposes, I'm assuming outcome can has_many purposes and has_many actions. As per this type of association, below is the code you should have.
Your form will become something like:
<%= form_for #outcome do |f| %>
<%= f.label :outcome, "Outcome" %>
<%= f.text_area :outcome %>
<%= f.fields_for :purpose, #outcome.purpose.build do |p| %>
<%= p.text_area :desc, label: "Purpose" %>
<% end %>
<%= f.fields_for :action, #outcome.action.build do |p| %>
<%= p.text_area :desc, label: "Action" %>
<% end %>
<%= f.submit "submit" %>
<% end %>
Models:
# outcome.rb
has_many :purposes, :dependent => :destroy
has_many :actions, :dependent => :destroy
accepts_nested_attributes_of :purposes, :actions
-----------------------------------------
# purpose.rb
belongs_to :outcome
-----------------------------------------
# action.rb
belongs_to :outcome
Controller:
# outcomes_controller.rb
def outcome_params
params.require(:outcome).permit(:outcome, purpose_attributes:[:desc], action_attributes: [:desc])
end
SUGGESTION: You should rename your action model name to avoid unwanted conflicts with rails keyword action.
This may help you
Nestd Attributes
If the objects are associated (as below), you'll be best using the accepts_nested_attributes_for method:
#app/models/outcome.rb
Class Outcome < ActiveRecord::Base
has_many :purposes
has_many :actions
accepts_nested_attributes_for :purposes, :actions
end
#app/models/purpose.rb
Class Purpose < ActiveRecord::Base
belongs_to :outcome
end
#app/models/action.rb
Class Action < ActiveRecord::Base
belongs_to :outcome
end
accepts_nested_attributes_for means you'll be able to send the associated objects through the Outcome model - meaning you can send them all in a single HTTP request
You have to remember the way Rails is set up (MVC pattern), meaning if you send a single request; any further model objects you have will be able to be stored too.
Here's how you can set it up:
#app/controllers/outcomes_controller.rb
Class OutcomesController < ApplicationController
def new
#outcome = Outcome.new
#outcome.purposes.build
#outcoe.actions.build
end
def create
#outcome = Outcome.new(outcome_params)
#outcome.save
end
private
def outcome_params
params.require(:outcome).permit(:outcome, purpose_attributes:[:purpose], action_attributes: [:action])
end
end
Which will give you the ability to use this form:
#app/views/outcomes/new.html.erb
<%= form_for #outcome do |f| %>
<%= f.label :outcome %>
<%= f.text_area :outcome %>
<%= f.fields_for :purposes do |p| %>
<%= p.text_area :purpose %>
<% end %>
<%= f.fields_for :actions do |a| %>
<%= a.text_area :action %>
<% end %>
<%= f.submit %>
<% end %>
--
Recommendation
From the looks of it, I'd recommend you'll be able to keep all of these details in a single model - storing in multiple models seems overkill

How to get Rails build and fields_for to create only a new record and not include existing?

I am using build, fields_for, and accepts_nested_attributes_for to create a new registration note on the same form as a new registration (has many registration notes). Great.
Problem: On the edit form for the existing registration, I want another new registration note to be created, but I don't want to see a field for each of the existing registration notes.
I have this
class Registration < ActiveRecord::Base
attr_accessible :foo, :bar, :registration_notes_attributes
has_many :registration_notes
accepts_nested_attributes_for :registration_notes
end
and this
class RegistrationsController < ApplicationController
def edit
#registration = Registration.find(params[:id])
#registration.registration_notes.build
end
end
and in the view I am doing this:
<%= form_for #registration do |r| %>
<%= r.text_field :foo %>
<%= r.text_field :bar %>
<%= r.fields_for :registration_notes do |n| %>
<%= n.text_area :content %>
<% end %>
<% end %>
and it is creating a blank text area for a new registration note (good) and each existing registration note for that registration (no thank you).
Is there a way to only create a new note for that registration and leave the existing ones alone?
EDIT: My previous answer (see below) was bugging me because it's not very nice (it still loops through all the other registration_notes needlessly). After reading the API a bit more, the best way to get the behaviour the OP wanted is to replace:
<%= r.fields_for :registration_notes do |n| %>
with:
<%= r.fields_for :registration_notes, #registration.registration_notes.build do |n| %>
fields_for optionally takes a second parameter which is the specific object to pass to the builder (see the API), which is built inline. It's probably actually better to create and pass the new note in the controller instead of in the form though (just to move the logic out of the view).
Original answer (I was so close):
Just to clarify, you want your edit form to include a new nested registration note (and ignore any other existing ones)? I haven't tested this, but you should be able to do so by replacing:
<%= r.fields_for :registration_notes do |n| %>
with:
<%= r.fields_for #registration.registration_notes.build do |n| %>
EDIT: Okay, from a quick test of my own that doesn't work, but instead you can do:
<%= r.fields_for :registration_notes do |n| %>
<%= n.text_area :content if n.object.id.nil? %>
<% end %>
This will only add the text area if the id of the registration note is nil (ie. it hasn't been saved yet).
Also, I actually tested this first and it does work ;)
If you want to create a new registration form on your edit action, you can just instantiate a new registration_note object. Right now, your form is for the existing registration object.
I believe this is what you want:
class RegistrationsController < ApplicationController
def edit
#new_registration_note = RegistrationNote.new
#registration = Registration.find(params[:id])
#registration.registration_notes.build
end
end
In your view, you should pass a hidden param that references the registration record id:
<%= form_for #new_registration_note do |r| %>
<%= r.hidden_field :registration_id, :value => #registration.id %>
<%= r.text_area :content %>
<% end %>
Now, you can create your new registration note that belongs to #registration. Make sure you have a column in your registration_notes table to point to the registration. You can read more about associations here: http://guides.rubyonrails.org/association_basics.html
Thank you so much for your help as I said in my post the only problem with the approach from "Zaid Crouch"(I don't know how to make a reference to a user hehe) is that if the form has error fields the form will be clear and boom after the page reloading you'll have nothing filled in your form and can you imagine if you form is like 20 or 30 fields that would be a terrible user experience of course
Here is my solution that works with validation models:
class Registration < ActiveRecord::Base
attr_accessible :foo, :bar, :registration_notes_attributes
has_many :registration_notes
has_one :new_registration, class_name: 'RegistrationNote'
accepts_nested_attributes_for :new_registration
end
class RegistrationsController < ApplicationController
def edit
#registration = Registration.find(params[:id])
#registration.build_new_registration
end
end
<%= form_for #registration do |r| %>
<%= r.text_field :foo %>
<%= r.text_field :bar %>
<%= r.fields_for :new_registration do |n| %>
<%= n.text_area :content %>
<% end %>
<% end %>
I'm using simple_form in my example if you want to see the same working with validations and transaction take a look at the complete post here:
http://elh.mx/ruby/using-simple_form-for-nested-attributes-models-in-a-has_many-relation-for-only-new-records/
As Heriberto Perez correctly pointed out the solution in the most upvoted answer will simply discard everything if there's a validation error on one of the fields.
My approach is similar to Heriberto's but nevertheless a bit different:
Model:
class Registration < ActiveRecord::Base
has_many :registration_notes
accepts_nested_attributes_for :registration_notes
# Because 0 is never 1 this association will never return any records.
# Above all this association don't return any existing persisted records.
has_many :new_registration_notes, -> { where('0 = 1') }
, class_name: 'RegistrationNote'
accepts_nested_attributes_for :new_registration_notes
end
Controller:
class RegistrationsController < ApplicationController
before_action :set_registration
def edit
#registration.new_registration_notes.build
end
private
def set_registration
#registration = Registration.find(params[:id])
end
def new_registration_params
params.require(:registration).permit(new_registrations_attributes: [:content])
end
end
View:
<%= form_for #registration do |r| %>
<%= r.text_field :foo %>
<%= r.text_field :bar %>
<%= r.fields_for :new_registration_notes do |n| %>
<%= n.text_area :content %>
<% end %>
<% end %>

Resources