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 setmodel: {whatever the instance varaible}
.If you have a form for a resource that does not have an associated Model, use
from_with
and setscope: {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 defineurl: '{whatever form submission route}'
. Or you can create the form by hand, the good-ol way.
Top comments (0)