Forms are a huge part of a dynamic website. It’s through forms that you take user input and do something with it. It’s worth taking time to understand how to build different types of forms with rails.
Forms for Models
You’ll store your data in the database. You’ll use rails models to work with database data. Let’s say you’re building a page that allows users to change their profile settings.
A person should be able to change things like their name, age. These fields are stored in the User table and accessible with the User model.
A form_for
helper let’s the user either:
- create a new record for a model in the database.
- update attributes of a specific model in the database.
The form_for
method returns an html form object. Inside the form_for
block, you’ll use helper methods to add labels and fields to your form.
Let’s create a user resource in routes.rb
to give all REST routes.
resources :users
Let’s create a form that creates a user by taking the name attribute. One way we can do that is by calling User.new
to create a blank User instance.
= form_for User.new do |form|
= form.label :name
= form.text_field :name
= form.submit
This form use a POST http verb that routes to the create action in Rails. Let’s create that action and put in a pry breakpoint to see what’s happening.
class UsersController < ApplicationController
def create
binding.pry
end
end
Put in a name and submit the form. Let’s jump into the pry console and see what’s happening.
params
<ActionController::Parameters {"utf8"=>"✓",
"authenticity_token"=>"C39DrozR2ODyI",
"user"=>{"name"=>"Jack"},
"commit"=>"Create User",
"controller"=>"users",
"action"=>"create"} permitted: false>
params are fancy hashes that contain data that is passed back into the controller. Our form passed in user
as a key with a hash value of {"name"=>"Jack"}
to the params.
params[:user] # like a ruby hash, call the user key to get the values
# => {"name"=>"Jack"}
params[:user][:name] # => "Jack"
We wanted to pass the users name and save the user. We have that information in the params. Let’s save.
def create
def create
user = User.new # creates new instance of User
user.name = params[:user][:name] # updated the name of the user object to what was passed in the params.
user.save # save user to database.
end
end
Let’s verify that it worked that that the user was saved through the rails console.
rails console
User.count # 1
User.first # => id: 1, name: "Jack"
When you pass in a model instance as we did with User.new
, the form_for
helper will only accept attributes on that model.
= form_for User.new do |form|
= form.label :name
= form.text_field :name # This works because name is an attribute on User.
= form.text_field :blah # This will NOT work because blah is not an attribute on User.
= form.submit
form_for object as string
Let’s strip the magic of Rails down. You didn’t have to pass in User.new
as the object in the form. You could have passed in any symbol or string. Let’s see that.
= form_for :whatever do |form|
= form.label :name
= form.text_field :name
= form.submit
You could have also passed in = form_for 'whatever' do |form|
as a string.
Take a look at your params in the create controller. You’ll see that Rails passed in a whatever
key with a value of {"name"=>"Bill"}
. Let’s use that to create the user.
def create
user = User.new
user.name = params[:whatever][:name]
user.save
end
Although you can pass in strings and symbols, you won’t see often see this in real world rails applications.
Expanding the form
Let’s add age and email as fields in our form.
= form_for User.new do |form|
= form.label :name
= form.text_field :name
= form.label :age
= form.text_field :age
= form.label :email
= form.text_field :email
= form.submit
Again, we’ll use pry as a breakpoint so we can investigate what was passed to the params in the console.
def create
binding.pry
end
You’ll see that the form passed in a key of user
with a value of {"name"=>"Jack", "age"=>"15", "email"=>"jack@example.com"}
def create
user = User.new
user.name = params[:user][:name]
user.age = params[:user][:age]
user.email = params[:user][:email]
user.save
end
I did the above scenarios in order to show you that forms don’t have to be mystical. However, in real world Rails, you won’t see the controller save model attributes like this. Imagine if there were twenty more attributes we had to save on the User. It would take a lot of typing to get there.
Rails magic comes in when we pass the entire user params hash to the User object and it updates all the passed values automatically.
def create
user = User.new
# Take the the entire hash value of `user` from params and update (saves to database) the user instance.
user.update(params[:user])
end
That is what you see used in real world rails. However, that gives us an error ActiveModel::ForbiddenAttributesError: ActiveModel::ForbiddenAttributesError
.
Strong Parameters
ActiveModel::ForbiddenAttributesError
is there for good reason. Strong parameters were added to Rails to prevent from clients passing in unapproved parameters. We need to specifically permit parameters that we want to be passed into the User model.
We need to use two methods on params. The first is permit
and the second is require
.
# Permit the name attribute to be saved from params.
params[:user].permit(:name)
# Permit name and age.
# Note that the output is the params with name and age only.
params[:user].permit(:name, :age)
Although it’s enough to use permit
to avoid the ForbiddenAttributesError
, most Rails app go one step further. You can also specify which key from the params should be used by using require
and then call permit
on the specific values to be used.
# notice that we call the entire params and the `require` calls the key.
params.require(:user).permit(:name, :age)
def create
user = User.new
# Take the the entire hash value of `user` from params and update (saves to database) the user instance.
user_params = params.require(:user).permit(:name, :age)
user.update(user_params) # Success! Name and age are updated in 1 line.
end
Typically, the variable user_params
is instead added as a private method in a Rails controller.
def create
user = User.new
user.update(user_params)
end
private
def user_params
params.require(:user).permit(:name, :age)
end
Updating Models
Let’s say we want to allow our users to change their name and age through a form. We’d have to call the correct instance of User and pass it into the form_for
helper.
Many tutorials show you that forms are created on a ‘new’ page with a ‘new’ action. It doesn’t matter where you create that form. Projects are not always as simple. Let’s create a form to update a user on the Users index page.
Let’s grab a user in our database and store it in @user
:
def index
@user = User.first
end
In our views, we’ll pass in the @user
instance variable that has the correct user selected.
Refresh your form and notice that Rails automatically loaded the correct name and age in the text fields! The submit button has also changed to say “Update User”.
Let’s change the name and hit submit. Uh oh. You get an error. Instead of going to the ‘create’ action, Rails is taking you to the ‘update’ action. It doesn’t exist yet.
Why are we changing actions? When we create a resource, we use POST HTTP verb which routes to the ‘create’ action. When we passed in an existing user instance variable to form_for
, Rails knows that your intention is to update that resource. The correct HTTP verb to update is PATCH, which takes you to the ‘update’ action in the User controller.
Let’s create that action. Again, put in binding.pry
in that action and try to submit and investigate what gets passed into the params.
def update
binding.pry
end
params
<ActionController::Parameters {"utf8"=>"✓",
"_method"=>"patch",
"authenticity_token"=>"JLyss4cBIK5/qkS",
"user"=>{"name"=>"Jill", "age"=>"19", "email"=>"nothing@hi.com"},
"commit"=>"Update User",
"controller"=>"users",
"action"=>"update",
"id"=>"2"} permitted: false>
You see the user
key that has the updated user values. You also see the id
of the User model that was update. That is what you’re looking for.
def update
@user = User.find(params[:id]) # Find and load the user we're updating.
@user.update(user_params)
end
That’s it. That’s all there is to updating an existing resource. Just pass in an instance variables of what you’re updating to form_for
and you’re good to go.
Form_for syntactic sugar
form_for
somehow knew whether we were creating a resource or updating a resource. It added a lot of Rails magic to our form. Let’s see what is possible to pass in manually to form_for
.
When you write:
form_for @user do |form|
Rails will actually run this:
= form_for @user, as: :user, url: user_path(@user), method: :patch, html: {class: "edit_user", id: "edit_post_2"} do |form|
That’s a mouthful. Let’s break it down.
as: :user
specified that the values passed the params will be stored in a user
key. You’ll be able to grab them with params[:user]
.
as: :whatever
will change that to params[:whatever]
. I don’t see a reason to do this.
url: user_path(@user)
is telling the form to submit to this specific url.
method: :patch
is very important. That’s because if we take a look at all the routes resource :user
has made for us, you’ll notice the https verbs GET, PATCH, and DELETE are all on the user_path. In the case of deleting an object, you’ll have to specify which method you’re using. In our example, rails is specifically using the PATCH method to update the user resource.
You don’t have to use all these options. You could, for example, use something like this:
= form_for @user, url: user_custom_path do |form|
Adding non-model fields to form_for
If you pass a model instance to form_for
, it will allow you to create fields associated with that model. However, there are times when you want to collect information from users that’s not mapped to the model.
= form_for @user do |form|
= form.text_field :name
= form.text_field :age
= form.text_field :city # This won't work as it's not part of the User model.
We can use a few techniques to accomplish this:
- Field Tag
- Virtual Attributes
Field Tag
You can use a field tag within form_for
to collect fields that aren’t relevant to the model.
This is a powerful technique because in real world applications, forms aren’t always limited to one model. Think about when you sign up for a service. You provide your name, email, address, payment information. All of that information should be stored on different tables in your database.
= form_for @user do |form|
= form.text_field :name
= form.text_field :age
= form.fields_for :address do |f|
= f.text_field :city
= f.text_field :county
= form.submit
Look at the params hash that is passed in. You’ll see that a key of address
with field values is passed into our user hash.
"user"=>{"name"=>"Jill",
"age"=>"43",
"address"=>
{"city"=>"Brooklyn",
"county"=>"Nassau"}},
"commit"=>"Update User",
"controller"=>"users",
"action"=>"update",
"id"=>"2"} permitted: false>
You can now grab those values and do whatever you want with them in the controller.
params[:user][:address][:city] # => Brooklyn
params.require(:user).permit(:name, :age, address: {}) # Permit all the values in the address key.
params.require(:user).permit(:name, :age, address: (:city)) # Permit only city from the address key.
Virtual Attributes
Let’s say you need to pass in only one or two variables into the form. Since our user.rb
is a Ruby file, we can simply create a virtual attribute without adding that attribute to the database.
An example where this can be used is to pass a payment token to the controller. A payment token is not a field on the User model.
Let’s play around with this first.
# In ruby.rb
attribute :payment_token
rails console
user = User.first
user.payment_token # nil
user.payment_token = 111 # We can set a value
user.payment_token # => 111 # We can get that value
= form_for @user do |form|
= form.text_field :name
= form.text_field :age
= form.text_field :payment_token # It's a virtual attribute and should work.
= form.submit
If you submit the form and check params, you’ll see:
"user"=>{"name"=>"Jill", "age"=>"55", "payment_token"=>"912391823"}
Hidden Fields
Isn’t it weird to pass the payment token as a visible field? It shouldn’t be visible to the client. In fact, the payment token isn’t something that should be entered by the client.
We want to pass it in the form but keep it hidden. The client should not see the payment token input field. We can do this with a hidden_field
.
= form_for @user do |form|
= form.text_field :name
= form.text_field :age
= form.hidden_field :payment_token, value: "1111111111"
= form.submit
Nested Forms
Let’s try building a complex form that updates two tables. I’m going to add an address table with a city, state, and user_id column.
One to One Relationship Nested Form
rails g model address city:string state:string user:references
rails db:migrate
class Address < ApplicationRecord
belongs_to :user
end
class User < ApplicationRecord
has_one :address
end
This form will have a one-to-one relationship. A user has one address and an address has one user. If we want rails to update the User model and the Address model at the same time, we need to add accepts_nested_attributes_for
on the user.
class User < ApplicationRecord
has_one :address
accepts_nested_attributes_for :address
end
The form is not going to be much different. We simply use a fields_for
tag to pass in the address
key with city and state values.
= form_for @user do |form|
= form.text_field :name
= form.text_field :age
= form.fields_for :address do |f|
= f.text_field :city
= f.text_field :state
= form.submit
What is the magic behind accepts_nested_attributes_for
? By adding that method to User, Ruby writes a setter method on User. Here’s what happens in the background
class User < ApplicationRecord
def address_attributes=(attributes)
end
end
One to Many Relationship Nested Form
Let’s say a person has multiple addresses. There’s not much of a change here.
class User < ApplicationRecord
has_many :addresses # Note it's plural
accepts_nested_attributes_for :addresses # Note it's plural
end
Since accepts_nested_attributes_for
takes a plural addresses
, that means the underlying setter method Rails will use addresses_attributes
is also plural. Because Rails is forgiving, they will also allow you to keep the singular way address_attributes
.
def user_params
params.require(:user).permit(:name, :age, addresses_attributes: {}) # make addresses_attributes plural
end
The form stays the same! Rails will automatically iterate through the fields_for :addreses
if there are multiple addresses in the database and you’ll see multiple input fields in your form.
= form_for @user do |form|
= form.text_field :name
= form.text_field :age
= form.fields_for :addresses do |f| # Will automatically iterate.
= f.text_field :city
= f.text_field :state
= form.submit
Let’s create another address in console.
rails console
user = User.first
user.addresses.new(city: "san diego", state: "california")
Refresh the form and you’ll see there are two iterations of the address fields. Make a change and submit the form. Add binding.pry
in the update controller action and take a look at the params.
"user"=>{"name"=>"Stacy",
"age"=>"25",
"addresses_attributes"=>{
"0"=>{"city"=>"San", "state"=>"CA", "id"=>"2"},
"1"=>{"city"=>"san diego", "state"=>"california", "id"=>"3"}
}
}
The addresses_attributes
key is has address keys of 0 and 1 passed with the different addresses. Notice how 0 and 1 also pass in an address id. If you pass in a new address into addresses_attributes
hash without an id, Rails will know that this is a new address and it should save it. If an existing id is passed, Rails knows to try to find an address with that id and update it.
Adding an address into the addresses_attributes
field without an id is useful when you want to edit a form and dynamically add another address using javascript. We’ll look at that another time.
Deeply Nested Forms
We can take form nesting even further. Let’s say a user has_many addresses. Addresses have many phone numbers. Let’s say we want to edit all of this in one form. I’m not saying this is an ideal idea. I think a form like this becomes too deeply nested. But it’s possible to do.
Let’s create a Phone resource that will create a Phone model and Phone controller.
rails g resource phone number:string
rails db:migrate
class Address < ApplicationRecord
belongs_to :user
has_many :phones
accepts_nested_attributes_for :phones
end
Modify the form to add another fields_for
tag for phones that is nested within the address fields_for
tag.
= form_for @user do |form|
= form.text_field :name
= form.text_field :age
= form.fields_for :addresses do |address_form|
= address_form.text_field :city
= address_form.text_field :state
= address_form.fields_for :phones do |phone_form|
= phone_form.text_field :number
= form.submit
Submit the form and look at the params again. Notice that the phone attributes are NESTED within the address attributes hash.
{"user"=>
{"name"=>"Stacy",
"age"=>"25",
"id" => "2"
"addresses_attributes"=>
{"0"=> {
"city"=>"san diego",
"state"=>"california",
"id"=>"3",
"phones_attributes"=>
{"0"=>{
"number"=>"2025551000",
"id"=>"1"}
}
}
}
}
Did you notice that we didn’t have to update the user_params to include phone_attributes
? Why does the form work without us needing to add address_attributes
.
# The address_attributes: {} hash below is empty. It will accept ANYTHING that's added into it.
# Since phone_attributes is inside addresses_attributes, we don't need to manually add it.
def user_params
params.require(:user).permit(:name, :age, addresses_attributes: {})
end
The more you play around with forms on your own time, the better you’ll get applying them to real problems.