Demo 12: Forms That Handle One-to-Many Associations
In this demonstration, I will show how incorporate an association into the basic CRUD resource pages and actions we have discussed previously (i.e., index, show, new/create, edit/update, and destroy). We will continue to build upon the QuizMe project from the previous demos.
In particular, because our association now specifies that McQuestion objects belong to a particular Quiz object, several changes to our CRUD pages and actions are necessitated. These changes will involve three main tasks:
- Update the
showpage forQuizto display associatedMcQuestionobjects, as depicted in Figure 1. - Move
index,new, andcreateactions into a newQuizMcQuestionsControllerclass with new routes that include theQuizID in the URI. - Update the existing
updateanddestroyactions to, for example, redirect to the parentQuiz’sshowpage (instead of theMcQuestionindexpage).
show page for Quiz records that now has a "Questions" subsection that displays the associated McQuestion objects.As we perform the tasks below, don’t forget, after you finish each page, to run the page so as to “fail fast”, catching and fixing bugs quickly when it’s easier to find and understand them.
1. Displaying Associated Records on a Model’s show Page
-
On the
showpage for aQuizobject, display the questions associated with that quiz by adding HTML.ERB code to theshow.html.erb, like this:<h2>Questions</h2> <% quiz.mc_questions.each do |question| %> <div id="<%= dom_id(question) %>"> <p> <%= question.question %> <%= link_to '🔎', mc_question_path(question) %> <%= link_to '🖋', edit_mc_question_path(question) %> <%= link_to '🗑', mc_question_path(question), method: :delete %> </p> <% choices = [question.answer, question.distractor_1] choices << question.distractor_2 if !question.distractor_2.blank? choices.each do |c| %> <div> <%= radio_button_tag "guess_#{question.id}", c, checked = c == question.answer, disabled: true %> <%= label_tag "guess_#{question.id}_#{c}", c %> </div> <% end %> </div> <% end %>
2. Moving index, new, and create Actions from McQuestion into QuizMcQuestionsController
-
Create a new controller
QuizMcQuestionsControllerusing this command:rails g controller QuizMcQuestions -
Replace the existing
McQuestionroutes forindex,new, andshowwith nested routes, like this:# get 'mc_questions', to: 'mc_questions#index', as: 'mc_questions' # index get 'quizzes/:id/mc_questions', to: 'quiz_mc_questions#index', as: 'quiz_mc_questions' # nested index # get 'mc_questions/new', to: 'mc_questions#new', as: 'new_mc_question' # new get 'quizzes/:id/mc_questions/new', to: 'quiz_mc_questions#new', as: 'new_quiz_mc_question' # nested new # post 'mc_questions', to: 'mc_questions#create' # create post 'quizzes/:id/mc_questions', to: 'quiz_mc_questions#create' # nested create -
Move the
index.html.erbandnew.html.erbview files fromapp/views/mc_questionstoapp/views/quiz_mc_questions. -
Comment out (or delete) the existing
index,new, andcreateactions inMcQuestionsController. -
Add a new
indexaction toQuizMcQuestionsController, like this:def index quiz = Quiz.includes(:mc_questions).find(params[:id]) respond_to do |format| format.html { render :index, locals: { quiz: quiz, questions: quiz.mc_questions } } end endThe
includesmethod helps minimize the number of database queries by specifying the associations that need to be loaded (see the N+1 Queries Problem). -
Update the “
New Question” link inquiz_mc_questions/index.html.erband add a “New Question” link toquizzes/show.html.erb(as per Figure 1), like this:<%= link_to 'New Question', new_quiz_mc_question_path(quiz) %> -
Add a
newaction toQuizMcQuestionsController, like this:def new quiz = Quiz.find(params[:id]) respond_to do |format| format.html { render :new, locals: { quiz: quiz, question: quiz.mc_questions.build } } end endThe call to
buildallocates in memory a new emptyMcQuestionobject that is associated with thequiz; however, theMcQuestionobject is not yet saved to the database (and thus, for example, has anidthat isnil). -
In
new.html.erb, change theurlargument forform_with, like this:<%= form_with model: question, url: quiz_mc_questions_path(quiz), method: :post, local: true, scope: :mc_question do |form| %> -
Add a
createaction toQuizMcQuestionsController, like this::def create # find the quiz to which the new question will be added quiz = Quiz.find(params[:id]) # allocate a new question associated with the quiz question = quiz.mc_questions.build(params.require(:mc_question).permit(:question, :answer, :distractor_1, :distractor_2)) # respond_to block respond_to do |format| # html format block format.html { if question.save # success message flash[:success] = "Question saved successfully" # redirect to index redirect_to quiz_mc_questions_url(quiz) else # error message flash.now[:error] = "Error: Question could not be saved" # render new render :new, locals: { quiz: quiz, question: question } end } end end
3. Updating McQuestion update and destroy Actions to Use Parent Quiz
-
Modify the
updateaction inMcQuestionsControllerto permitquiz_idand redirect to theQuizshowpage, like this:def update # load existing object again from URL param question = McQuestion.find(params[:id]) # respond_to block respond_to do |format| # html format block format.html { # if question updates with permitted params if question.update(params.require(:mc_question).permit(:question, :answer, :distractor_1, :distractor_2)) # success message flash[:success] = 'Question updated successfully' # redirect to index redirect_to quiz_url(question.quiz_id) else # error message flash.now[:error] = 'Error: Question could not be updated' # render edit render :edit, locals: { question: question } end } end end -
Change the
destroyaction inMcQuestionsControllerto redirect to theQuizshowpage, like this:def destroy # load existing object again from URL param question = McQuestion.find(params[:id]) # destroy object question.destroy # respond_to block respond_to do |format| # html format block format.html { # success message flash[:success] = 'Question removed successfully' # redirect to index redirect_to quiz_url(question.quiz_id) } end end
Having successfully completed the above tasks, the QuizMe app now provides users with a full set of features for CRUDing quizzes and questions.