rails
(8,900 views)

Replaceable render engines for Formtastic

One of the things I love about Formtastic is the semantic DSL for specifying a form in my code. One of the things that can frustrate me is the resulting DOM that is generated.

I recently forked the Formtastic project and took a shot at extracting the render code from the semantic DSL. Worked on it some more over the weekend at RailsCampNE.

While you can often use CSS to work through the DOM structure and get the form to look how you want, it's not always the way I would have done it, and there are cases that just aren't possible. So I decided to refactor Formtastic. At first I looked at just subclassing the builder but decided to fork the project. I made some decisions where to draw the line between DSL and "render", and split the rendering into a separate module.

Anyway, please have a look:  http://github.com/linoj/formtastic/blob/render/RENDERER.textile
and let me have any comments or suggestions.

Example

A frequent question on the Formtastic google group is something like "how do i show two or more fields inline".

When Formtastic renders fields, say a first, middle, and last name input string in a view like this:

<%= form.input :first_name, :label => 'First', :input_html => {:size => '15'} %>
<%= form.input :middle_name, :label => 'Middle', :input_html => {:size => '5'} %>
<%= form.input :last_name, :label => 'Last', :input_html => {:size => '15'} %>

 you get something like this:

<li id="user_first_name_input" class="string required">
  <label for="user_first_name">First<abbr title="required">*</abbr></label>
  <input id="user_first_name" name="user[first_name]" size="15" type="text">
</li>
<li id="user_middle_name_input" class="string required">
  <label for="user_middle_name">Middle<abbr title="required">*</abbr></label>
  <input id="user_middle_name" name="user[middle_name]" size="5" type="text">
</li>
<li id="user_last_name_input" class="string required">
  <label for="user_last_name">Last<abbr title="required">*</abbr></label>
  <input id="user_last_name" name="user[last_name]" size="15" type="text">
</li>

 

As you'd expect, your styles are setup so each <li> displays each form field on separate lines.

If you want to group these fields, you can use input.fields to create a fieldset like so,

<% form.inputs 'Full Name' do %>
  <%= form.input :first_name, :label => 'First', :input_html => {:size => '15'} %>
  <%= form.input :middle_name, :label => 'Middle', :input_html => {:size => '5'} %>
<%= form.input :last_name, :label => 'Last', :input_html => {:size => '15'} %>
<% end %>

which generates,

<fieldset class="inputs">
  <legend><span>Full Name</span></legend>
  <ol>
    <li id="user_first_name_input" class="string required"
      <label for="user_first_name">First<abbr title="required">*</abbr></label>
      <input id="user_first_name" name="user[first_name]" size="15" type="text">
    </li>
    <li id="user_middle_name_input" class="string required">
      <label for="user_middle_name">Middle<abbr title="required">*</abbr></label>
      <input id="user_middle_name" name="user[middle_name]" size="5" type="text">
    </li>
    <li id="user_last_name_input" class="string required">
      <label for="user_last_name">Last<abbr title="required">*</abbr></label>
      <input id="user_last_name" name="user[last_name]" size="15" type="text">
    </li>
  </ol>
</fieldset>

The fields are enclosed in a fieldset.

To then force these onto the same line, you'd float the input items so they're inline rather than on separate lines. And then adjust the CSS so the fieldset and legend don't really look like a fieldset and legend. If you've ever tried this, you know it's usually a lot of uninvited work.

Use a Formtastic ERB Renderer

Using my Formtastic ERB renderer this is a bit easier and more intuitive.

First, install my fork of Formtastic . Then configure the initializer to use the ERB renderer.

File: config/initializers/formtastic.rb

Formtastic::SemanticFormBuilder.renderer = Formtastic::ErbRenderer

Now we can specify alternative partial templates for the inputs, such as

<% form.inputs :partial => 'inline_items' do %>
  <%= form.input :first_name, :partial => 'inline', :label => 'First', :input_html => {:size => '15'} %>
  <%= form.input :middle_name, :partial => 'inline', :label => 'Middle', :input_html => {:size => '5'} %>
  <%= form.input :last_name, :partial => 'inline', :label => 'Last', :input_html => {:size => '15'} %>
<% end %>

What we've done is specified the inputs should use partial "inline_items" and the input use partial "inline". These might be defined as follows:  

File: app/views/formtastic/_inline_items.html.erb

<li class="<%= wrapper[:class] %>" >
  <label><%= legend -%></label>
  <div><%= contents %></div>
</li>

File: app/views/formtastic/_inline.html.erb

<p  id="<%= wrapper[:id] %>" class="<%= wrapper[:class] %>" >
  <%= label -%><br />
  <%= input -%><br />
  <%= inline_errors -%>
</p>

So now when we render the form, we get

<li class="inputs">
  <label>Full Name</label>
  <div>
    <p id="user_first_name_input" class="string required">
      <label for="user_first_name">First<abbr title="required">*</abbr></label><br>
      <input id="user_first_name" name="user[first_name]" size="15" type="text"><br>
    </p>
    <p id="user_middle_name_input" class="string required">
      <label for="user_middle_name">Middle<abbr title="required">*</abbr></label><br>
      <input id="user_middle_name" name="user[middle_name]" size="5" type="text"><br>
    </p>
    <p id="user_last_name_input" class="string required">
      <label for="user_last_name">Last<abbr title="required">*</abbr></label><br>
      <input id="user_last_name" name="user[last_name]" size="15" type="text"><br>
    </p>
  </div>
</li>

 so the inputs group is wrapped in a <div> and the items are in <p> tags. That looks palatable.

The CSS to make this inline may look like:

form.formtastic li.inputs div {
  margin-left: 25%;
}
  
form.formtastic li.inputs div label {
  display: inline;
  float: none;
}
     
form.formtastic li.inputs div p {
  float: left;
  margin-left: 0;
  margin-right: 1em;
}

Of course, this is just one example. In another case I need my form fields to be in arranged in a table. And so on. The point is we're no longer constrained by the hard-coded DOM structure generated by Formtastic.


 

 

 

 

 

Comments

by Jeff on Mar 21, 2010

Formtastic Feedback?

This looks pretty cool and addresses my frustration with Formtastic. What feedback have you gotten from the Formtastic maintainers/community?

by admin on Mar 21, 2010

>>

No feedback yet, and I'd love to see more discussion. But I know they're focusing on getting to 1.0, so I understand if the refactoring required to implement this is outside the scope of the current project. I do plan to continue to merge any updates on Formtastic master to my branch, and ensure we pass all the master rspecs.

by Justin French on Mar 22, 2010

>>

Okay, this is awesome. No idea when we'll merge this in, but this is definitely where we want to go. Great work, Justin.

by admin on Mar 31, 2010

new :structure option

NOTE: you can now do form.inputs :structure => :inline instead of the :partial option on inputs and input, and it'll automatically prepend 'inline' to the partial names it looks for ('inline_fields', '_inline_input' etc).

by Dom on Jun 24, 2010

>>

You sir, are a genius. Love your work! Thank you, thank you, thank you.

by Alex on Aug 03, 2011

>>

Hi! Excellent article of the precise problem I am dealing with in Formtastic at the moment in my project. Do you know if Justin French has added this into Formtastic 2.0 rc3?

New Comment

markdown formatting permitted