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.
-
Step 1 Add a Destroy-Resource Route. This step specifies that requests resulting from clicks of the destroy-resource link will be DELETE requests, specifies the URL resource-paths for such requests, and tells the web server which controller action to call to process such requests.
-
Step 2 Add a Model Controller (if Needed). If a controller class for the model objects to be created does not already exist, this step creates one.
-
Step 3 Add Link(s) to the Destroy-Resource (if Needed). This step generates links that enable users to access the destroy-resource.
-
Step 4 Add a Destroy-Resource Controller Action. This step adds an action to the controller that retrieves the object’s id sent in the request and uses it to find the appropriate object and delete it from the database. If the deletion is successful, the action sends an HTTP redirect response to a specified page.
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.
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.
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.
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 2 Add a Model Controller (if Needed)
Because the base app already has a controller for Todo
model objects, TodosController
, we skip this step.
Step 3 Add Link(s) to the Destroy-Resource (if Needed)
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 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.
Conclusion
Following the above steps, we have now made a destroy action for removing instances of an existing model class from the database.