Larry Wall famously wrote, “The three chief virtues of a programmer are: Laziness, Impatience[,] and Hubris.” (Editor: added Oxford comma. Come at me, bro.) These three traits are the reason you hate writing the same blocks of HTML over and over again, and the reason for your frustrations with HTML frameworks like Bootstrap. Because this is a lot of typing just to display a simple form field:
<div class="form-group">
<label for="exampleInputFile">File input</label>
<input type="file" id="exampleInputFile">
<p class="help-block">Example block-level help text here.</p>
</div>
It’s these three “virtues” that led developers to create Formtastic and Simple Form, and it’s these same virtues which might lead you to roll your own solution as well. If that’s the direction you’re headed, read on.
For the purpose of this demonstration, we’ll be using Bootstrap, partly because of it’s popularity and familiarity to most developers, and partly because its verbosity will highlight what we’re trying to accomplish.
In the end, we want to be able to go from writing this…
<div class="form-group">
<%= f.label :title, "Post Title" %>
<%= f.text_field :title, class: "form-control", placeholder: "The title of this post" %>
</div>
…to writing this…
<%= f.text_field :title, label: "Post Title", input_options: {placeholder: "The title of this post"} %>
…and get the same output.
To do this, we can use Rails’ FormBuilder
class and bend it to our will.
To begin, we need to create a new FormBuilder
subclass. This class could be stored somewhere under the /app
directory, but it makes a little more sense to keep it in the /config/initializers
directory since we’re using it to configure the display of our forms.
Create a new file in /config/initializers
called bootstrapped_form_builder.rb
with the following content:
# /config/initializers/bootstrapped_form_builder.rb
class BootstrappedFormBuilder < ActionView::Helpers::FormBuilder
end
With this file created, we can already start using it. We just need to define it as our form’s builder
:
<%# /app/views/authors/new.html.erb %>
<%= form_for @author, builder: BootstrappedFormBuilder do |f| %>
<div class="form-group">
<%= f.label :name %>
<%= f.text_field :name, class: "form-control", placeholder: "Name" %>
</div>
<div class="form-group">
<%= f.label :email %>
<%= f.email_field :email, class: "form-control", placeholder: "Email address" %>
</div>
<div class="form-group">
<%= f.label :bio %>
<%= f.text_area :bio, class: "form-control", placeholder: "A little about the author" %>
</div>
<div class="form-group">
<%= f.label :role %>
<%= f.select :role, [["Writer","writer"],["Author","author"],["Commenter","commenter"]] %>
</div>
<div class="form-group">
<%= f.label :active %>
<%= f.check_box :active %>
</div>
<%= f.submit class: "brn btn-success" %>
<% end %>
When we restart the server and reload the page, we’ll see that the output for our form is exactly the same as the default builder.
Now that we’ve established that our new FormBuilder
works, let’s start changing things. To start, we’ll override the text_field
method. It’s the most heavily used in forms and other input types such as “email”, “phone”, and “search” are built from it.
Looking at the Rails API, we see that text_field
takes two arguments: method
and options={}
. Furthermore, the base FormBuilder class provides us with five instance variables we can use to our advantage:
@object_name
: The name of the object (shocking, I know). In our case it would be “Author”.@object
: The object provided to the form_for
method. Ours would be Author
.@options
: The options provided to the form_for
method.@proc
: the block provided to the form_for
method.@template
: The current view. This object provides access to any method you would normally use in your view.We want to simplify the creation of the form-control
blocks while at the same time retaining the flexibility of being able to tweak each component in the block.
To do that, we’ll override the text_field
method:
def text_field(method, options={})
label_text = options.fetch(:label, method.to_s.humanize)
label_options = options.fetch(:label_options, {})
input_defaults = {class: "form-control"}
input_options = merge_options(input_defaults, options.fetch(:input_options, {}))
@template.content_tag :div, class: "form-group" do
@template.label(@object_name, method, label_text, label_options) +
super(method, input_options)
end
end
We want the #label
and #text_field
to continue accepting the same parameters as before, and in the code above we’re doing that by nesting hashes specific to the label and textfield in the options
hash (:label
, :label_options
, and :input_options
). We could further add a :wrapper_options
hash to apply to the wrapping div
, but you get the idea.
The input_options
variable on the fifth line is being set to the return value of a merge_options
method. That’s a private method we must use to concatenate values set in the view to default values we define for the text_field
method:
private
def merge_options(defaults, new_options)
(defaults.keys + new_options.keys).inject({}) {|h,key|
h[key] = [defaults[key], new_options[key]].compact.join(" ")
h
}
end
In the text_field
method, it concatenates our default CSS class
with any new CSS classes we added in the view.
Example:
<%= f.text_field :author, {input_options: {class: "emphasized"}} %>
…is transformed to…
<div class="form-group">
<label for="author_name">Name</label>
<input type="text" id="author_name" class="form-control emphasized" name="author[name]">
</div>
This is a good start. We’ve overridden the default Rails text_field
to be wrapped in a Bootstrap form-group
div
, automatically included the label
, and applied a default class to the input. Since so many other methods derive from text_field
, let’s look at how we can take this initial method and DRY it out.
HTML5 added many new input types, all of which are merely a variation of the typical “text” field. Because of the similarity, we can create a private method to handle all the duplicate work and the call it from the methods that handle those input types.
private
def text_layout(method, options, defaults={})
label_text = options.fetch(:label, method.to_s.humanize)
label_options = options.fetch(:label_options, {})
input_defaults = merge_options({class: "form-control"}, defaults)
input_options = merge_options(input_defaults, options.fetch(:input_options, {}))
@template.content_tag :div, class: "form-group" do
@template.label(@object_name, method, label_text, label_options) +
yield(method, input_options)
end
end
As you’ll notice, the text_layout
method is exactly the same as our text_field
method, with two exceptions
defaults
hash to allow element-specific attributes for our different element types.super
.Let’s change our text_field
to use this new method:
def text_field(method, options={})
text_layout(method, options, {class: "text-specific-class"}) do |method, input_options|
super method, input_options
end
end
We can see that nothing changed in the way we call text_field
– it still takes a method
and an options
hash. Furthermore, we’re now giving all text_field
elements a default text-specific-class
CSS class. Lastly, it’s sending the text_layout
method a message to super
.
We can override email_field
just as easily:
def email_field(method, options={})
text_layout(method, options) do |method, input_options|
super method, input_options
end
end
Again, there’s nothing new here, just a change to the method call. We can do the same for date_field
, phone_field
, url_field
, and the rest of the text-based input fields.
Selects, check boxes, radio buttons, and file fields each have a different method signature from one another and from that of text fields. Because of this, it makes more sense to define them individually than to try and shoehorn them into a generic method. The body of the method follows the pattern already laid out in the text_field
, but the signature will need to be changed accordingly. An example of a select box is shown below:
def select(method, choices=nil, select_options={}, input_options={}, &block)
label_text = options.fetch(:label, method.to_s.humanize)
label_options = options.fetch(:label_options, {})
input_defaults = {class: "form-control"}
input_options = merge_options(input_defaults, options.fetch(:input_options, {}))
@template.content_tag :div, class: "form-group" do
@template.label(@object_name, method, label_text, label_options) +
super(method, choices, select_options, input_options, &block)
end
end
As shown above, you need to pass form_for
the builder every time you use it. If you plan to use your FormBuilder by default you have a couple of options.
The first option is to create a helper method that sets some defaults and then passes those along to form_for
.
def bootstrapped_form_for(object, options={}, &block)
defaults = {builder: BootstrappedFormBuilder}.merge(options)
form_for object, options, &block
end
As you can see here, we’ve just copied the signature of the form_for
method. In it, we combine our builder with the options passed in and then pass along everything else. Both Simple Form and Formtastic do something along these lines with their semantic_form_for
and simple_form_for
.
Another method is to make form_for
use your builder by default. To do that, just add the following line to your app/helpers/application_helper.rb
file.
ActionView::Base.default_form_builder = YourFormBuilder
Of course, by doing this you’ll no longer have access to the Rails default FormBuilder. If you need that – and you probably will – just add a new builder named RailsFormBuilder
to the initalizers like so:
# config/initializers/rails_form_builder.rb
class RailsFormBuilder < ActionView::Helpers::FormBuilder
end
Then, when you need it, you can just set it as your builder in the form_for
:
<%= form_for @author, builder: RailsFormBuilder do |f| %>
<% end %>
You can use the super class instead of subclassing it, but I find typing ActionView::Helpers::FormBuilder
more difficult than RailsFormBuilder
, and also more difficult to remember.
This wouldn’t be a very respectable Ruby article if it didn’t cover the topic of testing (no, that’s not an invitation to find all my Ruby articles which don’t cover testing). Below you will find a “stub” of a test you can use for your own purposes:
# test/initializers/bootstrapped_form_builder_test.rb
require "test_helper"
class TestHelper < ActionView::Base; end
class BootstrappedFormBuilderTest < ActiveSupport::TestCase
let(:builder) {
BootstrappedFormBuilder.new(:author, Author.new, TestHelper.new, {})
}
describe "#text_field" do
it "does something" do
builder.text_field(:name).must_equal "<div class=\"form-group\"><label for=\"author_name\">Name</label><input class=\"form-control bar\" type=\"text\" name=\"author[name]\" id=\"author_name\" /></div>"
end
end
end
The meat of the example is in the defining of the TestHelper
and the :builder
. ‘FormBuilder’ requires a “template” passed to its initializer, and to do that, we provide it with a new instance of ActionView::Base
which we’ve subclassed to TestHelper
. From that point onward, it’s testing as usual.
Rolling your own FormBuilder feeds into Larry Wall’s three virtues marvelously: It allows us to write less code – and HTML/CSS code at that – satisfying our virtue of laziness; our impatience is satisfied by writing code we understand instead of learning yet another DSL for yet another library; and finally, our hubris is satisfied by building it “the right way”.