From HTML to Simple Form: anatomy of Rails forms
Tags: rails, forms, webdev, tutorial
Published: 2022-06-17 17:08:09

In our main project, we like to use the **[Simple Form](https://github.com/heartcombo/simple_form) gem** for our forms, we have been using it for nearly 10 years, and we still appreciate its very succinct but flexible syntax. 

We also like that using Simple Form, we can build **consistently looking and structured forms** with minimum effort. If you had a chance to read our [previous post](https://dev.to/nejremeslnici/from-partials-to-viewcomponents-writing-reusable-front-end-code-in-rails-1c9o) about our initial experience with View Components, you may remember that we [decided to not use View Components](https://dev.to/nejremeslnici/from-partials-to-viewcomponents-writing-reusable-front-end-code-in-rails-1c9o#we-did-not-use-view-components-for-forms-at-all) at all for our forms. Why? Mainly because **Simple Form has its own component system** already built in and in this post we will briefly describe how it works under the hood. Also, as we will see, **the same principles are valid for all Rails form builders** so findings in this post can be helpful for understanding the default style of building forms under Rails in general.

That said, diving into the details of Simple Form forms may be a bit confusing at first because it’s syntax looks so different from the default Rails form helpers. For example, in Simple Form, a single line of code may define all of: the form input field, its label, a hint or even the corresponding validation error message. So how does Simple Form actually generate the final HTML markup?

## Anatomy of a Simple Form (and Rails) form

To show this, let’s try a bottom-up approach. We’ll start with pure HTML and will build gradually, layer by layer, the same form using more and more abstracted syntax until we reach the Simple Form style. This will hopefully help us clarify the underlying concepts.

### HTML

A basic form may be rendered like the following in the final HTML:

```html
<form action="/users" method="post">
  <div>
    <label for="name">Enter your name: </label>
    <input type="text" name="name" id="name">
  </div>
  <div>
    <label for="email">Enter your email: </label>
    <input type="email" name="email" id="email">
  </div>
  <div>
    <input type="submit" value="Subscribe!">
  </div>
</form>
```

This HTML may be equally coded in a template (we use a [Slim template](http://slim-lang.com/) syntax here) like this:

```slim
form action="/users" method="post"
  div
    label> for="name" Enter your name:
    input#name type="text" name="name"
  div
    label> for="email" Enter your email:
    input#email type="text" name="email"
  div
    input type="submit" value="Subscribe!"
```

### Rails form tag helpers

We can get the same HTML output (except negligible details) using the [Rails form tag helpers](https://guides.rubyonrails.org/form_helpers.html#using-tag-helpers-without-a-form-builder). The word ”tag“ here illustrates that these are helper functions that can each do only one thing – **render a particular HTML tag**. In fact we are just using a slightly different syntax to render our form than before while the code structure stays the same:

```slim
= form_tag("/users", method: :post)
  div
    = label_tag :name, "Enter your name: "
    = text_field_tag :name
  div
    = label_tag :email, "Enter your email: "
    = text_field_tag :email
  div
    = submit_tag "Subscribe!"
```

### Rails form helpers

OK, now we’re getting to more interesting stuff: Rails form helpers, **[`form_with`](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with)** or its older cousin [`form_for`](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for), make a bigger change, especially when the form is [bound to an model object](https://guides.rubyonrails.org/form_helpers.html#binding-a-form-to-an-object), such as `@user`:

```slim
= form_with(model: @user) do |f|
  div
    = f.label :name, "Enter your name: "
    = f.text_field :name
  div
    = f.label :email, "Enter your email: "
    = f.text_field :email
  div
    = f.submit "Subscribe!"
```

The [`form_with` form helper](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with) used here does at least the following:

- it [checks](https://guides.rubyonrails.org/form_helpers.html#relying-on-record-identification) whether `@user` is a new or an existing record and generates the proper route (action and method) for the form,
- it recognizes that the `@user` has e.g. its `name` attribute set and prefills the `name` field with this value,
- and it follows the [Rails conventions](https://guides.rubyonrails.org/form_helpers.html#understanding-parameter-naming-conventions) to name the fields (e.g. with the `name="user[name]"` attribute) so that the submitted POST data can be nicely processed in the target controller.

Note that **we still have to layout the form by ourselves**: we define the form structure and Rails form helpers take care of the field / form tags. This pattern is the default in Rails and gives us great flexibility – we can structure each form any way we like – but perhaps makes it a bit **harder to keep the forms consistent**, especially when we have many complex forms (as is typical in a web admin section) because we have to keep all the form templates structure the same, too. To achieve greater consistency, we could get some help from Rails form builders.

### Rails form builders

A Rails form builder is an **object used by Rails to build forms**. It is instantiated in the form helper `form_with` / `form_for` and is yielded in the form block. In the code sample above, the `f` variable is a Rails form builder object. The builder methods (`text_field`, `label`, etc.) know about the form model object (`@user`) and use the form tag helpers to render the corresponding fields, labels or other elements, potentially wrapped with arbitrary HTML tags. The [default Rails form builder](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html) though serves just as a simple proxy to the form tag helpers and that is why the overall form HTML structure is, by default, fully on the developer.


#### Custom form builder

Now, we can create a **[custom form builder](https://guides.rubyonrails.org/form_helpers.html#customizing-form-builders)** to, for example, tighten the rendered form HTML. The simplest option is to derive it from the default Rails form builder. There are a few internal variables that we need to be aware of when working with form builders: `@template` represents the view context that we use to call the underlying tag helper methods, `@object` is the model object (`@user`) and `@object_name` is its name (`"user"`). 

So, for example, to support a possibility to wrap a text field and a label together with a wrapper div, we can define the following custom builder:

```ruby
# app/form_builders/labelling_form_builder.rb
class LabellingFormBuilder < ActionView::Helpers::FormBuilder
  def text_field(attribute_name, options = {})
    @template.content_tag(:div, class: "wrapper") do
      label(attribute_name, options[:label]) + 
         super(attribute_name, options.except(:label))
    end
  end
end
```

…and use it in a form template like this:

```slim
/ app/views/users/new.html.slim
= form_with(model: @user, builder: LabellingFormBuilder) do |f|
  = f.text_field :email, label: "Enter your email"
```

…which generates the following HTML output:

```html
<form action="/users" method="post">
  <div class="wrapper">
    <label for="user_email">Enter your email</label>
    <input type="text" name="user[email]" id="user_email" />
  </div>
</form>
```

Let’s see what is going on here:

- we inherit our custom builder class `LabellingFormBuilder` from the default Rails form builder
- we override its method called `text_field` so that it renders both label and the input field itself
- we can pass a custom label text via our custom option key `:label`
- both label and input are wrapped with a wrapper div, note that we have to use the `@template` variable the a view context for calling Rails view helpers
- in the template, we specify our custom builder with the `builder` option and pass the custom option for the label

OK, we successfully **amended the generated fields HTML structure using a custom form builder** and now we can build a text field including its label using a single line of code and have both elements wrapped with a div that we can style.

Also note that **we’ve actually created an initial version of a ”component“ to build forms** – the code for the layout and structure of the form HTML is defined in a single place in the code base. We can change this one place – the custom builder – and it will affect all forms using that builder.

Overall, custom form builders make it possible to:

- alter the way input fields and their accompanying HTML elements are rendered and structured in the form,
- add methods for special types of fields,
- define a specific API for building forms with a concrete structure and layout, thus helping forms consistency,
- use all of ruby syntax to define a logic around our forms, define presets / default values, styles and so on.

### Simple Form (and others)

By now, we should have enough information to understand how the Simple Form gem works. Technically, **[Simple Form](https://github.com/heartcombo/simple_form) just [provides](https://github.com/heartcombo/simple_form/blob/main/lib/simple_form/form_builder.rb) a custom Rails form builder**. The same holds for other Rails-based [form builder gems](https://www.ruby-toolbox.com/categories/rails_form_builders), such as [Formtastic](https://github.com/formtastic/formtastic/blob/master/lib/formtastic/form_builder.rb) or [bootstrap_form](https://github.com/bootstrap-ruby/bootstrap_form/blob/main/lib/bootstrap_form/form_builder.rb) (as opposed to Rails-independent form building gems such as [Forme](https://github.com/jeremyevans/forme)). 

So, once again, Simple Form is nothing but a custom Rails form builder that comes with a rich set of features around it:

- Using the builder’s helper methods, especially the one called `input` we can generate everything related to a form field: the field itself, its label, hint or error message.
- Or anything else, actually! Simple Form makes no assumptions about the form HTML markup and the form structure is [fully configurable](https://github.com/heartcombo/simple_form#configuration) in a single place – the Simple Form initializer file.
- It also provides a set of custom fields with their own logic for processing and rendering model data. Notably, Simple Form supports various automatic processing of [records associated](https://github.com/heartcombo/simple_form#associations) to the main model object or generic [collections](https://github.com/heartcombo/simple_form#collections).

With Simple Form, our sample form could be encoded in a template like this:

```slim
= simple_form_for(@user) do |f|
  = f.input :name, label: "Enter your name"
  = f.input :email, label: "Enter your email"
  = f.button :submit, "Subscribe!"
```

Note how everything about a form field is defined in a single line (and we only touched the surface here). **Simple Form is great if we prefer consistency** when building our forms. By configuring Simple Form [”wrappers“](https://github.com/heartcombo/simple_form#the-wrappers-api), we can define the default structure and layout of all our forms (plus, of course, alternative form versions if we need them). Moreover, all layout options can be **overridden in a particular form or field** so we don’t lose any flexibility if we need that.

### Mixing different types of form helpers together

As a final note, we can **mix different layers** of form building helpers, even in a single form. For example, nothing would stop us if we tried to render a form using Simple Form but used a Rails form tag helper for some particular field that must be handled specially. 

Doing so, we only have to think about Rails conventions, especially those for [naming form fields](https://guides.rubyonrails.org/form_helpers.html#understanding-parameter-naming-conventions), and obey them manually when rendering the field. The Rails documentation has [an example](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with-label-Mixing+with+other+form+helpers) showing how to mix form layers.

## Summary

We went through all layers from the bottom of the generated HTML to the custom form builders and Simple Form. In a future post, we will build on this information to describe how we created a custom Rails form builder to conveniently allow styling our new admin forms with Tailwind.

_If you don’t want to miss future posts like this, follow me here or [on Twitter](https://twitter.com/boramacz). Cheers!_