Rails nested forms building quiz

Viewed 88

I'm building a quiz and using nested forms to submit answers. I guess i'm looking on how to submit an array of nested forms?

I have the following setup:

Quiz

has_many :quiz_answers, dependent: :destroy
accepts_nested_attributes_for :quiz_answers, allow_destroy: true

Quiz Answer

belongs_to :quiz, optional: true

These next two create the Quiz question and options (ie. a, b, c, d) which are setup by an admin. This allows questions to be added and taken out as needed on the fly. This part works as it should and I have no issues here.

Quiz Question

has_many :quiz_options, dependent: :destroy
accepts_nested_attributes_for :quiz_options, allow_destroy: true

Quiz Option

belongs_to :quiz_question, optional: true

This issue I'm having is when a user begins a quiz, how can I iterate through each question and have a user submit an option, click next and go on to the next question until all are completed? So far I have it working in the sense it's saving one answer to the database, but it lists all questions:

class QuizzesController < ApplicationController
 def new
    @quiz = Quiz.new
    @quiz_question = QuizQuestion.where(active: true)
    @quiz.quiz_answers.build
  end
 
def create
    @quiz = Quiz.new(quiz_params)
   
    respond_to do |format|
      if @quiz.save
        format.html { redirect_to quiz_url(@quiz), notice: "Quiz was successfully created." }
        format.json { render :show, status: :created, location: @quiz }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @quiz.errors, status: :unprocessable_entity }
      end
    end
  end
end

Form

<%= form_with(model: quiz) do |form| %>
 <%= form.fields_for :quiz_answers do |builder| %>
  <%= render 'quiz_answer', form: builder %>
 <% end %>
 <%= form.submit "Submit", class: 'btn btn-primary' %>
<% end %>

Partial

<% @quiz.each do |question| %>
 <h2><%= question.question %></h2>
  <% question.quiz_options.each do |o| %>
   <%= form.radio_button :answer, o.answer %> <%= o.answer %>
  <% end %>
<% end %>

So in summary, I'm wondering if it's possible to have question 1, user submits answer, Question 2, user submits answers, etc. in a nested form environment all from an array of predetermined questions and answers?

1 Answers

You can do this like in code below:

The implementation is simple. You just mark each question as current step by step and validate only current question. In the view you hide all questions except current. When all questions have quiz_option_id you save whole quiz to the database.

I suppose that you have quiz_option_id column in your quiz_answers table if not, you have to add it to the table

add_reference :quiz_answers, :quiz_option, null: false, foreign_key: true
# controllers/quizzes_controller.rb

class QuizzesController < ApplicationController
  def new
    @quiz = Quiz.new
    @quiz_questions = QuizQuestion.where(active: true)
    @quiz_questions.each.with_index do |question, i|
      @quiz.quiz_answers.build(quiz_question_id: question.id, is_current: i.zero?)
    end
  end

  def create
    @quiz = Quiz.new(quiz_params)

    respond_to do |format|
      if @quiz.valid?
        if @quiz.quiz_answers.all?(&-> (answer) { answer.quiz_option_id.present? })
          @quiz.save
          format.html { redirect_to quiz_url(@quiz), notice: "Quiz was successfully created." }
          format.json { render :show, status: :created, location: @quiz }
        else
          new_current = @quiz.quiz_answers.detect(&-> (answer) { !ActiveModel::Type::Boolean.new.cast(answer.is_current) && answer.quiz_option_id.blank? })
          current_active = @quiz.quiz_answers.detect(&-> (answer) { ActiveModel::Type::Boolean.new.cast(answer.is_current) })
          current_active.is_current = false
          new_current.is_current = true

          format.html { render :new, status: :unprocessable_entity }
          format.json { render json: @quiz.errors, status: :unprocessable_entity }
        end
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @quiz.errors, status: :unprocessable_entity }
      end
    end
  end

  private

  def quiz_params
    params.require(:quiz).permit(quiz_answers_attributes: %i[is_current quiz_question_id quiz_option_id])
  end
end
# models/quiz_answer.rb

class QuizAnswer < ApplicationRecord
  attr_accessor :quiz_question_id, :is_current

  belongs_to :quiz, optional: true
  belongs_to :quiz_option, optional: true

  validates :quiz_option_id, presence: true, if: -> { ActiveModel::Type::Boolean.new.cast(is_current) }

  def quiz_question
    @quiz_question ||= QuizQuestion.find_by(id: quiz_question_id)
  end
end

Class d-none is from Bootstrap or you can define your own

.d-none {
  display: none;
}
# views/quizzes/_quiz_answer.html.erb

<div class="<%= ActiveModel::Type::Boolean.new.cast(form.object.is_current) ? "" : "d-none" %>">
  <h2><%= form.object.quiz_question.question %></h2>
  <%= form.hidden_field :quiz_question_id %>
  <%= form.hidden_field :is_current %>
  <%= form.collection_radio_buttons :quiz_option_id, form.object.quiz_question.quiz_options, :id, :answer %>
  <%= form.object.errors.full_messages.first %>
  <br/>
</div>

Also I suppose that in new.html.erb you have something file this:

# views/quizzes/new.html.erb

<%= render "form", quiz: @quiz %>

If you have any questions feel free to ask

Related