Forms That Handle One-to-Many Associations
In this demonstration, I will show how incorporate an association into the basic Rails resource pages and actions (i.e., index, show, new/create, edit/update, and destroy). We will continue to build upon the QuizMe project from the previous demos.
Recall from Figure 1 that our association now specifies that McQuestion objects belong to a particular parent Quiz object.
Figure 1. Model class design diagram showing the one-to-many association between Quiz and McQuestion. As per the association, each Quiz object has many McQuestion objects, and each McQuestion object belongs to one Quiz object.
As a consequence, several changes to our Rails resource pages and actions are necessitated. These changes will involve the following main tasks:
- Update the
showpage forQuizto display associatedMcQuestionobjects, as depicted in Figure 2. - Generate a
QuizMcQuestionsControllerto use for actions onMcQuestionrecords that require the ID of the parentQuizto be included in their routes. - Move the
indexcontroller actions forMcQuestionrecords into the newQuizMcQuestionsControllerclass with a new route that includes theQuizID in the URI. - Move the
newandcreatecontroller actions forMcQuestionrecords into the newQuizMcQuestionsControllerclass with new routes that include theQuizID in their URIs. - Update the existing
updateanddestroycontroller actions forMcQuestionrecords so that they redirect to the parentQuizobjectโsshowpage.

Figure 2. Updated show page for Quiz records that now has a โQuestionsโ subsection that displays the associated McQuestion objects.
1. Displaying Associated McQuestion Records on the show Page for Quiz Records
On the show page for a Quiz object, display the questions associated with that quiz by adding HTML.ERB code to the show.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 %>
Confirm that this code works correctly by running the app, opening the index page for Quiz records, and navigating to the show page for each Quiz record. The show pages should now include a โQuestionsโ subsection, as depicted in Figure 2.
2. Generating a QuizMcQuestionsController Class
Create a new controller QuizMcQuestionsController using this command:
rails g controller QuizMcQuestions
We will use this controller class, QuizMcQuestionsController, to handle HTTP requests that primarily act upon McQuestion records, include a parent Quiz ID in the resource path.
Confirm that the file app/controllers/quiz_mc_questions_controller.rb was generated correctly.
3. Moving the index Action from McQuestionsController to QuizMcQuestionsController
Replace the existing McQuestion route for index with a nested route, 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
Note that this route requires the parent Quiz ID in the resource path. The index route needs the parent ID, because it no longer makes sense to display all McQuestion objects, rather, we will group them by their parent Quiz object.
Comment out (or delete) the existing index action in McQuestionsController.
Add a new index action to QuizMcQuestionsController, 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
end
The includes method helps minimize the number of database queries by specifying the associations that need to be loaded (see the N+1 Queries Problem).
Move the index.html.erb view file from app/views/mc_questions to app/views/quiz_mc_questions.
Confirm that these changes work correctly by running the app and opening the URL http://localhost:3000/quizzes/1/mc_questions for the first Quiz record and http://localhost:3000/quizzes/1/mc_questions for the second Quiz record.
4. Moving new and create Actions from McQuestionsController to QuizMcQuestionsController
Replace the existing McQuestion routes for new, and create with nested routes, like this:
# 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
Note that these routes both require the parent Quiz ID in the resource path. The new and create routes need the parent Quiz ID, so the create controller action knows which Quiz object has the new McQuestion object.
Comment out (or delete) the existing new and create actions in McQuestionsController.
Add a new new action to QuizMcQuestionsController, 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
end
The call to build allocates in memory a new empty McQuestion object that is associated with the quiz; however, the McQuestion object is not yet saved to the database (and thus, for example, has an id that is nil).
Move the new.html.erb view file from app/views/mc_questions to app/views/quiz_mc_questions.
In new.html.erb, change the url argument for form_with, like this:
<%= form_with model: question, url: quiz_mc_questions_path(quiz), method: :post, local: true, scope: :mc_question do |form| %>
Add a new create action to QuizMcQuestionsController, 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|
format.html do
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
end
Update the โNew Questionโ link in quiz_mc_questions/index.html.erb and add a โNew Questionโ link to quizzes/show.html.erb (as per Figure 1), like this:
<%= link_to 'New Question', new_quiz_mc_question_path(quiz) %>
Confirm that these changes work correctly by resetting the database, running the app, navigating the various show pages for Quiz records, and using the โNew Questionโ link to add new McQuestion records.
5. Updating the McQuestion update and destroy Actions to Use the Parent Quiz
Modify the update action in McQuestionsController such that, on a successful save, the browser is redirected to the show page of the parent Quiz object, like this:
def update
# load existing object again from URL param
question = McQuestion.find(params[:id])
# respond_to block
respond_to do |format|
format.html do
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)
else
# error message
flash.now[:error] = 'Error: Question could not be updated'
# render edit
render :edit, locals: { question: question }
end
end
end
end
Change the destroy action in McQuestionsController such that, after the record is deleted, the browser is redirected to the show page of the parent Quiz object, 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|
format.html do
# success message
flash[:success] = 'Question removed successfully'
# redirect to index
redirect_to quiz_url(question.quiz)
end
end
end
Confirm that these changes work correctly by resetting the database, running the app, navigating the various show pages for Quiz records, and using the โ๐โ (edit) and โ๐โ (delete) links to update and delete some McQuestion records for each Quiz.
Having successfully completed the above tasks, the QuizMe app now provides users with a full set of features for CRUDing quizzes and questions that properly handle the association links between quizzes and questions.