Simply RESTful… “The missing action”

The ideas in this article came about whilst I was test-driving the Simply RESTful plugin following DHH’s RailsConf keynote on the subject.

The philosophy

The first thing I came across whilst experimenting with Simply RESTful (which is great by the way), was that there is no real way of deleting items with javascript disabled. Since I am currently working on a project that needs to function on a variety of mobile devices, this instantly caused me concern.

I could think of a few ways to hack around this limitation, however I was sure there had to be a better way, hence this article. I wanted to keep the current javascript functionality but in addition have a clean non-javascript fallback.

Consider the following:
CRUD Form (GET request) POST action
(C)reate /products/new create
(R)ead /products/24 n/a
(U)pdate /products/24/edit update
(D)elete - destroy

There are three “state changing” actions in CRUD, they are the ‘create’, ‘update’ and ‘delete’. You will notice from the table above that all three have a POST action1, however only two have GET actions… why is this?

Now, you see that dash in the second column… that’s “the missing action”. There is no good reason why our ‘destroy’ action shouldn’t have a corresponding form action (GET request) also. Let me explain myself…

1 The HTTP actions are PUT, POST and DELETE, however in this implementation (due to the limitations of HTML) they are all technically POST’s.

Putting it into practice

So we give ‘destroy’ it’s missing action which will act as a confirmation of our post… and what shall we call this missing action? …why let’s call it delete.

If we fill in this missing piece in our RESTful Rails puzzle, all becomes clear:
CRUD Form (GET request) POST action
(C)reate /products/new create
(R)ead /products/24 n/a
(U)pdate /products/24/edit update
(D)elete /products/24/delete destroy

Our routes would look something like:

map.resource :product, :member => { :delete => :get }

In our controller would be:

def delete
  @product = Product.find(params[:id])
end
 
def destroy
  Product.find(params[:id]).destroy if request.delete?
  redirect_to product_url
end

Our delete.rhtml would look like this:

<h1>Are you sure you wish to delete ?</h1>

Slight complication…

Update (13 Oct 2007): This has been fixed in more recent versions or Rails.

Now comes the slight complication… we want the javascript POST to /projects/24 to function as normal, however if javascript is disabled we want to request /projects/24;delete.

Wouldn’t it be nice if you could specify a fallback (non-javascript) href in the link_to helper, something that I’ve pondered with on many occasions. Unfortunately the link_to helper doesn’t let you override the href attribute (currently it adds a second one instead), until now.

Enter iq_noscript_friendly plugin which fixes this shortfall (I also have this as a Rails patch however the ticketing system on Trac is currently broken).

Install the plugin using:

./script/plugin install http://svn.soniciq.com/public/rails/plugins/iq_noscript_friendly/

In our listing view (index.rhtml) we are now able to do the following:

link_to 'Delete', product_url(product),
          :confirm =&gt; 'Are you sure?',
          :method =&gt; 'delete',
          :href =&gt; delete_product_url(product)

Ideally you would just give the link a class of “delete” and use unobtrusive javascript to make it do the delete request.

Beautiful.

Summary

By adding “the missing action”, we are able to POST as usual (using javascript) to ‘destroy’ but gracefully fallback to our ‘delete’ form when javascript is not available. Besides, why shouldn’t ‘destroy’ get it’s own form action… ‘create’ has ‘new’ and ‘update’ has ‘edit’?

Now to make this whole thing even better, lets make it part of the convention. ‘delete’ should default to GET and therefore negate the need for :member =&gt; { :delete =&gt; :get } in our routes.rb… DHH?

I would love to hear peoples comments on this technique as I’m using it for everything now and it works a treat.

Com’on… use “the missing action”, be kind to those without javascript, and lets make it the convention!

Rock on RESTfulness.

Tags: , ,

16 comments ↓

#1 rick on 07.26.06 at 3:36 am

Having a delete and destroy is just confusing. I usually do something like this:


def destroy
  if request.delete?
    @foo.destroy
    redirect_to foos_url
  end
end
#2 Jamie on 07.26.06 at 10:38 am

rick: You are missing my point, what if you have javascript turned off? You need an action to POST from i.e. a “confim delete” form.

Sure you could put this in the same action but I don’t think it belongs there.

Request deletion -> Confirm deletion -> Destroy

Just like edit:

Request edit -> Confirm changes -> Update

#3 Thomas Aylott on 07.26.06 at 9:25 pm

/people/1;delete falls right in line with the other two pre-action forms. (/people;new & /people;edit)

However, if you think of the pre-action forms (new & edit) as merely a way to define the parameters of the action. (name, password, etc…) Then it doesn’t really work for delete (or show, for that matter). Do you need parameters for the delete or show actions (other than the id)?

