I need a bunch of links on a page each of which makes a POST to a different controller. But when I use normal links, I get a ActionController::InvalidAuthenticityToken error. I understand this is because of the missing authenticity_token value. But I don't want to use forms to do the POST because I want them to be links and not buttons. Fact is, I want complete control over the styling of the links and buttons just don't do it for me. What's the standard way of doing such things?
You have lots of options.
- Disable AT check:
protect_from_forgery :only => []
- Assign onclick handler to the link and submit hidden form with javascript.
- Grab AT while generating view and add it as request parameter.
BTW, how exactly do you make 'post' requests using only 'href' attribute? I thought form is a must for that.
Technically speaking, you should be using buttons and forms for anything that isn't a GET
; hyperlinks intentionally don't allow for methods other than GET
(without hacks like the _method
parameter). One very practical reason is that sometimes, "web accelerator" browser add-ons prefetch links in the page; if a GET link kicks off a mutative action, the user or resource state may be erroneously modified.
That said, you can style buttons to behave like links; I use something like the following to do it quite nicely. It assumes a proper CSS reset with margins and padding and all that good stuff being nilled.
input.restlink {
border: 0;
background: #fff;
color: #236cb0;
cursor: pointer;
}
input.restlink:hover {
text-decoration: underline;
}
With a rule like that, you can use <%=button_to "Yay button", something_path, :method => :post %>
and it'll look and behave like a link just fine.
If you aren't opposed to using jQuery and some ajax'n, I have a blog post that covers this.
Here's some basic info if you'd like a high level overview.
Add this one your layout:
<%= javascript_tag "var AUTH_TOKEN = #{form_authenticity_token.inspect};" if protect_against_forgery? %>
This code adds the auth token to the response. This way the JS can pick it up and submit it to the server.
Next we intercept any ajax call in the application.js:
function isPost(requestType) {
return requestType.toLowerCase() == 'post';
}
$(document).ajaxSend(function(event, xhr, settings) {
if (isPost(settings.type)) {
settings.data = (settings.data ? settings.data + "&" : "") + "authenticity_token=" + encodeURIComponent( AUTH_TOKEN );
}
xhr.setRequestHeader("Accept", "text/javascript, application/javascript");
});
Add this to your application controller:
before_filter :correct_safari_and_ie_accept_headers
after_filter :set_xhr_flash
protected
def set_xhr_flash
flash.discard if request.xhr?
end
def correct_safari_and_ie_accept_headers
ajax_request_types = ['text/javascript', 'application/json', 'text/xml']
request.accepts.sort!{ |x, y| ajax_request_types.include?(y.to_s) ? 1 : -1 } if request.xhr?
end
And in your view:
<%= link_to "Delete", delete_product_path(product), :class => 'delete' %>
Back over to application.js:
$('a.delete').live('click', function(event) {
if(event.button != 0) { return true; }
$.post(link.attr('href').substring(0, link.attr('href').indexOf('/delete')), { _method: "delete" });
return false;
});
This example does a delete but it's really the same process to handle posts or puts. The blog post has a full sample application that demonstrates this.