Andy from Webcrunch

Subscribe for email updates:

Portrait of Andy Leverenz
Andy Leverenz

January 6, 2021

Last updated November 5, 2023

How to use FormBuilder in Ruby on Rails

Ruby on Rails ships with form helpers based in Ruby out of the box. These helpers are helpful for dynamically generating HTML fields on forms that pertain to the data layer backed by ActiveRecord in a given application.

When generating a new resource in rails a common path for many developers is to scaffold a new model. A scaffold contains everything you need from the data side to the front end views. This practice is a big time saver for a developer looking to be extremely productive.

When generating such a resource there is commonly a new form view partial included. The form is how data goes from an end user to a database. There's a lot to know about how Rails handles forms in a given app but this guide is focused on customizing the default form helper methods and/or creating your own from scratch.

Goals of automation with Tailwind CSS

In the video I reference my kickoff_tailwind Ruby on Rails application template. The template uses Tailwind CSS for stylesheets. Tailwind is a highly productive CSS framework but it comes with a lot of repetitive qualities that inspired me to create a custom FormBuilder.

Ultimately, the goal here is to allow any new resource that gets generated come already set up with Tailwind CSS classes. Doing this would allow me to not have to go back and add them every time.

My template comes with some form styles by default but they still need to be applied manually. Having a FormBuilder such as this could automate that.

Creating a custom FormBuilder class

Inside the app directory of a Ruby on Rails app you can create custom extensions in addition to what comes stock. This folder gets auto-loaded so there's very little to configure outside of whatever you're adding.

For the purposes of this guide I'll make a new folder called builders. Inside it I'll create a new file called tailwind_builder.rb.

This file will inherit from what's already part of the framework.

# app/builders/tailwind_builder.rb

class TailwindBuilder < ActionView::Helpers::FormBuilder
  # include ActionView::Helpers::TagHelper
  # include ActionView::Context

  def text_field(attribute, options={})
    super(attribute, options.reverse_merge(class: "input"))
  end

  def text_area(attribute, options={})
    super(attribute, options.reverse_merge(class: "input"))
  end

  def select(object_name, method_name, template_object, options={})
    super(object_name, method_name, template_object, options.reverse_merge(class: "select"))
  end

  def div_radio_button(method, tag_value, options = {})
    @template.content_tag(:div,
      @template.radio_button(
        @object_name, method, tag_value, objectify_options(options)
      )
    )
  end
end

In this file you can invent new form helpers or extend existing helpers using the super() method. Since my goal is to only modify the class attribute on the generated HTML I'll need to extend existing fields to accommodate.

If you look at the source code for a text_field form helper for example, you can get a sense of what parameters need to be passed through.

# https://github.com/rails/rails/blob/914caca2d31bd753f47f9168f2a375921d9e91cc/actionview/lib/action_view/helpers/tags/text_field.rb

# frozen_string_literal: true

require "action_view/helpers/tags/placeholderable"

module ActionView
  module Helpers
    module Tags # :nodoc:
      class TextField < Base # :nodoc:
        include Placeholderable

        def render
          options = @options.stringify_keys
          options["size"] = options["maxlength"] unless options.key?("size")
          options["type"] ||= field_type
          options["value"] = options.fetch("value") { value_before_type_cast } unless field_type == "file"
          add_default_name_and_id(options)
          tag("input", options)
        end

        class << self
          def field_type
            @field_type ||= name.split("::").last.sub("Field", "").downcase
          end
        end

        private
          def field_type
            self.class.field_type
          end
      end
    end
  end
end

There is a lot to unpack here but the line tag("input", options) is about all we need to know.

Unfortunately, there is little documentation on the API docs so I would suggest using the actual code as your guide.

There is a large list of form helper tags you can view source on via Github to get a better sense of what's going on under the hood.

Putting the builder to use

We haven't declared the builder in our view just yet so nothing should change from the default rendering. To do so you can simply pass a builder: TailwindBuilder option on the form.

For this guide I generated a Post model scaffold by running

rails g scaffold Post title content:text

And modified the form in app/views/posts/_form.html.erb.

<%= form_with(model: post, builder: TailwindBuilder) do |form| %>
<% if post.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>

    <ul>
      <% post.errors.each do |error| %>
        <li><%= error.full_message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

<div class="mb-6">
  <%= form.label :title %>
  <%= form.text_field :title %>
</div>

<div class="mb-6">
  <%= form.label :content %>
  <%= form.text_area :content %>
</div>

<%= form.div_radio_button :title, "Test" %>


<div class="actions">
  <%= form.submit %>
</div>
<% end %>

With this code in place our basic text_field and text_area fields not have an input class I created prior that stems from a style sheet in my template.

/* app/javascript/stylesheets/components/_forms.scss */

%focus-style {
  @apply shadow outline-none border-gray-500;

  box-shadow: 0 0 0 0.2rem theme("colors.gray.200");
  background-clip: padding-box;
}

.input {
  @apply appearance-none block w-full text-gray-700 border border-gray-400 rounded px-3 leading-tight bg-white shadow-inner;
  padding-top: .65rem;
  padding-bottom: .65rem;
}

.input:focus,
.input:hover {
  @extend %focus-style;
}

...

Opting into a form builder app-wide

Adding a form builder option to every form you create could get tedious and repetitive. Luckily, there is a way to enable a default builder on the application configration level.

# app/config/application.rb

require_relative "boot"

require "rails/all"

Bundler.require(*Rails.groups)

module CustomFormBuilder
  class Application < Rails::Application
    ...
    config.action_view.default_form_builder = TailwindBuilder
    ...
  end
end

Don't overdo it

I think form helpers solve a lot of problems for a rails developer looking to move fast. Extending them is a great way to increase speed but it comes at cost if you want more customization down the line. Generating large blocks of HTML might be overkill or it might be desired given your team size and how often you're creating form data. Hopefully this guide proved useful!

Happy Coding

Link this article
Est. reading time: 5 minutes
Stats: 8,682 views

Categories

Collection

Part of the Ruby on Rails collection

Products and courses