But, if you think about it from a fallback point of view, it works. You might want to do in-page AJAX fanciness for new, edit & delete. Then fall back to the actual pages if you can’t do the fanciness. (Yes, I’m including the javascript confirm as in-page fanciness.)

I totally agree that this should be the default implementation.

#4 Thomas Aylott on 07.26.06 at 9:26 pm

I’m not as sure about your implementation of it however. I do it a little differently.

#5 rcb on 07.28.06 at 1:13 am

I hate to rain on your parade, but HTTP GET requests are not supposed to change the server’s state. Deleting an item is most certainly a change of state, so that is why DELETE has no GET url.

#6 Rabbit on 07.28.06 at 2:51 am

@rcb

The delete action is a confirmation of deletion. I don’t think anyone here is advocating deleting records via GET.

The delete action’s view holds the submission form which acts as a gateway to the destructive operation.

Make sense?

#7 Jamie on 07.28.06 at 9:27 am

rcd: I am not suggesting you call destroy via a GET as you are right that would be bad.

Rabbit is right, it is simply a “request” to “delete” which gives you a form that then posts to “destroy”, just as “new” is a post to “create” and “edit” is a post to “update”.

#8 Tristan on 07.30.06 at 2:51 am

I think you misunderstood Rick. He’s doing it the better way. Just rename delete.rhtml to destroy.rhtml and it will render that unless it’s a DELETE request. So, when the form in destroy.rhtml or the JavaScript sends the DELETE request to the same URL it will delete the record and redirect to foos_url.

#9 Jamie on 07.30.06 at 11:16 am

Tristan: That would work if you weren’t using simply restful, however with simply restful the destroy url is the same as the show url, just with a different method set.

If you fall back to /products/24 when javascript is disabled you will just get the show page as the routes have no way of telling that you wanted destroy. That is why the fallback url needs to be different i.e. /products/24;delete, you can then POST to destroy with method set to delete and the routes know what you want.

Make sense?

#10 Tristan on 08.03.06 at 4:39 pm

You got me there. Luckily, this is Rails and we can do whatever we want.

First, drop in my handy dandy hack that allows you to use arrays for method requirements: http://buckymatters.com/simply_restful_hack.rb It looks like a lot, but I assure you it’s only an additional 12 lines consisting mostly of if statements.

Controller:

def destroy
  if request.delete?
    User.find(params[:id]).destroy

    respond_to do |type|
      type.html { redirect_to('/') }
      type.js   { render(:update) { |page| page.redirect_to('/') } }
    end
  end
end

Routes:

map.resources :users
map.resources :users, :member => {:destroy => [:delete, :get]}

You could also use an AJAX link here, but this way requires you to always
confirm it.

View:

<a href="<%= destroy_user_url(:id => current_user.id) %>">
  Cancel Account
</a>

I obviously take arguing too seriously, but boy did I get rid of that extra action. Maybe it’ll even make it to the core one day, but I doubt it.

By the way, this was my first core modification.

#11 Tristan on 08.03.06 at 4:57 pm

FYI, I’m running into a problem in testing with a destroy action in another controller. Not sure if it’s related, but I’m going to guess and say it is.

#12 Jamie Hill on 08.03.06 at 5:09 pm

Tristan: I really think you are over complicating things.

Why are you so keen to get rid of the extra action in favour of a load of conditional code?

  • update has edit
  • create has new
  • why shouldn’t destroy have delete?

I would much rather have one extra action and do away with all the conditional stuff and hacks.

#13 Jamie on 08.03.06 at 5:30 pm

Thomas: Sorry, my reply you your comment got lost.

In response to:

“However, if you think of the pre-action forms (new & edit) as merely a way to define the parameters of the action. Then it doesn’t really work for delete (or show, for that matter). Do you need parameters for the delete or show actions (other than the id)?”

There may be occasions when you want to e.g. log a reason for deletion etc. That is what Iove about this method, it caters for many scenarios.

#14 Tristan on 08.03.06 at 5:32 pm

I guess I was just looking for an excuse to make a hack. That or the fact that I’ve only had 6 hours of sleep in the past 41 hours. I see what your getting at with the other actions, and for some reason it only makes sense now that you pointed it out. I think I’ll go to sleep before I write more useless modifications for Rails.

#15 Jamie on 08.03.06 at 5:55 pm

Tristan: “Stop worrying and learn to love the delete” ;)

Have you had any luck submitting tickets to Rails Trac site? I keep getting an error.

#16 Tristan on 08.04.06 at 11:01 am

I have never tried to submit a ticket.

Leave a Comment