form_with — Building HTML forms in Rails 5.1

A couple of months ago a PR created by kaspth was merged to rails 5.1, introducing the form_with view helper. The idea behind it is to unify the interface of form_tag and form_for by extracting both implementations to a common object. As long as developers start using this new helper, form_tag and form_for will softly get deprecated.

A little bit of background

Let’s say that you have a Post model and want to create a new instance of it. In Rails 4, the form_for view helper allows you to create a form for it:

<%= form_for @post do |form| %>
<%= form.text_field :author %>
<%= form.submit “Create” %>
<% end %>

The variable form yielded to the block is a FormBuilder object that incorporates the knowledge about the model object represented by :post passed to form_for.

What happens when you need to create a form but you don’t have an underlying model instance? Rails provides us with the form_tag helper:

<%= form_tag “/posts” do %>
<%= text_field_tag “post[author]” %>
<%= submit_tag “Create” %>
<% end %>

There is no FormBuilder object in this second form. For each field that you need to add to the form, you need to use the correspondent input tag, such as text_field_tag.

form_with

As you can see in the example above, both scenarios build an HTML form for creating a post. The difference between form_for and form_tag is that the former has an underlying model instance while the latter does not.
Now let’s take a look at how we could accomplish the same result using form_with:

<%= form_with model: @post do |form| %>
<%= form.text_field :author %>
<%= form.submit “Create” %>
<% end %>

Clearly, both ways of building the form look pretty much the same. The only difference lies in telling form_with if we are using a model or a url for building the form.

Let’s take a look at some of the changes introduced by form_with.

Forms are remote by default

By default form_with attaches the data-remote attribute to the form. As a consequence, the form will be submitted using Ajax.

Thus, there is no need to specify remote: true anymore. In case we want to disable this feature, we need to set the option local to true:

<%= form_with model: @post, local: true do |form| %>

This suggested by DHH on Rails issue #25197. Reloading the whole page seems like an overkill when we can take advantage of Ajax and Turbolinks.

Adding a scope prefixes the input field names

scope is a new option you can set to form_with in order to prefix the input field names.

This is particularly useful when you want to submit params grouped to a controller.

<%= form_with url: “/posts”, scope: “post” do |form| %>
<%= form.text_field :author %>
<%= form.submit “Create” %>
<% end %>

If you were using form_tag, you would have to include this scope as part of each input field name in the form.

Fields don’t have to correspond to model attributes

In my opinion this is one of the nicest additions that keeps a unified interface.
Let’s say that you want to add a check box to your post creation form that allows the author of the post to notify his readers via email.

This check box could not map to an attribute in the model. Still, you can add it using the FormBuilder instance:

<%= form_with model: @post do |form| %>
<%= form.text_field :author %>
<%= form.check_box :notify_readers %>
<%= form.submit “Create” %>
<% end %>

Take into account that this new param will be scoped to post. If you don’t want it to be scoped, you will have to fallback to check_box_tag.

No more auto-generated ids and classes

Both form_for and form_tag generate automatic ids for the input fields. Even more, form_for generates a class for the form:

<%= form_for @post do |form| %>
<%= form.text_field :author %>
<%= form.submit “Create” %>
<% end %>

This is no longer the same with form_with. Ids and classes have to be specified by the developer, which is specially important in order to make labels to work:

<%= form_with model: @post do |form| %>
<%= form.label :author %>
<%= form.text_field :author, id: :post_author %>
<%= form.submit “Create” %>
<% end %>

In my opinion, the decision of removing the auto-generated ids is great. You now have complete freedom on how to design your forms.

As a counterpart, you’ll also have to code slightly more if you want to add ids to each input, which is not a big deal.

Setting HTML options

form_for and form_tag have the html option for setting HTML attributes to the form tag.

In case you want to specify the id and class for the form, you have to use the html option:

<%= form_for @post, html: { id: “custom-id”, class: “custom-class” } do |form| %>

With form_with any id and class are not wrapped in the html key:

<%= form_with model: @post, id: “custom-id”, class: “custom-class” do |form| %>

`fields` being replaced by `fields_for`

The new Action Viewfields method lets you group multiple fields together, by scoping input fields with either an explicit scope or model.

<%= form_with model: @post do |form| %>
<%= form.text_field :author %>
<% form.fields :comment do |fields| %>
<%= fields.text_area :body %>
<% end %>
<% end %>

In a similar way to form_with, a FormBuilder instance associated with the scope or model is yielded. So, all the generated field names are prefixed with either the passed scope or the scope inferred from the model. Take into account that HTML ids and classes have to be specified by the developer.

Some further readings

Software Engineer & Consultant

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store