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 »</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.