Build forms with FormBuilder

Forms are pretty much the heart of the web. Almost everything a user inputs is done through a form. So, when building forms in rails, you want to make the experience as easy for you as possible.

I know there's gems out there that tries to make your life easier to build forms (formtastic, simple_form, etc.) but did you know about ActionView::Helpers::FormBuilder?

Let me take a simple signup form and show you how form builder can be used to easily customize a form.

A simple form

# views/user/new.html.erb
<%= form_for User.new do |f|

  <%= f.errors.full_messages.to_sentence if f.errors.any? %>

  <%= f.text_field :username, placeholder: "Your username" %>
  <%= f.email :email, placeholder: "E-mail"%>
  <%= f.password :password, placeholder: "Password" %>

  <%= f.submit %>

<% end %>

Here's how it would look like without CSS customization.

This is pretty much as simple as it gets. There's 3 fields, one submit button and the errors are shown above if there are any.

Needs for a more complex form

As soon as the form start to get complex, you will get a lot of <%= %> tags inside your views. This makes it harder to read your code.

Instead of giving all the responsibility to the view for building the form, it's possible to use custom form builder to make it easier to build complex forms while keeping everything easy to maintain.

Create a formbuilder for your user

First, create a new folder named forms in app/ and create a new file.

# apps/forms/user_form_builder.rb
class UserFormBuilder < ActionView::Helpers::FormBuilder
end

Let's now use this builder with the signup form

# views/user/new.html.erb
<%= form_for User.new, builder: UserFormBuilder do |f|

  <%= f.errors.full_messages.to_sentence if f.errors.any? %>

  <%= f.text_field :username, placeholder: "Your username" %>
  <%= f.email :email, placeholder: "E-mail"%>
  <%= f.password :password, placeholder: "Password" %>

  <%= f.submit %>

<% end %>

The form is identical to the previous one, no surprise here.

The difference between the first form and the second is that now you have two different places where you can customize your form. From the view and from the form builder itself. This has a few benefits.

The builder really shines when you need to deal with conditionals and complex structure.

The view, on the other hand, will be useful to tell the builder what to build.

Said like this, it probably doesn't mean much to you. So let's build a form builder for the signup.

Customize fields

When using your own builder, you can take extra steps that would be otherwise impossible due to the abstract nature of the default form builder. Here's an example.

Let's take line #6 of views/user/new.html.erb.

<%= f.text_field :username, placeholder: "Your username" %>

Here's another version that would be much easier to read and yet convey enough information to understand the meaning.

<%= f.username placeholder: "Your username" %>

Here's how you implement this new method on your UserFormBuilder.

# apps/forms/user_form_builder.rb
class UserFormBuilder < ActionView::Helpers::FormBuilder

  def username(html_options = {})
    css = %w(username)
    content_tag :div, class: css do
      text_field :username, html_options
    end
  end

end

Exception raised!

If you tried to run your new form right away, you'll have an exception. Something along those lines:

