There are a lot of question on stackoverflow about rails complex forms and they all seems to point to Ryan Bates' nice railscast demos on this subject: part1 and part2.
That's great for what it does, but I don't see questions addressing the situation where you may want to create new children objects, or you may want to associate objects that already exist.
In my case I want the user to be able to create a new Incident. As part of that they need to say who was involved in the incident. About half the time, the People being added to the Incident already exist in the db. In that case the user should be encouraged to use those existing records vs creating new ones.
Any suggestions on how to handle this complexity in the form?
And as part of the solution, do you recommend, that if the user doesn't find the person that the app goes ahead and creates it on the fly BEFORE the parent object gets submitted? My thinking (but interested in hearing recommendations) is that the user should use an existing Person record if there is one, but that if the user needs to create a NEW Person record, that this new record isn't submitted to the DB until the user submits the incident. Thoughts?
Hi to accomplish what you need to do, there are a few easy steps to follow:
1) In the model classes (Incident and Person classes in your case) make sure you put something like this:
class Incident < ActiveRecord::Base
has_and_belongs_to_many :people
# Note if you want to be able to remove a person from the Incident form (to de-associate) put true in :allow_destroy => true on the accepts_nested_attributes_for
accepts_nested_attributes_for :people, :allow_destroy => false, :reject_if => :all_blank
validates_associated :people #so you won't be able to save invalid people objects
attr_accessible :date_occur, :location, :people_attributes # note the :people_attributes here
#do your Incident validations as usual...
end
class Person < ActiveRecord::Base
has_and_belongs_to_many :incidents
#the following line it's to allow mass assignment, basically it will allow you to create people from the Incident form
attr_accessible :first_name, :last_name, :dob
#do your Person validations as usual...
end
2) In the view side, the easiest way will be modifying the Incident form file (app/view/incidents/_form.html.erb) to allow the user to assign existing and create new people to the Incident:
# app/view/incidents/_form.html.erb
<%= semantic_form_for(@incident) do |f| %>
<% if @incident.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@incident.errors.count, "error") %> prohibited this incident from being saved:</h2>
<ul>
<% @incident.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :date_occur %><br />
<%= f.datetime_select :date_occur %>
</div>
<div class="field">
<%= f.label :location %><br />
<%= f.text_field :location %>
</div>
<%= f.input :people, :as => :select, :collection=>Hash[Person.all.map { |p| [p.first_name + ' - ' + p.last_name, p.id] }] %>
<%= f.fields_for :people, Person.new() do |new_person_form| %>
<div class="incident-people new-person">
<%= new_person_form.inputs :name=>'Add New person' do %>
<%= new_person_form.input :first_name, :label=>'First Name: ' %>
<%= new_person_form.input :last_name, :label=>'Last Name: ' %>
<%= new_person_form.input :dob, :label=>'Date of birth: ' %>
<% end %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
3) Lastly, you need to modify your update and create methods for the Incident controller as follow:
# app/controllers/incident_controller.rb
def create
selected_people = params[:incident][:person_ids].keep_if{ |v| v.present? }
params[:incident].delete(:person_ids)
@incident = Incident.new(params[:incident])
@incident.people = Person.find( selected_people )
respond_to do |format|
if @incident.save
format.html { redirect_to @incident, notice: 'Incident was successfully created.' }
format.json { render json: @incident, status: :created, location: @incident }
else
format.html { render action: "new" }
format.json { render json: @incident.errors, status: :unprocessable_entity }
end
end
end
def update
selected_people = params[:incident][:person_ids].keep_if{ |v| v.present? }
params[:incident].delete(:person_ids)
@incident = Incident.find(params[:id])
@incident.people = Person.find( selected_people )
respond_to do |format|
if @incident.update_attributes(params[:incident])
format.html { redirect_to @incident, notice: 'Incident was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: @incident.errors, status: :unprocessable_entity }
end
end
end
And that's it, let me know if you need further help.
FedeX