Using form helpers with split-up forms in Rails

Written . Tagged Ruby.

Say you have a two-column layout, where the layout looks something like:

1
2
3
4
5
6
<div id="left-column">
  <%= yield %>
</div>
<div id="right-column">
  <%= yield :sidebar %>
</div>

So your main content goes in the left column, and you can optionally specify content to go in the sidebar.

In a template using this layout, you might have a form where some parts are in the left column, some in the right:

1
2
3
4
5
6
7
8
9
10
11
12
<%# This goes in the left column: %>
<% form_for(@user) do |form| %>
  <%= form.text_field :name %>
  <%= form.submit %>
<% end %>

<%# This goes in the right column: %>
<% content_for(:sidebar) do %>
  <%# These do not work %>
  <%= form.radio_button :gender, "M" %>
  <%= form.radio_button :gender, "F" %>
<% end %>

How are you to get the radio buttons inside the form? You can’t just extend the form_for block to surround the content_for block; the form start and end tags would still be inside the left column and not encompass the right column.

This is what I would do. First, allow content to be injected into the layout before and after both columns:

1
2
3
4
5
6
7
8
<%= yield :before_content %>
<div id="left-column">
  <%= yield %>
</div>
<div id="right-column">
  <%= yield :sidebar %>
</div>
<%= yield :after_content %>

Then in the template we need to insert the start form tag before and the end form tag after:

1
2
3
4
5
6
<% content_for(:before_content) do %>
  <%= form_tag(@post) %>
<% end %>
<% content_for(:after_content) do %>
  </form>
<% end %>

Note that I’m using form_tag where I used form_for before. It seems form_for can’t be used without a block (=without creating an end tag as well).

form_tag will transform a record into a RESTful route just like form_for, but unlike that helper, it will not assign an id or class to the form, so be advised.

Now to the form fields. We’re not using form_for anymore, and even if we could have, the columns would not have been contained in its block. One solution is to use text_field_tag and friends, manually specifying the field name and default value. But thankfully, we can use fields_for.

fields_for is basically like form_for but without the form start and end tags. It’s most commonly used to mix multiple models in the same form. We can use it like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<% content_for(:before_content) do %>
  <%= form_tag(@post) %>
<% end %>
<% content_for(:after_content) do %>
  </form>
<% end %>

<%# This goes in the left column: %>
<% fields_for(@user) do |form| %>
  <%= form.text_field :name %>
  <%= form.submit %>
<% end %>

<%# This goes in the right column: %>
<% content_for(:sidebar) do %>
  <% fields_for(@user) do |form| %>
    <%= form.radio_button :gender, "M" %>
    <%= form.radio_button :gender, "F" %>
  <% end %>
<% end %>

This will set proper field names (“user[name]”, “user[gender]”) and default values (if @user has a name or gender already).

One thing to do note is that if you have other forms in the right column already (perhaps a search box), that form will of course be nested in this one and weirdness may ensue. In that case, rethink your layout.