ActionView::Template::Error (undefined method `content_tag' for #<UserFormBuilder:0x007f83ffae42c0>)

The reason is that the form builder is an object that doesn't include methods you'd normally use inside a view. No worry, you have two options that are, in my opinions, equally good.

The first is to use @template variable which is a reference to your current view context. This is how rails documentation suggest to use it.

The second, the one I personally use, is to use method_missing(method, *args, &block) and proxy the method to the templates if it can respond to them.

# apps/forms/user_form_builder.rb
class UserFormBuilder < ActionView::Helpers::FormBuilder

  def username(html_options = {})
    css = %w(username)
    content_tag :div, class: css do
      text_field :username, html_options
    end
  end

  def method_missing(method, *args, &block)
    @template.send(method, *args, &block)
  end

end

Now try this version, and celebrate your new, custom-made method on your form builder!

Add label to the custom field

So, you want to add label to your field. Normally, you'd do that in the view. But because we're using a custom form builder, we could leverage that.

# views/user/new.html.erb
<%= form_for User.new, builder: UserFormBuilder do |f|

  <%= f.errors.full_messages.to_sentence if f.errors.any? %>

  <%= f.username placeholder: 'Required', label: 'Username' %>
  <%= f.email placeholder: 'Required', label: 'E-mail' %>
  <%= f.password placeholder: 'Required', label: 'Password' %>

  <%= f.submit %>

<% end %>

The form builder would check for the label key in html_options and if it exists, it will create a label and use the value of the key as the label's content.

# apps/forms/user_form_builder.rb
class UserFormBuilder < ActionView::Helpers::FormBuilder

  def username(html_options = {})
    css = %w(username)
    content_tag :div, class: css do
      label_content = html_options.delete(:label)
      content = [
        text_field(:username, html_options)
      ]
      
      labelize_input! :username, content, label_content unless label_content.nil?

      content.join.html_safe
    end
  end

  def email(html_options = {})
    css = %w(email)
    content_tag :div, class: css do
      label_content = html_options.delete(:label)
      content = [
        super(:email, html_options)
      ]
      
      labelize_input! :email, content, label_content unless label_content.nil?

      content.join.html_safe
    end
  end

  def password(html_options = {})
    css = %w(password)
    content_tag :div, class: css do
      label_content = html_options.delete(:label)
      content = [
        super(:password, html_options)
      ]
      
      labelize_input! :password, content, label_content unless label_content.nil?

      content.join.html_safe
    end
  end

  private

  def method_missing(method, *args, &block)
    @template.send(method, *args, &block)
  end

  def labelize_content!(name, content, value)
    content.insert 0, label(name, value)
  end
end

Result form

Some explanations

In the last code snippet, there might be things that aren't obvious, let me try to simplify this.

The first thing that might be weird is how I use an array to build DOM elements. The reason I do this is because only the returned value is included.

That means, if I wouldn't wrap everything in an array, only the last item would be render in HTML.

Calling super in email and password method is done because the form builder already has those methods and we want to reuse the tag generator.

Remove the default error behavior

As I discussed in a previous post, I like to remove the default behavior for wrapping an input <div> container as it often limits my flexibility when it comes to presenting an error in a form.

# config/application.rb
module YourApp
  class Application < Rails::Application

    config.action_view.field_error_proc = Proc.new do |html_tag, instance|
      html_tag
    end

  end
end

Now, your fields that contain errors will be identicals to the one that don't. Fortunately, it can be done quite easily with form builders.

Customize errors

Taking the signup form above, let's implement it so that every fields are always wrapped in a container. This way, if an input has an error, the builder will simply add an error class to the container.

# apps/forms/user_form_builder.rb
class UserFormBuilder < ActionView::Helpers::FormBuilder

  def username(html_options = {})
    css = %w(username)
    css << 'error' if @object.errors.has_key?(:username)

    content_tag :div, class: css do
      label_content = html_options.delete(:label)
      content = [
        text_field(:username, html_options)
      ]
      
      labelize_input! :username, content, label_content unless label_content.nil?

      content.join.html_safe
    end
  end

end

By only adding class to existing DOM element, your structure doesn't change and it makes it easy to handle the changes through CSS Selector.

For this example, I'm going to add a red border through CSS (I've kept the CSS simple just to illustrate my point).

.error > input {
  border-color: red;
}

And the result, if a form with some errors would have to be rendered.

Now, you know how to create a form using a custom form builder.

Get on the list!

Every time I write a post here, I always wanted to add some context around what I write about. Unfortunately, I can't do it here because it would dilute the content and would make it harder for you to to follow the narrative.

So I decided to start a mailing list to talk about this kind of things. To talk about why I chose this method over other known methods, and maybe more example and stories related to the content of my post.

If you enjoyed this post, you will greatly benefit from joining my mailing list as every post will be sent to you by mail with exclusive content for your benefit.

Get on it!