I have a simple many to many relationship between a Submit and an Answer through SubmitAnswer.
Answers are grouped by a Question (in my case each question has three answers) - think of it as a multiple choice quiz.
I have been trying to use SimpleFormFor to make a form which renders a predetermined set of questions, where each question has a predetermined set of answers.
Something like this:
#form
<%= simple_form_for Submit.new, url: "/questionnaire" do |f| %>
<% @questions.each do |question| %>
<%= f.association :answers, collection: question.answers %>
<% end %>
<%= f.submit :done %>
<% end %>
#controller
def create
@submit = Submit.new(submit_params)
@submit.user = current_user
if @submit.save
redirect_to root_path
else
render :new
end
end
def submit_params
params.require(:submit).permit(answer_ids: [])
end
When I submit the form, Rails creates the join table, SubmitAnswers, automatically.
So here is the crux of the matter: Whats the easiest way to re-render the form, errors and all, if not all questions have been answered, ie if @submit.answers.length != @question.length
?
I can add a custom error with errors.add(:answers, 'error here')
, but when I re-render, the correctly selected answers arent repopulated, which is suboptimal.
For completions sacke, here are my models:
class Submit < ApplicationRecord
belongs_to :user
has_many :submit_answers
has_many :answers, through: :submit_answers
end
class SubmitAnswer < ApplicationRecord
belongs_to :submit
belongs_to :answer
end
class Answer < ApplicationRecord
has_many :submit_answers
has_many :submits, through: :submit_answers
end
Alright, after some digging we did find the answer to make the form work, albeit with more pain that we anticipated a simple many-to-many should take.
#model
class Submit < ApplicationRecord
belongs_to :user
has_many :submit_answers
has_many :answers, through: :submit_answers
accepts_nested_attributes_for :submit_answers
end
#controller
def new
@submit = Submit.new
@questions.count.times { @submit.submit_answers.build }
end
def create
@submit = Submit.new(submit_params)
@submit.user = current_user
if @submit.save
redirect_to root_path
else
render :home
end
end
def submit_params
params.require(:submit).permit(submit_answers_attributes:[:answer_id])
end
#form
<%= simple_form_for @submit do |f| %>
<%= f.simple_fields_for :submit_answers do |sa| %>
<%= sa.input :answer_id, collection: @answers[sa.options[:child_index]], input_html: { class: "#{'is-invalid' if sa.object.errors.any?}"}, label: @questions[sa.options[:child_index]].name %>
<div class="invalid-feedback d-block">
<ul>
<% sa.object.errors.full_messages.each do |msg| %>
<li> <%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= f.submit :done %>
<% end %>
The solution is to use simple_fields_for
/fields_for
. Note that <%= sa.input :answer_id %>
must be :answer_id, not :answer, which is something I had tried before.
Also one must allow accepts_nested_attributes_for :submit_answers
, where :submit_answers is the join_table.
I prebuild my SubmitAnswers like so: @questions.count.times { @submit.submit_answers.build }
which generates an input field for each question, all of which get saved on the form submit, a la build.
For the strong_params one needs to permit the incoming ids:
params.require(:submit).permit(submit_answers_attributes:[:answer_id])
, so in this case submit_answers_attributes:[:answer_id].
For anyone wondering what the params look like:
{"authenticity_token"=>"[FILTERED]",
"submit"=>
{"submit_answers_attributes"=>
{"0"=>{"answer_id"=>""}, "1"=>{"answer_id"=>""}, "2"=>{"answer_id"=>""}, "3"=>{"answer_id"=>""}, "4"=>{"answer_id"=>""}, "5"=>{"answer_id"=>""}, "6"=>{"answer_id"=>""}}},
"commit"=>"done"}
As for the errors, im sure there might be a better way, but for now I have just manually added them with input_html: { class: "#{'is-invalid' if sa.object.errors.any?}"}
.
On a final note, the sa.object # => SubmitAnswer
allows me to retrieve the Model, the errors of that Model or whatever else one might want.