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.
I tried to do this for my site (I need something different, but I was trying to see if I could add methods to the FormHelper, but I get the following error:
undefined method `labeled_input’ for #<#:0xa301114>
on the line
@template.labeled_input(@object_name, method, objectify_options(options))
of the labeled_input method on the FormBuilder module. Why could this be? It seems the object being used in the form_for block is not a FormBuilder? I don’t get it. The relevant parts of the view I’m using it in is this:
Pedro,
Might be a difference between rails 3.1 and 3.2? All of my projects are 3.1 at the moment, I haven’t tested this technique in 3.2 yet.
I’m using Rails 3.1.3. I just noticed that the view code that I posted didn’t render. Maybe it was because of the tags. Anyway, it was only a simple form; I’ll try with HAML:
- form_for @user, … do |f|
…
= f.labeled_input :email
…
= f.submit “Send”
Maybe it has to do something with my module file? Here it is: custom_forms_helper.rb:
module CustomFormsHelper
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
def self.included(arg)
ActionView::Helpers::FormBuilder.send(:include, CustomFormsHelper::FormBuilder)
end
end
module FormBuilder
def labeled_input(method, options = {})
@template.labeled_input(@object_name, method, objectify_options(options))
end
end
ActionView::Helpers::FormHelper.send(:include, CustomFormsHelper::FormHelper)
end
I tried to follow the tutorial closely, but I’m not certain I did it correctly. Thanks for any help.
Oh, and I’m including it in my application_controller file.
There’s some important information missing on this blog post, and I wish I knew what it was. I have tried copying exactly what is in this post as well as the configuration on the lrd_view_tools github, and still I get “undefined method for ActionView::Helpers::FormBuilder.” I wish I knew what I was misunderstanding! Or perhaps something is different for rails 3.2.3?
Is this one of those places you need to switch from @template to view_context? I’m just getting started with it, but had to do that in a controller the other day in the app I’m updating from rails 2.3.8 to 3.2.6.
I am trying to figure out where the ActionView::Helpers::FormHelper.send(:include, LRD::FormHelper) line should go.
Does it go in the file containing the LRD module? If so, what actually executes it?
Like everyone else in this thread who thought the code seemed a great idea, I cant get it to work!
@craig, you can put this in any file that gets loaded; for example you could put it all in a file inside config/initializers, or in lib/ and have your application require it during initialization. The AV::Helpers::FormHelper.send() line can be executed anytime after the class FormHelper class is defined. As written, it goes just as shown above: in the same file as the FormHelper definition. When that file is loaded, first the class is defined and then afterwards that line runs to include it into ActionView. It’s all in the same file.
Alternatively, you can do what we did, and package the whole thing in a gem. You’re welome to install our gem (lrd_view_tools), or follow the pattern to create your own if you’re doing something different. You can see the (somewhat more complicated) version we use here: https://github.com/LRDesign/lrd_view_tools/blob/master/lib/app/helpers/lrd_form_helper.rb
@pedro – I got your problem and I figured it out. The posted code is missing one crucial line
ActionView::Base.send(:include, LRD::FormHelper)
I figured it out by reading the referenced source and noticing an extra include (thanks Evan)
Enjoy
Add A Comment