Unobtrusive object deletion in Rails, the easy way

Published:

Out of the box, Rails only has one route to the destroy action of each controller (when using RESTful resources) - see below.

resources :posts
    posts GET     /posts(.:format)           { :action=>"index",   :controller=>"posts" }
          POST    /posts(.:format)           { :action=>"create",  :controller=>"posts" }
 new_post GET     /posts/new(.:format)       { :action=>"new",     :controller=>"posts" }
edit_post GET     /posts/:id/edit(.:format)  { :action=>"edit",    :controller=>"posts" }
     post GET     /posts/:id(.:format)       { :action=>"show",    :controller=>"posts" }
          PUT     /posts/:id(.:format)       { :action=>"update",  :controller=>"posts" }
          DELETE  /posts/:id(.:format)       { :action=>"destroy", :controller=>"posts" }

The trouble with this is handling deletion, you need to perform a DELETE request which requires a form to be submitted, rather than a link to be clicked, which would perform a GET request. Handily, Rails includes an option on link_to allowing you to specify a :method, this is then enhanced by JavaScript to perform the deletion.

link_to 'Delete', @post, :method => 'delete', :confirm => 'Are you sure?'

This all looks great until you realise it won’t work without JavaScript. Now for some people, this is a moot point, but personally, I want my applications to be as widely accessible as possible and it also means if there is a slight bug in your JS code, the deletion won’t work.

Simple fix

The solution comprises two parts - handling when JavaScript is unavailable (disabled / erroneous) and then handling the changes to when it is available.

First of all, we’ll need a new route within our controller, this will render the deletion form to HTML:

resources :posts do
  get :delete, :on => :member
end
delete_post GET  /posts/:id/delete(.:format)  { :action=>"delete", :controller=>"posts" }

Our action should follow the same pattern as your edit action, below is an example.

Note: this action SHOULD NOT process the deletion, merely display a form (a confirmation) which the user must submit manually. Otherwise you will run into CSRF attacks.

def delete
  @post = Post.find(params[:id])
end

Now, we’ll need a simple form for this action, much like your edit form, so create delete.html.erb:

<%= form_for(@post, :url => { :action => :destroy }, :html => { :method => :delete }) do |f| %>
  <p>Are you sure you want to delete the post entitled '<%= @post.title %>'?</p>
  
  <div class="footer">
    <%= link_to 'No', :back %>
    <button type="submit">Yes &raquo;</button>
  </div>
<% end %>

Now we can update our link_to to point to this form:

link_to 'Delete', delete_post_path(@post), :method => 'delete', :confirm => 'Are you sure?'

The trouble we have now, is that if the user does have JavaScript enabled, it will perform a DELETE request to our delete action, which isn’t a valid route, nor does the action handle deletion. To get round this, we just need to handle that route, but redirect it to the existing destroy method:

resources :posts do
  get    :delete, :on => :member
  delete :delete, :on => :member, :action => :destroy
end
DELETE  /posts/:id/delete(.:format)  { :action=>"destroy", :controller=>"posts" }

Now, if a user has JavaScript, the deletion will happen as usual: using JavaScript, Rails will create a delete form and submit it to the destroy method. If they do not, it will render the delete confirmation form.