DEV Community

Ayaz
Ayaz

Posted on

A guide to creating forms in Ruby on Rails 6

In any app, forms serve the most primary way for data creation and modification and Ruby on Rails (from here on called Rails) app is no different.

In this guide, we will look at different ways of creating forms in Rails.

A High-Level Overview

At a high level, there are two ways of creating forms in Rails.

1) We can create the entire form by hand using HTML just as we would in any static HTML file.

2) Alternatively, we can use a powerful form_with helper that makes our lives much easier. This helper can have the following use cases.

  • It can be used to set up a form for a resource that is associated with an Active Record model meaning it has REST routes and also a database model. It can be both for resource creation or update.

  • It can be used to create a form for a resource that doesn't have any Active Record model, meaning it has REST routes but no database model.

  • It can be used for setting up a form having no resource and Active Record model, meaning there are neither REST routes nor any database model. Such a form can also be created by hand as point 1 illustrates but using form_with makes it easier still.

Now let's explore form creation in little more detail.

Form created with regular HTML markup

If you have done any HTML, then this one is the simplest and the most straightforward way of creating a form in Rails. Here the form is created with the usual HTML markup.

<form method="post" action="/users">
  <label for="email">Email</label>
  <input type="email" name="email" id="email">

  <label for="password">Password</label>
  <input type="password" name="password" id="password">

  <input type="submit" name="Sign up">
</form>

In such a form you define everything by yourself and don't take any help from Rails. You have to declare method="post" and set the form submission route action="/users" yourself. So far there is nothing Rails specific here.

Form Created with form_with method (The Rails Way)

Rails conveniently offer us a form_with method to help us in creating a form. With this method, we leverage the Rails magic.

Let's try to understand how this method works.

form_with in action

Suppose we have a User model with email and password attributes. If we want to create a user signup form (resource creation) this is how the form would look like.

 <%= form_with(model: @user, local: true) do |f| %>

  <%= f.label :email %>
  <%= f.email_field :email %>

  <%= f.label :password %>
  <%= f.password_field :password %>

  <%= f.submit "Sign Up" %>

<% end %>

When erb is converted to HTML through Rails asset pipeline it produces:

<form action="/users" accept-charset="UTF-8" method="post">

  <label for="user_email">Email</label>
  <input type="email" name="user[email]" id="user_email">

  <label for="user_password">Password</label>
  <input type="password" name="user[password]" id="user_password">

  <input type="submit" name="commit" value="Sign Up" data-disable-with="Sign Up">

</form>

I have skipped the input type="hidden" name="authenticity_token" tag which Rails add automatically to every form to prevent CSRF attacks.

Let's dissect the form bit by bit!

form_with is a method to which we have provided a configuration options hash. model: @user defines for which model we want to create the form. Or in other words which object will drive the form creation.

local: true configures the form submission request to be local form request. In the absence of this option, Rails configures the form to send a remote XHR request on submission.

form_with automatically sets the method="post" and the submission route action="/users". But how do Rails know which route to submit the form to and what method (HTTP Verb) to use?

Let's understand this now.

form_with method with POST request (resource creation)

If the user is signing up for the first time meaning the user doesn't exist in the database yet, then the form will be served by the User#new action in which we initialize @user = User.new.

This @user will be available in the User#new view which contains sign up form. Currently, all @user attributes are set to nil, since we didn't initialize attributes in User.new.

Thus when we set model: @user, Rails checks if any of the attributes of @user have a value other than nil. If all the fields are nil then it infers this user (resource) will be created for the first time, so it correctly sets the method="post".

But what about the route?

In order to set the route rails calls @user.class to determine the Model class name which is User, looks for a similarly named Controller class User and maps to the correct action at which the user will be created, which would be User#create for which method is POST and route is /users. And that is how it finds out where to submit this form.

If you name instance variable something else, such as @foobar = User.new and set model: @foobar it would still correctly determine the submission route by calling @foobar.class which would still be User. So instance variable name doesn't matter.

But if the Rails automatically inferred route is not where you want to submit the form, you can always use url: {whatever the submission route}. With this attribute to the form_with you have full control over the submission route, in case you need it.

form_with method with PATCH request (resource update)

The second case is where you are creating a user edit form. Here when you click a particular user link that needs an edit, the request will be mapped to User#edit where you will fetch the user as @user = User.find(params[:id]). This @user will be available in the User#edit view.

But since this time @user attributes have values, rails prepopulate corresponding form fields with available values taking care of not populating password field, so it filters that out.

