Extending form_for in Rails 3 with your own methods
April 25, 2011At LRDesign, we have a bunch of internal tools to make laying out Rails views more consistent. I recently upgraded and improved some of ours for Rails 3, and published them as a gem. (The published / open source ones are available at https://github.com/LRDesign/lrd_view_tools, if you're interested). One of the handy techniques we figured out (poring through the Rails code) is how to correctly add a method to FormBuilder so that you can properly use it inside a form_for block.
An example method added to forms:
Since I nearly always want <input> and <label> tags at the same time, I created a labeled_input method that lets me say this (in HAML):
= form_for(@book) do |f| = f.labeled_input :title = f.labeled_input :author = f.labeled_input :price
to get:
<form action="/books/new">
<div class="labeled_input">
<label for="book_title">Title:</label><input id="book_title" name="book[title]" type="text" />
</div>
<div class="labeled_input">
<label for="book_author">Author:</label><input id="book_author" name="book[author]" type="text" />
</div>
<div class="labeled_input">
<label for="book_price">Price:</label>
<input id="book_price" name="book[price]" type="text" />
</div>
</form>Combined with some default CSS code in our application template that aligns the <label>s and <input>s in columns, this saves us a couple of hours setting up clean-looking forms on every new project, while significantly shortening and prettifying our view templates. (Markup Haiku, just like HAML intended.)
Implementing the extension in Rails 3
The code that handles form_for in Rails 3 is rather dense and incomprehensible and takes a while to pore through. Here's the short version to understanding it so you can add your own methods to FormBuilder properly. Since we dug through it, hopefully this will save others some time. The only Rails file you care about for this purpose is actionpack-3.0.x/lib/action_view/helpers/form_helper.rb.
module ActionView::Helpers::FormHelperdefines a bunch of helpers, likelabel,text_field, etc. that define helpers you use outside of aform_for. For example,text_field(@user, :title)calls this version of the helper.class ActionView::Helpers::FormBuilderis what's used to define the helpers you run inside a form_for. It works automatically via metaprogramming ... when loaded, it finds each helper inFormHelper(except for a few) and defines a similarly named method inFormBuilder.form_for(@user) { |f| f.text_field(:title)calls this version of the helper, which basically just calls the FormHelper version but passes the FormBuilder's @object_name as an additional first argument. In version 3.0.7, this metaprogramming happens on lines 1131-1141 of form_helper.rb.- As a result, if you were to write a new helper in
ActionView::Helpers::FormHelperthat uses the same argument structure as the pre-built ones, you'd automatically get both kinds of helper. However, if you're writing your own plugin or gem and injecting new helpers, this won't happen because by the time you inject your method FormBuilder will have already done its metaprogramming (it happens when the file is loaded). - The solution to this is that your gem needs to do the second half - defining the
FormBuilderversion of the helper - itself. I'll put an example below. - Most of the helper methods work by instantiating InstanceTag, a local one-size-fits-all class to emit a form tag, and then calling the appropriate method for the kind of tag that's wanted, like
to_text_field_tag. It's very confusing why the Rails team decided to do one class for InstanceTag and a bunch of different methods, rather than make subclasses of InstanceTag for each kind of tag they want; an odd OOP decision, but that's what we've got. - InstanceTag itself has only one line: it includes InstanceTagMethods, a model that defines all the methods for the class, and which isn't used elsewhere.
So to implement a FormBuilder method yourself that you can use inside a form_for, the best way is to inject your method inside FormHelper, and then call that from a method you inject into FormBuilder. This gives you both versions of the method, in the same structure that Rails defines them. You could do this either in a helper file directly in your application, or in a gem (like we have) so you can reuse your form helpers in more than one projects.
An example implementation.
Here's a simplified construction of the labeled_input method we use at LRD. This one just emits a label and a text field and wraps them in a <div>.
Start by defining the helper:
module LRD module FormHelper def labeled_input(object_name, method, options = {}) input = text_field(object_name, method, options) label = label(object_name, method, options) content_tag(:div, (label+input), { :class => 'labeled_input' } end end end ActionView::Helpers::FormHelper.send(:include, LRD::FormHelper)
This will successfully define labeled_input that you can use outside of a form_for.
Now add the FormBuilder version:
To get it working inside of a form_for, you need to add a similar method to ActionView::Helpers::FormBuilder. As mentioned above, Rails does this automatically for its own FormHelper methods using a metaprogramming approach. But since that has already happened by the time your code can inject into FormHelper, you have to do it yourself. The solution we used is to make our own FormBuilder module that manually defines the labeled_input method in the same format that FormBuilder would have done, and then auto-include that into FormBuilder when our own FormHelper module gets included. Add this stuff to the above code block:
# Inside LRD::FormHelper, add this method: def self.included(arg) ActionView::Helpers::FormBuilder.send(:include, LRD::FormBuilder) end module LRD::FormBuilder # ActionPack's metaprogramming would have done this for us, if FormHelper#labeled_input # had been defined at load. Instead we define it ourselves here. def labeled_input(method, options = {}) @template.labeled_input(@object_name, method, objectify_options(options)) end end
In practice, our labeled_input method is much more complex; it handles other input types, can add instructional comments/notes to the field, and can accept a block if you want to put something other than an <input> where the text field normally goes. This guide should get you started to writing your own form_for methods quickly, but if you want to see how to do more complex things, check out the full version.
Adding more input types or other tags.
If you wanted to add an entire different tag or input type (as opposed to combining different ones, the way labeled_input does), you would probably start by building a module that you inserted into InstanceTag or InstanceTagMethods. It should define a method like MyInstanceTagModule#to_some_funky_tag() in parallel with to_input_field_tag().
Testing it with rSpec 2
Another challenge we faced was writing specs for labeled_input's behavior. It's a bit of a trick because we needed to instantiate ActionView and render some templates to check the output, but rspec-rails is written with the assumption that you will be loading an entire rails project and all the rails gems. If you want to spec just a view helper, you need to load a bunch of rspec-rails's files one by one, and then manually include RSpec::Rails::ViewExampleGroup into RSpec's configuration. We may write a separate post on this process in the future, but in the meantime, take a look at lrd_view_tools' spec_helper file and example spec for labeled_input to get the sense of it.
Add A Comment