Recently, while pairing with Abel as we hacked on some code for his budget tracking app, we came across an interesting problem while trying to acomplish what seems like a pretty straightforward task.
The Context: We had a table of records were using remote_form_for to output an edit form for each row so that we could allow inline editing of any row via AJAX. The code looked something like this:
-remote_form_for(person) do |f|
%tr
%td
=f.text_field :first_name
%td
=f.text_field :last_name
%td
=f.submit 'Save'If you prefer straight HTML, the relevant code looked something like this:
<form> <input name="person[first_name]" type="text" /> <input name="person[last_name]" type="text" /> <input type="submit" value="Save" /> </form>
The Problem: When submitting an edit on any of the people rows, we would see an ActionController::InvalidAuthenticityToken exception in the server log. This error is usually a sign of a configuration problem with your environment, but our case seemed different. Firebug showed us that the browser didn’t appear to be passing any params to the server during it’s update post and the server log didn’t show any params coming through, either. So, it wasn’t an invalid authenticity token being passed; no token was being pased at all. I would prefer if Rails gave us a MissingAuthenticityToken error instead but I digress.
The Discovery: After whipping up a test case where we had a remote form on a page working fine with no errors, we discovered that the problem only occured when the form was wrapping the record’s table row tag in the view. And, well, this makes a good deal of sense. Here’s what the HTML 4.01 spec says about FORMs and TABLEs (relevant lines from the spec have been pasted below in a convenient order):
<!--ELEMENT TABLE - -
(CAPTION?, (COL*|COLGROUP*), THEAD?, TFOOT?, TBODY+)-->
<!--ELEMENT TBODY O O (TR)+ table body -->
<!--ELEMENT TR - O (TH|TD)+ table row -->
<!--ELEMENT (TH|TD) - O (%flow;)* table header cell, table data cell -->
<!--ENTITY % flow "%block; | %inline;"-->
<!--ENTITY % block
"P | %heading; | %list; | %preformatted; | DL | DIV | NOSCRIPT |
BLOCKQUOTE | FORM | HR | TABLE | FIELDSET | ADDRESS"-->If you’re not used to looking at DTDs, all that synax can be hard to grok. Here’s what the relevant parts of it say, in plain English:
- TABLEs must have a TBODY
- TBODYs must have a TR
- TRs must have THs or TDs
- TDs can have any elements inside them as long as those elements belong to the flow group
- The flow group consists of elements inside the block group and the inline group
- FORM elements are part of the block group.
So, what was happening was that the browser saw our FORM element defined as a sibling to the TR. That’s not allowed in the spec, so it makes some bit of sense that the browser wouldn’t pass the form data to the server the way we expect it would. After all, the browser is dealing with invalid markup here.
The Solution: The solution we found was to wrap the form input controls inside one big table that’s wrapped by the form tag, like this:
%table
-@people.each do |person|
%tr
%td
-remote_form_for(person) do |f|
%table
%tr
%td
=f.text_field :first_name
%td
=f.text_field :last_name
%td
=f.submit 'Save'Again, in HTML:
<table border="0"> <% @people.each do |person| %> <tbody> <tr> <td> <form> <table border="0"> <tbody> <tr> <td> <input name="person[first_name]" type="text" /></td> <td> <input name="person[last_name]" type="text" /></td> <td> <input type="submit" value="Save" /></td> </tr> </tbody></table> </form></td> </tr> <% end %></tbody></table>
Note: at this point we’re using a table for layout, and it’s nasty as all hell. I don’t recommend that you structure your markup this way.
But, this example illustrates is how you can use a FORM element inside of a TABLE and have it actually work. And, more importantly, now we know why we were getting an InvalidAuthenticityToken error when everything seemed like it should have worked.
Hope this helps anyone else in the same situation.
Great post! Really helped me out.
Thanks, this solved my problem.
Thanks, this post pointed me in the right direction (although my problem was actually a JavaScript error rather than invalid markup).
Hey, youre the goto expert. Thanks for hnaingg out here.