Also because @user attributes have values (even if one attribute has a value other than nil) rails infers that it is not a new resource meaning you are not creating it for the first time but instead you are looking to modify an existing resource.

So it sets up the route properly by again calling the @user.class to determine Model, find its namesake Controller User and map to User#update which deals with updating a user, having the route users/:id with a PATCH request.

But, there is a problem the browser doesn't natively support the PATCH request it only supports POST AND GET.

Rails achieve that by inserting the following tag inside of the form

<input type="hidden" name="_method" value="patch">

How does that work?

POST simply allows the browser to send data to the server. Form submission piggybacks that to transport data to the server but after the data is transferred it needs to tell Rails that it's not a POST data but a PATCH data and it accomplishes that by using such an input tag.

It simply acts as a label to tell Rails that it's a patch request and use incoming data as an edit data, not a creation data.

When such a request hits the Rails app, it looks at the form find the hidden input field with a value="patch" and routes it to the User#update which handles PATCH request. So a lot of work is done by Rails.

Let's take some other intricacies about the form_with method

Form submitting to itself (GET and POST route are the same)

If you create a random instance variable such as @have_no_class that has no associated Model. So when Rails try to determine the submission route it calls @have_no_class.class it gets NilClass and since it couldn't correctly infer the submission route for this resource it would set the route to itself.

So if you landed on the page with GET request to '/signup' then it would submit the form to itself at '/signup' with POST. Unless the Rails did actually determine the route and it turned out to be the same as the GET route.

form_with gives a FormBuilder object

form_with takes an options hash but it also takes a block and passes a FormBuilder object as a block variable.

<%= form_with(model: @user, local: true) do |f| %>

|f| is the FormBuilder object (you can name it anything you want) that helps us in setting up form fields. It has methods for creating all the form elements such as radio button, text-area, check-box, input etc.

So if you want to create an input of type=password you would do

<%= f.label :password %>
<%= f.password_field :password %>

It will produce

<label for="user_password">Password</label>
<input type="password" name="user[password]" id="user_password">

Here we gave :password symbol as an argument to the f.label method which it uses to set up the label text and for="user_password". for is set to user_password and not password because we set model: @user so it prefixes it with the name of the model.

Similarly in the input tag for password, it sets the id="password", type="password" and name="user[password]". In order to understand user[password] we have to look at the params hash.

A look at the Params hash

You have to be mindful of the fact the Rails use the params hash for sending any data that is being transferred with the request, be it dynamic URL segment like /:id or query param like ?page=1 or the data sent via POST or PATCH request, all are available inside the params hash. In simple words params contain some information about the incoming request.

What Rails does is that it sets up user hash inside of the params hash and inside the user hash it sets up all the form name, value pairs.

When the form is submitted it takes all the form values, stuffs them in the user hash of the params hash and sends it to the server.

params: {
  user: {
    email: 'foobar@baz.com',
    password: 'foobar'
  }
}

That is why you see the name="user[password]" because it is setting up the password field on the user hash and setting its value to whatever is typed into the field. But this is all done behind the scenes by Rails.

Now coming back to the FormBuilder object |f|.

You can additionally set up any other attribute on input password tag (or any other form field) by passing in the options hash such as setting up the class attribute.

<%= f.password_field :password, class: 'form-control' %>

FormBuilder object, in our case |f| has convenient methods to which you pass configurations and it produces the desired form element for you.

form_with method for a non-model resource

form_with can also be used to set up a form for a resource that has no Model. It means it has some or all of the REST routes but no Model to persist the data. A classic example would be creating Session as a REST resource but with no Model, because data is sent to the browser and not stored on the server.

In such a form creation you don't say model: @session because there is no Model to instantiate the object which can be passed into view. Instead, we use scope: 'session'. This does all the Rails magic steps as discussed earlier.

Before we have been using model attribute now we use scope. We use model when we have the Model whose instance is going to drive the creation of the form.

With Session since we don't have a Model we don't use model attribute. Instead, we use scope that sets up the session hash inside of params hash and all the form values will be stuffed inside of the session hash.

Since this time we don't have an instance variable to determine the class and the route, the form submits to itself (same route it is served from).

Key Takeaways

  • If you have a form to either create or update a resource that happens to have an associated Model as well, use from_with and set model: {whatever the instance varaible}.

  • If you have a form for a resource that does not have an associated Model, use from_with and set scope: {whatever the scope name you want in params hash}.

  • If you want to create a non-resource form, you can do so with a form_with in which case you would have to define url: '{whatever form submission route}'. Or you can create the form by hand, the good-ol way.

Top comments (0)