DEV Community

Cover image for From HTML to Simple Form: anatomy of Rails forms
Matouš Borák for NejŘemeslníci

Posted on • Edited on

From HTML to Simple Form: anatomy of Rails forms

In our main project, we like to use the 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 about our initial experience with View Components, you may remember that we decided to not use View Components 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:

<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>
Enter fullscreen mode Exit fullscreen mode

This HTML may be equally coded in a template (we use a Slim template syntax here) like this:

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!"
Enter fullscreen mode Exit fullscreen mode

Rails form tag helpers

We can get the same HTML output (except negligible details) using the Rails form tag helpers. 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:

= 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!"
Enter fullscreen mode Exit fullscreen mode

Rails form helpers

OK, now we’re getting to more interesting stuff: Rails form helpers, form_with or its older cousin form_for, make a bigger change, especially when the form is bound to an model object, such as @user:

= 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!"
Enter fullscreen mode Exit fullscreen mode

The form_with form helper used here does at least the following:

  • it checks 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 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 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 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:

# 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
Enter fullscreen mode Exit fullscreen mode

…and use it in a form template like this:

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

…which generates the following HTML output:

<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>
Enter fullscreen mode Exit fullscreen mode

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 just provides a custom Rails form builder. The same holds for other Rails-based form builder gems, such as Formtastic or bootstrap_form (as opposed to Rails-independent form building gems such as 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 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 to the main model object or generic collections.

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

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

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“, 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, and obey them manually when rendering the field. The Rails documentation has an example 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. Cheers!

Top comments (0)