Using form_for in Helper Methods

Writing good Ruby and Rails code (actually any code for that matter) involves keeping your code reasonably DRY (“don’t repeat yourself”). For many situations Rails allows you to do this using helper methods, e.g. the methods defined in the ApplicationHelper module.

In an attempt to follow this guideline I recently ran into the problem of using the form_for helper inside such a method to generate a form based on some parameters. Let me justify this: The standard approach to DRY up larger parts of views like forms actually is to use partials. You don’t however want to put too much logic into a partial either. Logic actually belongs into the controller and I highly recommend to prefer a combination of controller logic and partials over the method presented in this article. Sometimes however you want to keep your controllers as skinny as possible or don’t want to include certain code in it for other reasons. Also it can be beneficial to readability of your code to have the generation of your form all in one place.

Note: Googling around also suggests using custom FormBuilders for DRY up forms and while this is completely legitimate it is somewhat besides the point of this article and offers way more flexibility than needed in the situation I am in.

So the goal of this article ist to write a helper method fancy_form that frees us from repeatedly typing something like

<% form_for @instance do |f| %>
  <%
    # do some very complicated stuff with models or other data that generally does not belong into a view template and possibly also not the controller...
  %>
  <%= f.label :attribute %>
  <%= f.text_field :attribute %>
  <!-- Possibly more form fields -->
<% end %>

and instead lets us write

<%= fancy_form @instance, possible_other_parameters %>

Now, unfortunately you cannot simply put the following snippet into your ApplicationHelper module:

module ApplicationHelper
  def fancy_form(instance, other_parameters = {})
    form_for instance do |f|
      # Perform said complicated operations...
      f.label :attribute
      f.text_field :attribute
      # Possibly more form fields
    end
  end
end

Instead of generating the desired form, a call to this helper generally will mess up our markup. I still don’t quite understand what happens, so don’t beat me if I am wrong here, but basically the problem is that by calling the above helper, our output is not generated in the same way or order as when parsed by the ERB engine.

If we put a line like

<%= foo %>

into our template foo is evaluated and the return value of this statement is placed in the generated HTML. So what does our helper method return? Obviously not what we expect. form_for and the FormBuilder methods don’t seem to return the code they generate, but rather write it directly to the screen, so to say. The ActionView::Helpers::CaptureHelper method capture allows us to “suppress” this behavior for the scope of a given block and returns the generated output in a simple String. Our second attempt thus looks like this:

module ApplicationHelper
  def fancy_form(instance, other_parameters = {})
    capture do
      form_for instance do |f|
        # as above
      end
    end
  end
end

Our markup is intact again but where is the form? It still won’t show up. The problem is that capture puts you into a context similar to the one generated by the <% ... %> ERB syntax, where you cannot directly output something to the generated code. The solution to this problem however is not a secret: We’re going to wrap every call whose output we want in our HTML with a call to the concat method (ActionView::Helpers::TextHelper):

module ApplicationHelper
  def fancy_form(instance, other_parameters = {})
    capture do
      form_for instance do |f|
        # exemplarily
        concat f.label(:attribute)
        concat f.text_field(:attribute)
      end
    end
  end
end

And there you have it. A simple form generating helper method! Note that using concat alone (i.e. without capture) you could already use the helper like this:

<% fancy_form @instance, possible_other_parameters %>

but this does not express the intent to output the code of a form into our view and thus should not be used in my opinion.

Disclaimer

Note that I am by no means a Rails expert and the above text only mirrors my understanding of the situation. I did not study the Rails sources extensively to confirm these claims. Also there may be better solutions.

Oh and yes, I am probably the last man on earth still using Rails 2.3.8. 😛