Simple_form_for many to many with validation

Viewed 46

Setup

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
1 Answers

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.

Related