So far, we've worked with three pieces of the CRUD puzzle:
- Creating records, using HTTP
POSTrequests. - Reading records, using HTTP
GETrequests. - Updating records, using HTTP
PATCHrequests.
One piece remains:
- Deleting records, using HTTP
DELETErequests.
But, all is not well in Browsertown. In many cases, sending a request with the
PATCH or DELETE method will not work. In this lesson, we'll focus on
DELETE, but many of the same issues arise with PATCH requests, as well.
Why? What can we do as a workaround?
After this lesson, you'll be able to...
- Draw a
deleteroute mapping to a#destroy()action - Explain the problem with submitting
deleterequests - Use
form_withto build a delete form for an object - Build a
#destroy()action that finds the instance, destroys it, and redirects to theindexaction - Use
button_to(orlink_to) withmethod: :deleteto destroy an object without a form
Before we dive into the problem with DELETE (and PATCH) requests, let's
proceed as if we were none the wiser, setting up our route and form as usual:
# config/routes.rb
delete '/people/:id', to: 'people#destroy', as: 'person'# app/views/people/show.html.erb
<h2><%= @person.name %></h2>
<%= @person.email %>
<%= form_with url: person_path(@person.id), method: :delete do %>
<%= submit_tag "Delete #{@person.name}" %>
<% end %>But, wait a minute... there's something weird about the output we get:
<h2>Caligula</h2>
caligula@rome-circa-40-AD.com
<form accept-charset="UTF-8" action="/people/1" method="post">
<input name="_method" type="hidden" value="delete" />
<input name="utf8" type="hidden" value="✓" />
<input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
<input name="commit" type="submit" value="Delete Caligula" />
</form>Enhance!
<form accept-charset="UTF-8" action="/people/1" method="**post**">
<input name="_method" type="hidden" value="**delete**" />What's going on? Why the extra input?
Web developers love to be on the cutting edge. We hoover up new tools and techniques and aren't afraid of breaking a few million eggs to figure out that we actually have no idea how to make an omelette. Projects like Rails themselves are a product of this incredible devotion to progress and automation.
Browser and server developers are the yin to the web developers' yang. These are the people who own and maintain the tools themselves: Internet Explorer, Firefox, Chrome, Apache, and so on.
When a web developer makes a mistake, it might affect the users of their site.
When a browser or server developer makes a mistake, it might affect over a billion people!
Because of this, browser/server developers have to go slow. Really slow. And they have to resist the urge to release "duct tape" solutions because duct tape doesn't scale to a billion users!
This means that, sometimes, incomplete solutions can remain in place for years, or even decades, while the maintainers go back and forth trying to find a better approach that won't open an eldritch portal to Bosch's Garden of Earthly Delights.
As of HTML5, forms officially do not support DELETE and PATCH for their methods.
There's no short and sweet answer to explain this. If you want to dive deep and understand as much as possible about the decisions that went into it, you can read more on this illuminating StackExchange post, but, for the purposes of succinct explanation, you can always stick with the tried and true "for historical reasons."
What you're seeing in the above form_with behavior is a workaround
implemented for us by Rails itself. With this in mind, we get the best of both
worlds:
- We get to be good HTTP-abiding citizens who use the correct request methods
for their corresponding goals (
GETfor read,PATCHfor update, and so on). - We get to maintain our sanity and not worry about W3C drama while writing views.
Thus enlightened, we can (finally) proceed with our original goal:
# app/controllers/people_controller.rb
def destroy
Person.find(params[:id]).destroy
redirect_to people_url
endNothing too special happening here except for a bit of method-chaining to immediately destroy the found instance.
As shown, you have to go to a user's show page to delete them. What if we want
an admin control panel where users can be deleted from a list?
The safest option in modern Rails is button_to, which renders a small
form (including the hidden _method and CSRF fields) wrapped around a single
button:
<!-- app/views/people/index.html.erb -->
<% @people.each do |person| %>
<div class="person">
<span><%= person.name %></span>
<%= button_to "Delete", person, method: :delete, form: { data: { turbo_confirm: "Really?" } } %>
</div>
<% end %>The turbo_confirm data attribute pops up a confirmation window before the
request is sent, letting the user make sure they're ready to delete the record
forever (what a decision!).
You can also delete via a link. In older versions of Rails this relied on
rails-ujs and the method: / data-confirm options. As of Rails 7,
Turbo handles this, so you use the turbo_method
and turbo_confirm data attributes instead:
<%= link_to "Delete", person, data: { turbo_method: :delete, turbo_confirm: "Really?" } %>which generates:
<a data-turbo-method="delete" data-turbo-confirm="Really?" href="/people/1">Delete</a>Because a link is normally a GET request, Turbo's JavaScript intercepts the
click and submits it as a DELETE instead — and falls back to GET if
JavaScript is unavailable. That fragility is exactly why Rails treats button_to
(a real form) as the safer, more accessible default for destructive actions.
A slight variation from the link_to method described above is using button_to to send a delete request.
Says the documentation:
[
button_to] Generates a form containing a single button that submits to the URL created by the set of options.
As noted above, Rails treats button_to as the safer default for destructive actions. Here it is on its own, outside the context of an index list:
<%= button_to "Delete Image", image_path(@image), method: :delete %>As you can read in the documentation, or guess thanks to Ruby's simple syntax, the first argument is the button text; the second argument is an expression of a route, and method is used to tell the form to send its payload as an HTTP DELETE action.