Rails form_with tutorial
“form_with” is known as a form helper, which means it’s an abstraction to build well-known, standard HTML form.
In the need of form_with
First, we should mention why we need a helper anyway. After all, a web form is just as old as the web itself, wrapping it around a Ruby or Rails helper wouldn’t make things more complex? At first glance, you’re absolutely right.
But remember the Rails philosophy. Rails value integrated system, it means at one point we have to rely on strong collaboration between the browser and the server. And this collaboration is (partially) ensured by helpers
Without helpers, you would have to take care about :
- Security token,
- Classes and naming consistency,
Manually ensure that all pathes (URLs) exist and are always up-to-date.
The last point alone justifies the absolute need for helpers - at least for web forms.
Side note : A lot of criticism against Rails is that the tool pushes abstractions too far, which I somehow agree. How much abstraction is “good enough” for you is entirely up to you (or your tech lead).
A Rails tutorial from scratch
So let’s start a tutorial from scratch.
Tool I will use in this tutorial :
1
2
3
ruby -v # 3.3.0
bundle -v # 2.4.10
node -v # 20.9.0
Then let’s go at the root of your usual workspace, and start to build a fresh new Rails app :
1
2
3
4
5
mkdir formwith && cd formwith
echo "source 'https://rubygems.org'" > Gemfile
echo "gem 'rails', '7.1.3'" >> Gemfile
bundle install
bundle exec rails new . --force
Notice here that I don’t use the --minimal
flag - see options here. It would remove Hotwire, Stimulus, and so on - the collaboration between the browser and the server would be then different. I want to stick to plain old Rails in this tutorial.
Create a data layer
Forms are made to send data to the server, so it will make more sense to add some model and data to our app.
Let’s say we have some books with title, description, and isbn (isbn are business ID for books).
1
bin/rails generate model Book title:string body:text isbn:integer --no-test-framework
A migration file was created under db/migrate/20240131201925_create_books.rb
- the number is a timestamp, of course you will have another one.
1
2
3
4
5
6
7
8
9
10
11
class CreateBooks < ActiveRecord::Migration[7.1]
def change
create_table :books do |t|
t.string :title
t.text :body
t.integer :isbn
t.timestamps
end
end
end
A default view where to put forms
Nothing but standard Rails here. In your terminal, go at the root of your app, and paste the following commands:
1
2
3
4
5
6
7
8
9
10
11
12
13
# Create a default controller
echo "class WelcomeController < ApplicationController" > app/controllers/welcome_controller.rb
echo "end" >> app/controllers/welcome_controller.rb
# Create a default route
echo "Rails.application.routes.draw do" > config/routes.rb
echo ' get "welcome/index"' >> config/routes.rb
echo ' root to: "welcome#index"' >> config/routes.rb
echo 'end' >> config/routes.rb
# Create a default view
mkdir app/views/welcome
echo '<h1>form_with tutorial</h1>' > app/views/welcome/index.html.erb
Then create database,
1
bin/rails db:create db:migrate
Then launch your local Rails server by typing :
1
bin/rails server
And check that “form_with tutorial” is displayed in your browser at localhost:3000
An endpoint where to send data
Open the routes.rb
file, and add a path where to send the data, like this :
1
2
3
4
5
Rails.application.routes.draw do
get "welcome/index"
post "welcome/book_endpoint", as: "book_endpoint" # add this line
root to: "welcome#index"
end
And add the corresponding method in the WelcomeController :
1
2
3
4
5
6
7
class WelcomeController < ApplicationController
# Add this method
def book_endpoint
end
end
Now open your browser at localhost:3000/rails/info/routes, you should see the endpoint.
Side note : It’s better to follow REST conventions, and Rails helps to do so in the routes.rb file. For demo purpose, we stick to an explicit, simpler route where to send the data.
Build a first form with… form_with
Now it’s time to see how form_with
could help to build forms.
1
2
3
4
5
6
<%# inside app/views/welcome/index.html.erb %>
<h1>form_with tutorial</h1>
<%= form_with do |myform| %>
Form contents
<% end %>
Which renders an empty form, like this one :
1
2
3
4
5
6
7
8
9
10
<body>
<h1>form_with tutorial</h1>
<form action="/welcome/index" accept-charset="UTF-8" method="post">
<input type="hidden" name="authenticity_token" value="zOt30hNDyv2GLIUHeHmpUksAk8eujPFwbd6r7-pIpHigC6TLF9eAplosFKQxWF2N65NwKjDCurP5y3c1WYUwmw" autocomplete="off">
Form contents
</form>
</body>
- the default HTTP method is
post
- the security token is built by Rails
- the UTF-8 charset is automatically set
- In the, we have a
myform
object that will help to build data that will be sent to the server
form_with in the real world
Let’s add the required fields:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<%# inside app/views/welcome/index.html.erb %>
<h1>form_with tutorial</h1>
<%= form_with scope: "book", url: book_endpoint_path, method: :post do |myform| %>
<div>
<%= myform .label :title, "Title is:" %>
<%= myform.text_field :title %>
</div>
<div>
<%= myform.label :text, "Text is:" %>
<%= myform.text_area :text %>
</div>
<div>
<%= myform.label :isbn, "Isbn is:" %>
<%= myform.number_field :isbn %>
</div>
<%= myform.submit "Search" %>
<% end %>
Which renders the following HTML :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<body>
<h1>form_with tutorial</h1>
<form action="/welcome/book_endpoint" accept-charset="UTF-8" method="post">
<input type="hidden" name="authenticity_token" value="AsfAGgNmd52Y6AUjbhtulwVtc6Q8r9r6U0wkKRAqnwbzlpeIK0x29NOzUDpcJrxAEht6njABQ_WoCdOB0Haq_Q" autocomplete="off">
<div>
<label for="book_title">Title is:</label>
<input type="text" name="book[title]" id="book_title">
</div>
<div>
<label for="book_text">Text is:</label>
<textarea name="book[text]" id="book_text"></textarea>
</div>
<div>
<label for="book_isbn">Isbn is:</label>
<input type="number" name="book[isbn]" id="book_isbn">
</div>
<input type="submit" name="commit" value="Search" data-disable-with="Search">
</form>
</body>
- Note that “url” and “method” are explicitly set in the block declaration
- Note you can mix HTML and Rails helpers inside the form block
- Note that “name” and “id” are explicitly set for each field
- Note that for the submit field, “commit” is the default name. If there are multiple submit fields, we explicitly could set different names for each submit.
form_with using a model
Now let’s cheat for a second by using rails scaffolding.
Instead of a book, let’s say we want to create, read, update or delete a fruit.
1
bin/rails generate scaffold fruits name:string
It will generate migration, model, controller, and a view (yes, just for fruits).
now stop your local server and run
1
2
bin/rails db:migrate
bin/rails server
And go to localhost:3000/fruits/new
Open app/views/fruits/_form.html.erb
You should see something like this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<%= form_with(model: fruit) do |form| %>
<!-- Skipped the error messages here -->
<div>
<%= form.label :name, style: "display: block" %>
<%= form.text_field :name %>
</div>
<div>
<%= form.submit %>
</div>
<% end %><form action="/fruits" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="ZoY6D8B5iEd5Pp2PEWt-QHEYUgmVTQwz4omUz_e42g2DSFl-N5cZvmj54aVZVh8ZkXgmF7vs2FBjxXOsuCnctg" autocomplete="off">
<div>
<label style="display: block" for="fruit_name">Name</label>
<input type="text" name="fruit[name]" id="fruit_name">
</div>
<div>
<input type="submit" name="commit" value="Create Fruit" data-disable-with="Create Fruit">
</div>
</form>
Which is rendered like this :
1
2
3
4
5
6
7
8
9
10
11
12
<form action="/fruits" accept-charset="UTF-8" method="post">
<input type="hidden" name="authenticity_token" value="ZoY6D8B5iEd5Pp2PEWt-QHEYUgmVTQwz4omUz_e42g2DSFl-N5cZvmj54aVZVh8ZkXgmF7vs2FBjxXOsuCnctg" autocomplete="off">
<div>
<label style="display: block" for="fruit_name">Name</label>
<input type="text" name="fruit[name]" id="fruit_name">
</div>
<div>
<input type="submit" name="commit" value="Create Fruit" data-disable-with="Create Fruit">
</div>
</form>
Just from model: fruit
, Rails is able to guess :
- the endpoint URL
- the method
- the name and id of each field
- everything else is no surprise given what we saw earlier
It’s probably too much for many people, but you can see how Rails is focused on model and conventions.
Notice that you don’t “have to” always follow the Rails way, just code the way that seems more comfortable to you.
And one day maybe, you will find that following convention is easier than repeating the same boring boilerplate. But my advice is “don’t try this too early”, and maybe not at all if it doesn’t suit your mindset.
Conclusion
Trying to read at least once what happens between Rails abstraction and actual browser rendering, as we did in this article, helps to understand deeply how things works.
I didn’t try to submit a form as I did my Rails and form article, so you can try to see what happens in the Rails console as an exercise.
I hope you learned something new today!
Best,
David.