Nested-Resource Pages
In this demonstration, I will show how to create pages and user-interface features for CRUDing model classes with a has-many/belongs-to one-to-many association. In particular, we will be building upon an app that already has the following (all covered in prior demos):
- A parent model class and a child model class with a has-many/belongs-to one-to-many association.
- Pages and UI features for CRUDing parent objects, including:
- An index page for the parent model objects.
- A show pages for each parent model object.
- A new/create form for creating and saving parent objects to the database.
- An edit/update form for saving changes to existing parent objects.
- A destroy action for deleting parent objects.
To this app, we will integrate pages and features for CRUDing child objects that belong to a parent. Because each child object belongs to a parent object, the child objects will be treated as nested resources within parent objects.
General Steps
In general, the steps for creating pages and UI features for child model objects in a has-many/belongs-to one-to-many model association are as follows. Each of these steps is similar to a prior demo on resource-CRUDing pages and UI features, except that the pages and UI features must now CRUD a nested resource.
-
Step 1 Create an Index Page for the Child Objects That Belong to Each Parent. This step (similar to the Index Pages demo) creates the route, action, and view template needed to make an index page that displays the child model objects that belong to a particular parent model object. Links to the index pages will also be added (generally on the parent object’s show page).
-
Step 2 Create a Show Page for Each Child Object. This step (similar to the Show Pages demo) creates the route, action, and view template needed to make a show page for each child model object. Links to these show pages will also be added (generally on the relevant child index page).
-
Step 3 Create a New/Create Form That Makes a New Child Object That Belongs to a Particular Parent. This step (similar to the New/Create Forms demo) creates the routes, actions, and view needed to make a new/create form that creates a child object that belongs to a particular parent model object. Links to the form pages will also be added (generally on the relevant child index page).
-
Step 4 Create an Edit/Update Form for Modifying Existing Child Objects. This step (similar to the Edit/Update Forms demo) creates the routes, actions, and view template needed to make an edit/update form for an existing child model object. Links to the form pages will also be added (generally to the relevant child index page and show page).
-
Step 5 Create a Destroy Action for Deleting Existing Child Objects. This step (similar to the Destroy Actions demo) creates the route, action, and view code needed to make a destroy action deleting an existing child model object. A link to destroy each child object will be added to the relevant child index page.
Creating Pages and UI Feature for CRUDing Quiz Questions
To demonstrate the steps for creating resource-CRUDing pages and UI features for a nested resource, we will continue to build upon the partially implemented demo app (demo-one-to-many-associations
branch) from the previous demo. Recall that the app is for authoring quizzes with multiple-choice questions, and it includes a parent Quiz
model class and a child Question
model class to which we added a one-to-many association, as depicted in Figure 1.
Additionally, this app already has the standard resource-CRUDing pages and UI features for the parent Quiz
model class, including an index page, show pages, a new/create form, an edit/update form, and a destroy action.
To these pages and UI features, we will be adding similar ones for the nested Question
resource. A key difference between the nested-resource actions and the standard ones is that the URL resource paths for the nested-resource actions all include the ID of the parent object. To clarify, here are some examples of the nested-resource pages.
As Figure 2 shows, we will add to each Quiz
show page a link to an index page of the Question
objects that belong to that Quiz
object.
Figure 3 depicts the index page for a nested Question
resource. Note how the page is related to its parent Quiz
object. It includes the parent object’s ID in its URL resource path, and it displays only Question
objects that belong to the parent Quiz
object.
Figure 4 depicts the new/create form for creating a child Question
object. Note that this form is always linked to from an index page of Question
objects that belong to a particular Quiz
object (e.g., as in Figure 3). That link will include the parent Quiz
object’s ID in the URL resource path of the form (e.g., as in Figure 4). The parent object’s ID will also be included in the URL resource path of the POST request sent by submitting the form. The inclusion of the parent’s ID is important so that when the new child object is created, the controller action will know which parent object the child should belong to.
Figure 5 depicts the show page for a child Question
object. Again, knowledge of the parent object is present in the URL resource path and also in the link back to the index page of Question
objects that belong to the parent.
Finally, Figure 6 depicts the edit/update page for a child Question
object, which like the above pages, includes the ID of the parent Quiz
object in the URL resource path.
Because the steps for creating these pages and features overlap heavily with the prior demos, the description of each step will be terse, leaving it to the reader to note the differences with nested resources.
Step 1 Create an Index Page for the Child Objects That Belong to Each Parent
Substep Add a nested index route for child model objects.
config/routes.rb
Rails.application.routes.draw do
root to: redirect('/quizzes')
get 'quizzes', to: 'quizzes#index', as: 'quizzes'
post 'quizzes', to: 'quizzes#create'
get 'quizzes/new', to: 'quizzes#new', as: 'new_quiz'
get 'quizzes/:id', to: 'quizzes#show', as: 'quiz'
patch 'quizzes/:id', to: 'quizzes#update'
delete 'quizzes/:id', to: 'quizzes#destroy'
get 'quizzes/:id/edit', to: 'quizzes#edit', as: 'edit_quiz'
get 'quizzes/:quiz_id/questions', to: 'questions#index', as: 'quiz_questions'
end
Substep Create a model controller for child model objects.
From the command line:
rails generate controller Questions
Substep Add an index action to the child model controller.
app/controllers/questions_controller.rb
class QuestionsController < ApplicationController
def index
@quiz = Quiz.find(params[:quiz_id])
@questions = @quiz.questions
render :index
end
end
Substep Create an index view template for child model objects.
app/views/questions/index.html.erb
<h1>Questions</h1>
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Question</th>
</tr>
</thead>
<tbody>
<% @questions.each do |question| %>
<tr>
<td><%= question.question %></td>
</tr>
<% end %>
</tbody>
</table>
<p>
<%= link_to 'Quiz', quiz_path(@quiz), class: 'btn btn-outline-secondary' %>
</p>
Substep Add links to the index pages for child model objects.
app/views/quizzes/show.html.erb
<h1><%= @quiz.title %></h1>
<p>
<%= @quiz.description %>
</p>
<p>
<%= link_to 'Questions', quiz_questions_path(@quiz), class: 'btn btn-secondary' %>
</p>
<p>
<%= link_to 'Home', root_path, class: 'btn btn-outline-secondary' %>
</p>
Test It!
To verify that we made this change correctly, we run the web app (as per the steps in the running apps demo), we open the URL http://localhost:3000/ in our web browser, and navigate to the various child index pages. The pages displayed should look similar to Figure 3, except without the show/edit/destroy links and without the new-question link.
Step 2 Create a Show Page for Each Child Object
Substep Add a nested show route for child model objects.
config/routes.rb
Rails.application.routes.draw do
root to: redirect('/quizzes')
get 'quizzes', to: 'quizzes#index', as: 'quizzes'
post 'quizzes', to: 'quizzes#create'
get 'quizzes/new', to: 'quizzes#new', as: 'new_quiz'
get 'quizzes/:id', to: 'quizzes#show', as: 'quiz'
patch 'quizzes/:id', to: 'quizzes#update'
delete 'quizzes/:id', to: 'quizzes#destroy'
get 'quizzes/:id/edit', to: 'quizzes#edit', as: 'edit_quiz'
get 'quizzes/:quiz_id/questions', to: 'questions#index', as: 'quiz_questions'
get '/quizzes/:quiz_id/questions/:id', to: 'questions#show', as: 'quiz_question'
end
Substep Add a show action to the child model controller.
app/controllers/questions_controller.rb
class QuestionsController < ApplicationController
def index
@quiz = Quiz.find(params[:quiz_id])
@questions = @quiz.questions
render :index
end
def show
@quiz = Quiz.find(params[:quiz_id])
@question = @quiz.questions.find(params[:id])
render :show
end
end
Substep Create a show view template for child model objects.
app/views/questions/show.html.erb
<h1><%= @question.question %></h1>
<ul>
<li class="text-success">
<%= @question.answer %>
</li>
<li class="text-danger">
<%= @question.distractor_1 %>
</li>
<li class="text-danger">
<%= @question.distractor_2 %>
</li>
</ul>
<p>
<%= link_to 'Back to Questions', quiz_questions_path(@quiz), class: 'btn btn-outline-secondary' %>
</p>
Substep Add links to the show pages for child model objects.
app/views/questions/index.html.erb
<h1>Questions</h1>
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Question</th>
<th></th>
</tr>
</thead>
<tbody>
<% @questions.each do |question| %>
<tr>
<td><%= question.question %></td>
<td class="text-nowrap">
<%= link_to 'show', quiz_question_path(@quiz, question), class: 'btn btn-outline-secondary btn-sm' %>
</td>
</tr>
<% end %>
</tbody>
</table>
<p>
<%= link_to 'Quiz', quiz_path(@quiz), class: 'btn btn-outline-secondary' %>
</p>
Test It!
To verify that we made this change correctly, we run the web app (as per the steps in the running apps demo), we open the URL http://localhost:3000/ in our web browser, and navigate to the various child show pages. The pages displayed should look similar to Figure 5.
Step 3 Create a New/Create Form That Makes a New Child Object That Belongs to a Particular Parent
Substep Add a nested new-resource route for child model objects.
config/routes.rb
Rails.application.routes.draw do
root to: redirect('/quizzes')
get 'quizzes', to: 'quizzes#index', as: 'quizzes'
post 'quizzes', to: 'quizzes#create'
get 'quizzes/new', to: 'quizzes#new', as: 'new_quiz'
get 'quizzes/:id', to: 'quizzes#show', as: 'quiz'
patch 'quizzes/:id', to: 'quizzes#update'
delete 'quizzes/:id', to: 'quizzes#destroy'
get 'quizzes/:id/edit', to: 'quizzes#edit', as: 'edit_quiz'
get 'quizzes/:quiz_id/questions', to: 'questions#index', as: 'quiz_questions'
get 'quizzes/:quiz_id/questions/new', to: 'questions#new', as: 'new_quiz_question'
get '/quizzes/:quiz_id/questions/:id', to: 'questions#show', as: 'quiz_question'
end
Substep Add a new-resource action to the child model controller.
app/controllers/questions_controller.rb
class QuestionsController < ApplicationController
def index
@quiz = Quiz.find(params[:quiz_id])
@questions = @quiz.questions
render :index
end
def show
@quiz = Quiz.find(params[:quiz_id])
@question = @quiz.questions.find(params[:id])
render :show
end
def new
@quiz = Quiz.find(params[:quiz_id])
@question = Question.new
render :new
end
end
Substep Create a new-resource view template for child model objects.
app/views/questions/new.html.erb
<h1>Add New Question</h1>
<%= bootstrap_form_with model: @question, method: :post, url: quiz_questions_path(@quiz), local: true do |f| %>
<%= f.text_field :question %>
<%= f.text_field :answer %>
<%= f.text_field :distractor_1 %>
<%= f.text_field :distractor_2 %>
<%= f.submit %>
<% end %>
<p class="mt-3">
<%= link_to 'Cancel', quiz_questions_path(@quiz), class: 'btn btn-outline-secondary' %>
</p>
Substep Add links to the new-resource pages for child model objects.
app/views/questions/index.html.erb
<h1>Questions</h1>
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Question</th>
<th></th>
</tr>
</thead>
<tbody>
<% @questions.each do |question| %>
<tr>
<td><%= question.question %></td>
<td class="text-nowrap">
<%= link_to 'show', quiz_question_path(@quiz, question), class: 'btn btn-outline-secondary btn-sm' %>
</td>
</tr>
<% end %>
</tbody>
</table>
<p>
<%= link_to "New Question", new_quiz_question_path(@quiz), class: "btn btn-secondary"%>
</p>
<p>
<%= link_to 'Quiz', quiz_path(@quiz), class: 'btn btn-outline-secondary' %>
</p>
Substep Add a nested create-resource route for child model objects.
config/routes.rb
Rails.application.routes.draw do
root to: redirect('/quizzes')
get 'quizzes', to: 'quizzes#index', as: 'quizzes'
post 'quizzes', to: 'quizzes#create'
get 'quizzes/new', to: 'quizzes#new', as: 'new_quiz'
get 'quizzes/:id', to: 'quizzes#show', as: 'quiz'
patch 'quizzes/:id', to: 'quizzes#update'
delete 'quizzes/:id', to: 'quizzes#destroy'
get 'quizzes/:id/edit', to: 'quizzes#edit', as: 'edit_quiz'
get 'quizzes/:quiz_id/questions', to: 'questions#index', as: 'quiz_questions'
post 'quizzes/:quiz_id/questions', to: 'questions#create'
get 'quizzes/:quiz_id/questions/new', to: 'questions#new', as: 'new_quiz_question'
get '/quizzes/:quiz_id/questions/:id', to: 'questions#show', as: 'quiz_question'
end
Substep Add a create-resource action to the child model controller.
app/controllers/questions_controller.rb
class QuestionsController < ApplicationController
def index
@quiz = Quiz.find(params[:quiz_id])
@questions = @quiz.questions
render :index
end
def show
@quiz = Quiz.find(params[:quiz_id])
@question = @quiz.questions.find(params[:id])
render :show
end
def new
@quiz = Quiz.find(params[:quiz_id])
@question = Question.new
render :new
end
def create
@quiz = Quiz.find(params[:quiz_id])
@question = @quiz.questions.build(params.require(:question).permit(:question, :answer, :distractor_1, :distractor_2))
if @question.save
flash[:success] = "Question saved successfully"
redirect_to quiz_questions_url(@quiz)
else
flash.now[:error] = "Question could not be saved"
render :new
end
end
end
Test It!
To verify that we made this change correctly, we run the web app (as per the steps in the running apps demo), we open the URL http://localhost:3000/ in our web browser, and navigate to the various child new/create forms. The forms displayed should look similar to Figure 4.
Step 4 Create an Edit/Update Form for Modifying Existing Child Objects
Substep Add a nested edit-resource route for child model objects.
config/routes.rb
Rails.application.routes.draw do
root to: redirect('/quizzes')
get 'quizzes', to: 'quizzes#index', as: 'quizzes'
post 'quizzes', to: 'quizzes#create'
get 'quizzes/new', to: 'quizzes#new', as: 'new_quiz'
get 'quizzes/:id', to: 'quizzes#show', as: 'quiz'
patch 'quizzes/:id', to: 'quizzes#update'
delete 'quizzes/:id', to: 'quizzes#destroy'
get 'quizzes/:id/edit', to: 'quizzes#edit', as: 'edit_quiz'
get 'quizzes/:quiz_id/questions', to: 'questions#index', as: 'quiz_questions'
post 'quizzes/:quiz_id/questions', to: 'questions#create'
get 'quizzes/:quiz_id/questions/new', to: 'questions#new', as: 'new_quiz_question'
get '/quizzes/:quiz_id/questions/:id', to: 'questions#show', as: 'quiz_question'
get '/quizzes/:quiz_id/questions/:id/edit', to: 'questions#edit', as: 'edit_quiz_question'
end
Substep Add a edit-resource action to the child model controller.
app/controllers/questions_controller.rb
class QuestionsController < ApplicationController
def index
…
end
def show
…
end
def new
…
end
def create
…
end
def edit
@quiz = Quiz.find(params[:quiz_id])
@question = @quiz.questions.find(params[:id])
render :edit
end
end
Substep Create a edit-resource view template for child model objects.
app/views/questions/edit.html.erb
<h1>Edit Question</h1>
<%= bootstrap_form_with model: @question, method: :patch, url: quiz_question_path(@quiz, @question), local: true do |f| %>
<%= f.text_field :question %>
<%= f.text_field :answer %>
<%= f.text_field :distractor_1 %>
<%= f.text_field :distractor_2 %>
<%= f.submit %>
<% end %>
<p class="mt-3">
<%= link_to 'Cancel', quiz_questions_path(@quiz), class: 'btn btn-outline-secondary' %>
</p>
Substep Add links to the edit-resource pages for child model objects.
app/views/questions/index.html.erb
<h1>Questions</h1>
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Question</th>
<th></th>
</tr>
</thead>
<tbody>
<% @questions.each do |question| %>
<tr>
<td><%= question.question %></td>
<td class="text-nowrap">
<%= link_to 'show', quiz_question_path(@quiz, question), class: 'btn btn-outline-secondary btn-sm' %>
<%= link_to 'edit', edit_quiz_question_path(@quiz, question), class: 'btn btn-outline-secondary btn-sm' %>
</td>
</tr>
<% end %>
</tbody>
</table>
<p>
<%= link_to "New Question", new_quiz_question_path(@quiz), class: "btn btn-secondary"%>
</p>
<p>
<%= link_to 'Quiz', quiz_path(@quiz), class: 'btn btn-outline-secondary' %>
</p>
Substep Add a nested update-resource routes for child model objects.
config/routes.rb
Rails.application.routes.draw do
root to: redirect('/quizzes')
get 'quizzes', to: 'quizzes#index', as: 'quizzes'
post 'quizzes', to: 'quizzes#create'
get 'quizzes/new', to: 'quizzes#new', as: 'new_quiz'
get 'quizzes/:id', to: 'quizzes#show', as: 'quiz'
patch 'quizzes/:id', to: 'quizzes#update'
delete 'quizzes/:id', to: 'quizzes#destroy'
get 'quizzes/:id/edit', to: 'quizzes#edit', as: 'edit_quiz'
get 'quizzes/:quiz_id/questions', to: 'questions#index', as: 'quiz_questions'
post 'quizzes/:quiz_id/questions', to: 'questions#create'
get 'quizzes/:quiz_id/questions/new', to: 'questions#new', as: 'new_quiz_question'
get '/quizzes/:quiz_id/questions/:id', to: 'questions#show', as: 'quiz_question'
patch '/quizzes/:quiz_id/questions/:id', to: 'questions#update'
put '/quizzes/:quiz_id/questions/:id', to: 'questions#update'
get '/quizzes/:quiz_id/questions/:id/edit', to: 'questions#edit', as: 'edit_quiz_question'
end
Substep Add a update-resource action to the child model controller.
app/controllers/questions_controller.rb
class QuestionsController < ApplicationController
def index
…
end
def show
…
end
def new
…
end
def create
…
end
def edit
…
end
def update
@quiz = Quiz.find(params[:quiz_id])
@question = @quiz.questions.find(params[:id])
if @question.update(params.require(:question).permit(:question, :answer, :distractor_1, :distractor_2))
flash[:success] = "Question updated successfully"
redirect_to quiz_question_url(@quiz, @question)
else
flash.now[:error] = "Question could not be updated"
render :edit
end
end
end
Test It!
To verify that we made this change correctly, we run the web app (as per the steps in the running apps demo), we open the URL http://localhost:3000/ in our web browser, and navigate to the various child edit/update forms. The forms displayed should look similar to Figure 6.
Step 6 Implement Destroy
Substep Add a nested destroy-resource route for child model objects.
config/routes.rb
Rails.application.routes.draw do
root to: redirect('/quizzes')
get 'quizzes', to: 'quizzes#index', as: 'quizzes'
post 'quizzes', to: 'quizzes#create'
get 'quizzes/new', to: 'quizzes#new', as: 'new_quiz'
get 'quizzes/:id', to: 'quizzes#show', as: 'quiz'
patch 'quizzes/:id', to: 'quizzes#update'
delete 'quizzes/:id', to: 'quizzes#destroy'
get 'quizzes/:id/edit', to: 'quizzes#edit', as: 'edit_quiz'
get 'quizzes/:quiz_id/questions', to: 'questions#index', as: 'quiz_questions'
post 'quizzes/:quiz_id/questions', to: 'questions#create'
get 'quizzes/:quiz_id/questions/new', to: 'questions#new', as: 'new_quiz_question'
get '/quizzes/:quiz_id/questions/:id', to: 'questions#show', as: 'quiz_question'
patch '/quizzes/:quiz_id/questions/:id', to: 'questions#update'
delete '/quizzes/:quiz_id/questions/:id', to: 'questions#destroy'
put '/quizzes/:quiz_id/questions/:id', to: 'questions#update'
get '/quizzes/:quiz_id/questions/:id/edit', to: 'questions#edit', as: 'edit_quiz_question'
end
Substep Add links to destroy child model objects.
app/views/questions/index.html.erb
<h1>Questions</h1>
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Question</th>
<th></th>
</tr>
</thead>
<tbody>
<% @questions.each do |question| %>
<tr>
<td><%= question.question %></td>
<td class="text-nowrap">
<%= link_to 'show', quiz_question_path(@quiz, question), class: 'btn btn-outline-secondary btn-sm' %>
<%= link_to 'edit', edit_quiz_question_path(@quiz, question), class: 'btn btn-outline-secondary btn-sm' %>
<%= link_to 'destroy', quiz_question_path(@quiz, question), method: :delete, class: 'btn btn-outline-secondary btn-sm' %>
</td>
</tr>
<% end %>
</tbody>
</table>
<p>
<%= link_to "New Question", new_quiz_question_path(@quiz), class: "btn btn-secondary"%>
</p>
<p>
<%= link_to 'Quiz', quiz_path(@quiz), class: 'btn btn-outline-secondary' %>
</p>
Substep Add a destroy-resource action to the child model controller.
app/controllers/questions_controller.rb
class QuestionsController < ApplicationController
def index
…
end
def show
…
end
def new
…
end
def create
…
end
def edit
…
end
def update
…
end
def destroy
@quiz = Quiz.find(params[:quiz_id])
@question = @quiz.questions.find(params[:id])
@question.destroy
flash[:success] = "Question deleted successfully"
redirect_to quiz_questions_url(@quiz)
end
end
Test It!
To verify that we made this change correctly, we run the web app (as per the steps in the running apps demo), we open the URL http://localhost:3000/ in our web browser, navigate to the various child index pages, and try destroying some of the Question
model objects.
Conclusion
Following the above steps, we have now added standard resource-CRUDing pages and UI features for a nested resource that is the child in a parent-child one-to-many association between two model classes.