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):

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.

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.

A class diagram depicting the classes from Figure 1 with an association line connecting them

Figure 1. The parent Quiz and child Question model classes.

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.

xxx

Figure 2. Example parent Quiz show page, including a link to an index page of child Question objects.

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.

xxx.

Figure 3. Example index page of child Question model objects that belong to the same 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.

xxx.

Figure 4. Example new/create form for creating a new child Question object such that it belongs to a parent Quiz object (whose ID is in the URL resource path).

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.

xxx.

Figure 5. Example show page for a child Question model object.

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.

xxx.

Figure 6. Example edit/update form for a child Question model object.

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.

Base App Code

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 1 Changeset

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 2 Changeset

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 3 Changeset

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 4 Changeset

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.

Step 5 Changeset

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.

Demo App Code