Search code examples
ruby-on-railsvalidationmany-to-manysimple-form

Simple_form_for many to many with validation


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

Solution

  • 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.