Unobtrusive Destroy Links in Rails using jQuery
Wednesday, February 25, 2009
Rails has a few helpers that make creating RESTful action links, like a delete link, pretty easy. A delete action has to use a POST request which means it has to use a form or AJAX. To get around this Rails spits out some inline JavaScript that generates the form and submits it for you when you click the link in the browser. This means that the delete link requires JavaScript to work. This is becoming a typical requirement for web apps (possibly propagated by how simple Rails and similar frameworks make it to be obtrusive). However, with minimal effort we can create a more flexible solution that offers more control to the designers and provides an alternative for those without JavaScript.
The Rails
The first thing we need to do on the Rails side is add a delete action to our controller. Actually, we really should write a failing spec or test first but I want to keep things a little more focused.
The delete action is to the destroy action as the new is to create and edit is to update. Here is what the delete method might look like in a Post Controller.
class Post < ApplicationController
# some filters
# some actions
def delete
@post = Post.find( params[:id] )
respond_to do |format|
format.html # delete.html.erb
end
end
end
The delete.html.erb should contain a form that contains a button to confirm the deletion or to cancel the deletion. Here is what it might look like.
<h2>Are you sure you want to delete this Post?</h2>
<% form_for @post, :html => { :method => 'delete' } do |f| %>
<%= submit_tag "Yes" %>
<%= submit_tag "No", :name => "cancel" %>
<% end %>
In the destroy action you’ll want to check to see if the cancel button was clicked and probably redirect the user somewhere. The destroy action might look something like this.
def destroy
@post = Post.find( params[:id] )
redirect_to(@post) and return if params[:cancel]
@post.destroy
respond_to do |format|
format.html { redirect_to posts_path }
end
end
Next, we’ll need to include this new action in our Routes. It should go in the member hash since this action operates on a single object.
map.resource :post, :member => { :delete => :get }
Finally, in our views we’ll write a link using a class of ‘delete’ so that jQuery can find it easily.
<%= link_to("delete", delete_post_path(@post), :class => "delete") %>
The jQuery
So now that we have a link that works without JavaScript, we can unobtrusively and progressively enhance the user experience by hijacking the link with jQuery.
I recommend using the technique from my blog post jQuery, Rails, and AJAX to handle passing the authenticity token with the request.
There are basically two ways of handling this. We could dynamically create a form and submit it or just use AJAX to make the DELETE request. First, I’ll show you the code required to dynamically create a form and submit it.
jQuery(function($) { // document ready
// Uses the new live method in jQuery 1.3+
$('a.delete').live('click', function(event) {
if ( confirm("Are you sure you want to delete this Post?") )
$('<form method="post" action="' + this.href.replace('/delete', '') + '" />')
.append('<input type="hidden" name="_method" value="delete" />')
.append('<input type="hidden" name="authenticity_token" value="' + AUTH_TOKEN + '" />')
.appendTo('body')
.submit();
return false;
});
});
The second approach is to use AJAX to make the request.
jQuery(function($) { // document ready
// Uses the new live method in jQuery 1.3+
$('a.delete').live('click', function(event) {
if ( confirm("Are you sure you want to delete this Post?") )
$.ajax({
url: this.href.replace('/delete', ''),
type: 'post',
dataType: 'script',
data: { '_method': 'delete' },
success: function() {
// the item has been deleted
// might want to remove it from the interface
// or redirect or reload by setting window.location
}
});
return false;
});
});
For the AJAX based approach to work properly you’ll need to add a format.js to the respond_to block in the destroy action. In this case I don’t actually want to send anything back to the client so I’ll use render :nothing => true. The updated destroy action for the AJAX based delete link might look like this.
def destroy
@post = Post.find( params[:id] )
redirect_to(@post) and return if params[:cancel]
@post.destroy
respond_to do |format|
format.html { redirect_to posts_path }
format.js { render :nothing => true }
end
end
And Then?
There are a number of variations to this approach. For example in cubeless we use a jQuery UI dialog to confirm the delete instead of the JavaScript confirm dialog. Maybe I’ll write about that next! :) You could also do some interesting stuff in the AJAX response once the delete is successful.
Posted in jQuery, Ruby on Rails with 12 comments
i’m looking forward to the future posts! ujs ftw
By Burin Asavesna on Wednesday, February 25, 2009 at 09:39 PM
Keep the Rails + jQuery goodness coming. :)
By Mike T. on Wednesday, February 25, 2009 at 10:17 PM
Thanks for this post, obstrusivity in rails is really the thing that annoys me.
I’ve looked if there were a way to override the rspec_scaffold and scaffold generator in an initializer or something, but each is based upon a template file, “lib/rails_generator/generators/components/scaffold/templates/controller.rb”. So, the only way to hook it is to make a new generator and maintain it.
Maybe you could make a pull request to the rails core? Forgetting the jQuery part, your solution seems totally portable and may benefits to everyone.
By oelmekki on Thursday, February 26, 2009 at 09:39 AM
Thanks for sharing. It is really nice to see a complete example. How would you send a error from the controller to the view? “Access denied”, “nil” etc.
By Asbjørn Morell on Saturday, February 28, 2009 at 01:58 AM
@Asbjørn, One simple way is to use
flash[:error]for the non-js version and render the error back in the js block of the js version.I’ve found that I like to send JSON back to the client. If the request was successful, I’ll send the updated record back as JSON. If there were errors, then I send those back as JSON as well.
If you wanted to use JSON for the delete action, then just switch the
dataTypeto ‘json’ and use something like the following format block.Now your success callback will just need to handle the JSON and decide what to do with it.
Perhaps this warrants a new blog post on how I handle this application-wide. :)
By Brandon Aaron on Saturday, February 28, 2009 at 04:38 PM
why not to use jrails
By Shtirlic on Sunday, March 1, 2009 at 04:23 PM
@Shtirlic, I don’t use jrails because it is a complete drop-in replacement for prototype. That means it still uses obtrusive patterns like inline JS where it isn’t necessary.
If jrails provided alternatives like this blog post illustrates then that would be a different story.
By Brandon Aaron on Sunday, March 1, 2009 at 04:57 PM
note to self, don’t have anything in your url (like a slug) with the word delete in it…
should you maybe modify that so that is searches and replaces only the last part == “/delete”
well not you, but whoever might implement this. Which I am, so don’t start your slug with a /delete $)
Anyway, great frickin post!
By taelor on Thursday, March 12, 2009 at 12:35 AM
Yes, please do this. I can remove the dom element containing the post and the link, but I can’t see how to get a message to display. :-(
By bourneslippy on Thursday, April 30, 2009 at 02:32 AM
Great post! but one small suggestion:
if you’ll swap out
url: this.href.replace('/delete', '')forurl: this.href.replace(/\/delete$/,'')it will only replace the occurrence of/deleteat the end of thehrefstring.By Josh Taylor on Wednesday, May 20, 2009 at 07:15 PM
Thanks for the helpful posts on using jQuery unobtrusively in Rails.
One question: How would you suggest hooking into the Rails flash? I’m using the AJAX approach you mentioned above and would like to set a flash notice after setting the window.location.
By Patrick Berkeley on Monday, August 3, 2009 at 04:01 AM
I’m glad someone finally agrees with me on this, see: http://thelucid.com/2006/07/26/simply-restful-the-missing-action/
This really needs to be the default in Rails. “Besides, why shouldn’t ‘destroy’ get it’s own form action… ‘create’ has ‘new’ and ‘update’ has ‘edit’?”.
By Jamie Hill on Wednesday, September 9, 2009 at 09:15 PM