#311 Form Builders pro
- Download:
- source codeProject Files in Zip (89.7 KB)
- mp4Full Size H.264 Video (26.6 MB)
- m4vSmaller H.264 Video (14.3 MB)
- webmFull Size VP8 Video (17.1 MB)
- ogvFull Size Theora Video (32.7 MB)
Rails applications often include a large number of forms. These generally follow a similar pattern which can lead to a lot of repetition in the view code. Gems such as SimpleForm, which was covered back in episode 234 and Formtastic, covered in episodes 184 and 185, are available to help with this but in this episode we’ll show you how you can remove duplication from the view code by writing your own form builder from scratch.
The page we’ll be working with is the form for editing a Product
record.
There are a variety of fields on this form. Let’s dive in to the source code and see how we can clean its code up using a custom form builder. There’s a quite a bit to the template but for now we’ll concentrate on the section below that generates form fields for some of the model’s attributes.
<div class="field"> <%= f.label :name %><br> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :price %><br> <%= f.text_field :price %> </div> <div class="field"> <%= f.label :description %><br> <%= f.text_area :description, rows: 5 %> </div> <div class="field"> <%= f.check_box :discontinued %> <%= f.label :discontinued %> </div>
There’s a pattern to this code. Each field is has a div
with a class
of field
followed by a label and the form field itself. We can make this code more concise by using a form builder and we’ll write one to extend text_field
so that it outputs the label and the wrapper div
as well. The first step is to create a form builder. We’ll call it labelled_form_builder
and put it in a new form_builders
directory under /app
.
class LabelledFormBuilder < ActionView::Helpers::FormBuilder def text_field(name, *args) @template.content_tag :div, class: "field" do label(name) + @template.tag(:br) + super end end end
This class must inherit from ActionView::Helpers::FormBuilder
. We can override any of this class’s methods here and we’ve overridden text_field
. This method needs to take a name attribute and any other arguments that might be passed in. In the method we use content_tag
to generate the wrapper div
. We can’t call helper methods like this directly but as we have access to a @template
instance variable we can call them through it. In the div
we call label to generate the label and here we can call the method directly. In the view we’d use f.label
but here f
is the form builder we’re already in so we don’t need it. Finally we use content_tag
to generate a br
element then call super to call the same method in the class’s superclass to generate the text field.
Whenever we call text_field
in the view now it will generate the wrapper div and the label too so we can remove these and just call text_field
.
<%= f.text_field :name %> <%= f.text_field :price %> <div class="field"> <%= f.label :description %><br> <%= f.text_area :description, rows: 5 %> </div> <div class="field"> <%= f.check_box :discontinued %> <%= f.label :discontinued %> </div>
For this to work we need to tell the form to use our custom form builder. We do this by using form_for
’s builder option.
<%= form_for @product, builder: LabelledFormBuilder do |f| %> # Form omitted <% end %>
By doing this the f
variable that’s passed to the block will be an instance of LabelledFormBuilder
. Once we’ve restarted the server so that the form_builders
directory is picked up we can reload the form and looks just like it did before.
Extracting Other Field Types
We can apply this technique now to other fields in the form such as the description
text area. We could copy the text_field
method that we wrote and rename it text_area
but if we want to use this technique for every time of input control the code would soon become repetitive. Instead we can use a little meta-programming to eliminate this repetition.
class LabelledFormBuilder < ActionView::Helpers::FormBuilder %w[text_field text_area password_field collection_select].each do |method_name| define_method(method_name) do |name, *args| @template.content_tag :div, class: "field" do label(name) + @template.tag(:br) + super(name, *args) end end end end
In the code above we take each of the method names we want to override and list them in an array. Then we loop through each name and pass it to define_method
to define an instance method with that name. We have to pass the same arguments to this method’s block that we passed to our text_field
method and inside the block is the same code we had in text_field
with one difference. The arguments passed in to the block can be implicit or explicit and so we specify these same argument in the call to super
.
There are some fields where we don’t want this behaviour such as checkboxes where we display the label after the input field and there’s no line break. For controls like these we will need to define a separate method.
class LabelledFormBuilder < ActionView::Helpers::FormBuilder %w[text_field text_area password_field collection_select].each do |method_name| define_method(method_name) do |name, *args| @template.content_tag :div, class: "field" do label(name) + @template.tag(:br) + super(name, *args) end end end def check_box(name, *args) @template.content_tag :div, class: "field" do super + label(name) end end end
We can now clean up this part of the view in the same way as we did the textboxes.
<%= f.text_field :name %> <%= f.text_field :price %> <%= f.text_area :description, rows: 5 %> <%= f.check_box :discontinued %>
When we reload the page now it still looks the same as before, but now our view is much cleaner.
Adding Custom Labels
What if we need to customize this behaviour even more? For example, what if we want to change the label on the price
field so that it says “Unit Price” instead of “Price”? It would be good if we could add a label
option to our customized form fields so that we can change the label’s text. Rails provides a handy method that we can pass to an array of arguments called extract_options
. This method returns an options
hash at the end of the arguments or a generated new empty hash if no options have been passed in. We can pass this to the label
and it will customize the text. We’ll need to do this for each type of form field and so we’ll write a new method in the class to do this and use it in each overridden method.
class LabelledFormBuilder < ActionView::Helpers::FormBuilder %w[text_field text_area password_field collection_select].each do |method_name| define_method(method_name) do |name, *args| @template.content_tag :div, class: "field" do field_label(name, *args) + @template.tag(:br) + super(name, *args) end end end def check_box(name, *args) options = args.extract_options! @template.content_tag :div, class: "field" do super + " " + field_label(name, *args) end end private def field_label(name, *args) options = args.extract_options! label(name, options[:label]) end end
We can now use the label
option in our price field.
<%= f.text_field :name %> <%= f.text_field :price, :label => "Unit Price" %> <%= f.text_area :description, rows: 5 %> <%= f.check_box :discontinued %>
When we reload the page now the price
field’s label has changed.
There’s a problem with this, though. The options we pass in fall through to the text field and are rendered as attributes so our price
textfield now has a label
attribute.
<div class="field"> <label for="product_price">Unit Price</label> <br /> <input id="product_price" label="Unit Price" name="product[price]" size="30" type="text" value="29.95" /> </div>
To fix this we override the objectify_options
method in our form builder. All this method needs to do is pass on the options to the same method in the superclass having first removed the label
option.
def objectify_options(options) super.except(:label) end
The next time we reload the page the label
attribute will have gone.
Indicating Required Fields
Another useful thing we can do with a form builder is read a model’s validations and alter the form fields to display information about required fields. For example in the Product
model the name
and price
fields are required and it would be useful if we could let the user know this automatically.
class Product < ActiveRecord::Base has_many :categorizations has_many :categories, through: :categorizations validates_presence_of :name, :price end
In our form builder we can determine the required fields by reflecting on the validations. We’ll add a required
CSS class to each required field’s label.
def field_label(name, *args) options = args.extract_options! required = object.class.validators_on(name).any? { |v| v.kind_of? ActiveModel::Validations::PresenceValidator } label(name, options[:label], class: ("required" if required)) end
To determine if a field is required we fetch the object
that the form is for and then get its class
. We can then call validators_on
on that and pass in the name of the current attribute. This returns an array of validators for that attribute and we use any?
to see if any of these are a required field validator by checking each validator’s class. If there is a matching validator we add the required
class to the label.
When we reload the page now the name
and price
fields’ labels are displayed in bold as we’ve already added some CSS for the required
class.
Error Messages
The four fields shown above are now look great in the view template but there are still other areas of the form that we can clean up. One such area is the area of the code that displays the error messages. We want to display error messages in the same way on each form so we can move the code that displays the error messages into a form builder.
<% if @product.errors.any? %> <div class="error_messages"> <h2><%= pluralize(@product.errors.count, "error") %> prohibited this product from being saved:</h2> <ul> <% @product.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %>
We’ll replace this with an error_messages
method similar to that which was available in earlier versions of Rails.
<%= f.error_messages %>
There’s quite a lot of code in this method but it’s fairly simple. Most of it adds tags, duplicating what we had in the view code.
def error_messages if object.errors.full_messages.any? @template.content_tag(:div, :class => 'error_messages') do @template.content_tag(:h2, "Invalid Fields") + @template.content_tag(:ul) do object.errors.full_messages.map do |msg| @template.content_tag(:li, msg) end.join.html_safe end end end end
We’re using content_tag
a lot in our form builder and we have to go through the @template
instance variable each time we call it. We can use a delegation to tidy this up a little so that calls to content_tag
and tag
are automatically delegated to @template
meaning that we can remove all the references to @template.
class LabelledFormBuilder < ActionView::Helpers::FormBuilder delegate :content_tag, :tag, to: :@template %w[text_field text_area password_field collection_select].each do |method_name| define_method(method_name) do |name, *args| content_tag :div, class: "field" do field_label(name, *args) + tag(:br) + super(name, *args) end end end def check_box(name, *args) options = args.extract_options! content_tag :div, class: "field" do super + " " + field_label(name, *args) end end def error_messages if object.errors.full_messages.any? content_tag(:div, :class => 'error_messages') do content_tag(:h2, "Invalid Fields") + content_tag(:ul) do object.errors.full_messages.map do |msg| content_tag(:li, msg) end.join.html_safe end end end end # Private methods omitted. end
The Final Parts
We’ve almost finished cleaning up our form template now. There are just a couple of things left to do. The first is to add a submit
form builder which includes the wrapper div
we have in the template.
<div class="actions"> <%= f.submit %> </div>
We can do this in a similar way to our other methods by writing a submit
method.
def submit(*args) content_tag :div, class: "actions" do super end end
This leaves us with one more thing to clean up in the template, this section of code that displays the product’s categories.
<div class="field"> <%= hidden_field_tag "product[category_ids][]", nil %> <% Category.all.each do |category| %> <%= check_box_tag "product[category_ids][]", category.id, @product.category_ids.include?(category.id), id: dom_id(category) %> <%= label_tag dom_id(category), category.name %><br> <% end %> </div>
This code creates a list of checkboxes for a many-to-many relationship and we created this in the revised version of episode 17. If you want to see exactly how this is done then take a look at that episode. This code is a good candidate for moving into a form builder as it’s quite complex and we can refactor it better there.
We want this method to behave just like collection_select
because it has a similar function, i.e. listing associated records. We’ll replace this code with a collection_check_boxes
method that will take a name, a collection of items and id
and name
attributes to use to uniquely define each item.
<%= f.collection_check_boxes :category_ids, Category.all, :id, :name %>
The code necessary to do this is rather complicated. We’ll just put it in one method but if this code was going to be used in a production application we’d probably refactor parts of it out into separate methods.
def collection_check_boxes(attribute, records, record_id, record_name) content_tag :div, class: "field" do @template.hidden_field_tag("#{object_name}[#{attribute}][]") + records.map do |record| element_id = "#{object_name}_#{attribute}_#{record.send(record_id)}" checkbox = @template.check_box_tag("#{object_name}[#{attribute}][]", record.send(record_id), object.send(attribute).include?(record.send(record_id)), id: element_id) checkbox + " " + @template.label_tag(element_id, record.send(record_name)) end.join(tag(:br)).html_safe end end
This does the same thing that the equivalent view code did, looping through all the records and generating a checkbox and a label for each one. When we reload the page now the form looks just as it did before and we can still assign and remove categories to a product.
Our form looks a lot cleaner now and if we need to use many-to-many checkboxes in another part of our application it’s easy to use our form builder method again. We need to use the builder
option to specify the builder at the top of each form in which we use our custom form builder methods, however, and this isn’t very pretty. To help with this we can write our own custom form_for
helper method and use this instead. We’ll call ours labelled_form_for
.
<%= labelled_form_for @product do |f| %> <%= f.error_messages %> <%= f.text_field :name %> <%= f.text_field :price, :label => "Unit Price" %> <%= f.text_area :description, rows: 5 %> <%= f.check_box :discontinued %> <h2>Categories</h2> <%= f.collection_check_boxes :category_ids, Category.all, :id, :name %> <%= f.submit %> <% end %>
We’ll write our helper method in the ApplicationHelper
.
module ApplicationHelper def labelled_form_for(object, options = {}, &block) options[:builder] = LabelledFormBuilder form_for(object, options, &block) end end
This method adds the LabelledFormBuilder
option to form_for
so that we don’t need to include it each time.
That’s it for this episode. We now have a fully-working form that hasn’t changed much in appearance but which now has a much cleaner template thanks to our custom form builder. Before writing your own custom form builder don’t forget to take a look at SimpleForm and Formtastic. These both help you to do something similar to what we’ve done here and may well save you the trouble of having to write your own form builder.