Destroy Actions Deleting Model Records

In this demonstration, I will show how to make a “destroy” action for deleting model records from the database. Unlike the “new/create” and “edit/update” resources that use form submissions to generate POST and PATCH requests, we use a link with a special option to generate a DELETE request to the web server. The “destroy” action processes that DELETE request.

General Steps

In general, the steps for adding a “destroy” action for an existing model class are as follows.

Making a Destroy-Resource for Todo Model Objects

To demonstrate the steps for adding a destroy-resource, we will be making a destroy-resource for a model class representing items in a to-do list. In addition to adding the destroy-resource route and controller action, we will add “Delete” links to the provided index page. Figure 1 illustrates how the modified index page will look.

A screenshot of the right side of the index page that shows the 'Delete' links.

Figure 1. Buttons on the right side of the index page that link to the destroy-resource.

For this demo, we will build upon a base app (base-app-todo-plus branch) for managing a to-do list that contains a Todo model class (depicted in Figure 2), which includes a couple attribute validations, corresponding model tests, a seeds script for populating the database with some sample Todo objects, a TodosController class, a root route, infrastructure for displaying flash notifications (as described in the flash notifications deets), and the routes, controller actions, and view templates for displaying an index page and a show page for Todo objects.

A class diagram depicting a Todo model class with the following attributes: a title string, description text, and a due date

Figure 2. The Todo model class.

After a user clicks the link to the destroy-resource and the object is successfully destroyed, the browser will be redirected to the app’s index page, and a success notification will be displayed, as illustrated in Figure 3.

A screenshot of the top of the index page that shows a notification that says The to-do item was successfully destroyed.

Figure 3. A success notification that is displayed after an object is successfully destroyed.

Base App Code

Step 1 Add a Destroy-Resource Route

To add a route for the destroy-resource, we add a route declaration to the config/routes.rb file, like this:

Rails.application.routes.draw do

  root to: redirect('/todos')

  get 'todos', to: 'todos#index', as: 'todos'
  get 'todos/:id', to: 'todos#show', as: 'todo'
  delete 'todos/:id', to: 'todos#destroy'

end

