Observant folks pointed out that in my post about helpers I've stopped using that I skipped one crucial feature of link_to: RESTful deleting. A REST patterned application uses four HTTP verbs (POST, PUT, GET, DELETE). Only POST and GET can be used reliably through a browser (although libraries like curl or ActiveResource can use all four), so support for the others is added by including a hidden parameter named '_method' that is POSTed through a form. You'll notice it if you view the source of an /edit/ page (since edit's evil twin update lurks in the controller gobbling up PUT requests).
This is a decent solution for updating a resource because you'll already be using a form and sending one extra hidden parameter is a small price to pay for a solid architecture pattern. Deleting, however, is typically done with a link, not a form. To get the a delete link with the link_to you supply an argument of :method => :delete and Rails handles the magic of making your delete link work.
Needing delete links means you have to use two methods for creating a link (assuming you buy my argument of haml's built-in tag building being preferable) leading to code like the example below:
%a{:href => post_url(@post)} read more
%a{:href => edit_post_url(@post)} edit
= link_to 'remove post', post_url(@post), :method => :delete
I'm not entirely happy with view code like that, but it's workable. Let's take a look at what this will output
<a href='/posts/251'>read more</a>
<a href='/posts/251/edit'>edit</a>
<a href='/posts/251/' onclick='if (confirm("Are you sure?")) { var f = document.createElement("form");f.style.display = "none"; this.parentNode.appendChild(f); f.method = "POST"; f.action = this.href; var m = document.createElement("input"); m.setAttribute("type", "hidden"); m.setAttribute("name", "_method"); m.setAttribute("value", "delete"); f.appendChild(m);f.submit(); };return false'>remove post</a>
Ick! link_to's behind the scenes magic involves writing some javascript to the page. The javascript waits for clicks on the link, stops the click event, creates a new form in the DOM, adds it to the page (with display set to 'none' so the user won't see it) and POSTs a request along with the hidden attribute of _method set to the value 'delete'. This is just standard, browser agnostic javascript so it will work in any browser but has two flaws: It writes javascript directly to the page and will not work with javascript turned off.
I can ignore my preference for unobtrusive javascript to make my views a little prettier but I often can't have a link that fails if javascript is off. One solution to this problem is to flip the logic being used: instead of a link that acts like a form when javascript is on (but fails entirely when it's off) you use a form that acts like a link when javascript is on, but remains a form when javascript is off. The railscasts folks did a whole screencast about deleting sans javascriptusing this method.
A drawback is that folks without javascript will see a delete button (a submit-type input) but seeing a button is better than not being able to delete at all. Using the form-that-becomes-a-link tactic will net us view code like this
%a{:href => post_url(@post)} read more
%a{:href => edit_post_url(@post)} edit
= link_to_destroy 'remove post', post_path(@post), confirm_destroy_post_path(@post)
Unfortunately link_to_destroy demonstrated in the screencast is really just a wrapper for a specific kind of call to link_to_function and will place inline javascript into your view. Everyone can delete, but we have uglier views and uglier output. I consider this a step backwards: the goal should be pretty views, unobtrusive javascript for clean output, and graceful fallbacks that don't require much hoop jumping.
Making reliable delete links
To make resource deleting work consistently there is no escaping the need for a form element. Let's whip up a basic helper that will create a button for deleting
def link_to_delete(options = {})
haml_tag :form, {:action => options[:href], :method => 'post', :class => 'delete-form'} do
haml_tag :input, {:type => 'hidden', :name => '_method', :value => (options[:method] || :delete)}
haml_tag :input, {:type => 'hidden', :name => 'authenticity_token', :value => form_authenticity_token}
haml_tag :input, {:type => 'submit', :value => options[:value], :class => 'delete-button'}
end
end
Used in the view, it looks like this
%a{:href => post_url(@post)} read more
%a{:href => edit_post_url(@post)} edit
= link_to_destroy(:href=> post_path(@post), :value => 'remove post')
and will the following html
<a href="/posts/251">read more</a>
<a href="/posts/251/edit">edit</a>
<form action="posts/251" method='post' class='delete-form'>
<input type='hidden' name='_method' value='delete' />
<input type='hidden' name='authenticity_token' value='1c75f5e944f90f38' />
<input type='submit' name='submit' value='remove post' class='delete-button' />
</form>
The view code in this situation isn't great, but the html output is clean, compliant, and easy to understand.
Making delete links pretty
At this point every user will see a delete submit input. We can use some unobtrusive javascript to give users with javascript turned on a more desirable experience. A tiny prototype-based class will hide the form, show a link instead, and make the click event on the link submit the form properly.
var DeleteLink = Class.create({
initialize: function(form) {
form.hide();
var submitButton = form.down('input.delete-button');
var anchor = new Element('a', { 'class': 'link-to-delete', href: form.action }).update(submitButton.value);
form.insert({after : anchor});
anchor.observe('click', this.commitDeletion.bindAsEventListener(form));
},
commitDeletion: function(event) {
event.stop();
this.submit();
}
});
You can apply this behavior unobtrusively with prototype's dom:loaded event. I stick all my behavior adding javascript in application.js
document.observe("dom:loaded", function() {
$$('.delete-form').each(function(link) { new DeleteLink(link) });
});
This looks for all items with the class of 'delete-form' and creates a new DeleteLink object. Now users with javascript on will see a link (that can be styled just like any link), folks without javascript will see a button, and everyone will be able to properly delete.
Gussy up the views
So far we've added unobtrusive javascript for clean output with graceful fallbacks. The last task is to make the views a little nicer to look at. This requires just a dab of aesthetics and no additional code. Recall that our views with deleting look like this right now.
%a{:href => post_url(@post)} read more
%a{:href => edit_post_url(@post)} edit
= link_to_destroy(:href => post_path(@post), :value => 'remove post')
The link_to_destroy method stands our just like link_to did, but since we needed to write our own helper anyway (to get the unobtrusive and reliable functionality) we can go ahead and name this helper method something that fits in more pleasantly. Ideally we'd be able to have a view like this:
%a{:href => post_url(@post)} read more
%a{:href => edit_post_url(@post)} edit
%a{:href =>post_path(@post), :method => :delete} remove post
but that will just write method='delete' directly into the HTML anchor tag (which is meaningless). I decided to name the helper 'a' and set the value of the button/link with an argument
%a{:href=> post_url(@post)} read more
%a{:href=> edit_post_url(@post)} edit
=a({:href=>post_path(@post), :method => :delete, :value => 'remove post'})
Haml already uses some special characters to denote specific behavior in an HTML tag (compare %input and ~input) making the use of = fit into the the general structure of haml documents decently well. I wasn't able to get it to work sans curly braces but I'm happy with how well it fits in surrounded by other "regular" anchor tags.