Note that this route matches HTTP DELETE requests with a resource path of todos/:id (e.g., as in the URL http://localhost:3000/todos/1). As per the to: argument, the controller action to process such requests is TodosController#destroy. This route will use the todo path and URL helpers from the show-resource route, but with an additional method parameter as :delete.

Important! It does not matter where the destroy-resource route declaration (delete 'todos/:id'…) goes in the list of standard resource routes for the Todo model since it is the only one with that request type and cannot be confused with any of the other default resource routes. However, to keep things organized, convention dictates it should be the last one for that model.

Test It!

To confirm that we made this change correctly, we run the following command in the terminal:

rails routes -E

The command should output a long list of routes, and the fourth one should look like this:

--[ Route 4 ]---------------------------------------------------------------------------------
Prefix            | 
Verb              | DELETE
URI               | /todos/:id(.:format)
Controller#Action | todos#destroy

If we made an error, something different will be outputted. For example, if we made a syntax error, it would instead produce an error message.

Step 1 Changeset

Step 2 Add a Model Controller (if Needed)

Because the base app already has a controller for Todo model objects, TodosController, we skip this step.

To make the destroy action accessible to users of the app, we add a button to each record’s row in the index table which generates a DELETE request with that object’s id. To complete this step, we will perform two substeps: (1) generating the link in the index view template and (2) styling the link to look like a button.

Substep Add link(s) from the index page to the destroy-resource action. To complete this substep, we add to the app/views/todos/index.html.erb view template an empty <th> and corresponding <td> element containing an embedded-Ruby call to the link_to view helper inside the loop, like this:

<h1>To-Do Items</h1>

<table class="table table-hover">

  <thead class="thead-dark">
    <tr>
      <th>Title</th>
      <th>Due Date</th>
      <th></th>
    </tr>
  </thead>

  <tbody>
    <% @todos.each do |todo| %>
      <tr>
        <td><%= link_to todo.title, todo_path(todo) %></td>
        <td><%= todo.due_date.strftime("%m/%d/%Y") %></td>
        <td><%= link_to "Delete", todo_path(todo), method: :delete %></td>
      </tr>
    <% end %>
   </tbody>

</table>

The options set on the link_to are the link text as “Delete”, the request URL as given by the todo_path helper with the id parameter set by the passed in todo object, and the request method as :delete indicating a DELETE request.

Substep Style the link to look like a button. To complete this substep, we add an argument to the call to link_to that adds Bootstrap button CSS classes to the generated link, like this:

<td><%= link_to "Delete", todo_path(todo), method: :delete, class: "btn btn-danger" %></td>

Test It!

To verify that we completed this step correctly, we run the web app (as per the steps in the running apps demo); we open the URL http://localhost:3000/todos in our web browser; and we press the “Delete” button for one of the to-do items. An “Unknown action” error page should be displayed because “The action ‘destroy’ could not be found for TodosController”. This message indicates that our delete route was matched, but there is no TodosController#destroy method, which we will define in the next step.

Step 3 Changeset

Step 4 Add a Destroy-Resource Controller Action

To create the controller action for the destroy-resource, we perform three substeps: (1) create an empty destroy-resource controller action, (2) add code to the action that retrieves the model object, and (3) add code to the action that destroys the model object and responds to the browser with an HTTP redirect.

Substep Create an empty destroy-resource controller action. To complete this substep, we define an create method in the TodosController class (in app/controllers/todos_controller.rb), like this:

class TodosController < ApplicationController

  def index
    @todos = Todo.order(:due_date)
    render :index
  end

  def show
    @todo = Todo.find(params[:id])
    render :show
  end

  def destroy

  end

end

Substep Retrieve the model object. To complete this substep, we add to the destroy method a call to the Todo.find method, like this:

class TodosController < ApplicationController

  …

  def destroy
    @todo = Todo.find(params[:id])
  end

end

The find model class method is used to retrieve a model object from the database by id. The above call to Todo.find returns a Todo object (i.e., the one retrieved from the database), and the @todo instance variable is assigned a reference to that object. The use of an instance variable (indicated by the @ in @todo) is important here, because instance variables are accessible within view templates rendered by the class, thus allowing data to be passed from the controller to the view. The params method is provided by Rails as a way to access the all the data received in the HTTP request to be processed by the controller action. Although params is technically a method, it can be used like a hash. The above call to params[:id] returns the ID embedded in the resource path of the HTTP request (recall the 'todos/:id' part of the destroy route). The ID is, in turn, passed to the find call, effectively telling the call which Todo object to retrieve from the database.

Substep Destroy the model object and redirect with success message. To complete this step, we first add to the destroy action a call to the destroy method, like this:

class TodosController < ApplicationController

  …

  def destroy
    @todo = Todo.find(params[:id])
    @todo.destroy
  end

end

In our standard case, the call to destroy will always succeed as long as the model object is retrieved without error. Therefore, the Rails guides suggest to skip wrapping the destroy call in an if statement with success and failure handling.

Next, we set the behavior after a successful deletion, like this:

class TodosController < ApplicationController

  …

  def destroy
    @todo = Todo.find(params[:id])
    @todo.destroy
    flash[:success] = "The to-do item was successfully destroyed."
    redirect_to todos_url
  end

end

The call to redirect_to causes the web server to reply to the browser with an HTTP redirect response. In particular, the call, redirect_to todos_url, tells the browser to make a new HTTP GET request to the URL of the app’s index page. The flash[:success] line above that assigns a message string to the flash hash will cause a flash success notification to appear on the index page when the browser displays it. (See the flash notifications deets for a full explanation of how this works.)

Test It!

To verify that we completed this step correctly, we run the web app (as per the steps in the running apps demo). We open the URL http://localhost:3000/todos in our web browser, and click the “Delete” button for one of the to-do items. The browser should be redirected back to the index page (http://localhost:3000/todos), and the to-do item we selected should no longer appear in the table. A success notification should also appear at the top of the index page.

Step 4 Changeset

Conclusion

Following the above steps, we have now made a destroy action for removing instances of an existing model class from the database.

Demo App Code