Introduction
Quite often in my applications, I want to create multiple types of objects in one form submission. For example, when one instance is created, you want the associations to be created at the same time. Everything I do here is based on the official documentation. I'm going to move forward using this database schema:
As you can see here, I have one group
(with a name
) which has_many :people
. One person
that belongs_to :group
. If I follow regular Rails CRUD actions, I would have to create a group then follow up with creating the people in this group separately. It might look something like this:
So how can I create both a group
but also people
at the same time?
Form Setup
I would love my form to look more like this:
This allows me to create a group
with the name
but then also add people
, which has first_name
and last_name
, into that group.
I'm using the gem Simple Form for my forms. So the original form would look like this:
<%= simple_form_for @group do |f| %>
<%= f.input :name, placeholder: 'The Berkleys' %>
<%= f.submit 'Submit', class: 'btn btn-primary mt-3' %>
<% end %>
This needs two things in order to work:
- Create the
@group
instance variable in your controller#action - Add the route to create a
group
in yourroutes.rb
But this obviously only allows me to create a group. We want to add the people into this form. For this, we're going to use simple_fields_for to build a form within a form.
Nested Form
To add a person
in our app, we would normally have a form like this:
<%= simple_form_for @person do |f| %>
<%= f.input :first_name, placeholder: 'John' %>
<%= f.input :last_name, placeholder: 'Doe' %>
<%= f.submit 'Submit', class: 'btn btn-primary mt-3' %>
<% end %>
But the idea here is to move the person
form into our group
form. This is where our simple_fields_for
comes into play. Our nested form will look like this:
<%= simple_form_for @group do |f| %>
<!-- Anything a group would have -->
<%= f.input :name, placeholder: 'The Berkleys' %>
<!-- The nested form -->
<%= f.simple_fields_for :people do |person_form| %>
<div class="d-flex align-items-center w-100">
<!-- Anything a person would have -->
<%= person_form.input :first_name, placeholder: 'John' %>
<%= person_form.input :last_name, placeholder: 'John' %>
</div>
<% end %>
<%= f.submit 'Submit', class: 'btn btn-primary mt-3' %>
<% end %>
This doesn't just work by default, though. We have three more steps to get this setup working:
- Tell our
group
model that we'll be creating people along with the group - Define an object in the controller so our form helper can build it.
- Update the strong parameters since we're sending more information from the form.
Update the Model
Let's go into our group.rb
to tell our model that we'll also be creating people at the same time. accepts_nested_attributes_for
comes from the docs.
# group.rb (model)
# ...
has_many :people
accepts_nested_attributes_for :people, allow_destroy: true
# ...
Update the Controller
So normally in our new
action, we create an empty instance variable to give to our form helper.
def new
@group = Group.new
end
But because we're also creating people
at the same time, we need to set this up in our controller too.
def new
@group = Group.new
@group.people.build # needed for nested form
end
What is the @group.people.build
doing?
It is used to initialize a new person
object associated with the @group
instance. The build
method in Active Record (when used on an association like @group.people
) creates a new associated person
object in memory without saving it to the database.
Why is the @group.people.build
needed?
Our nested form expects at least one person
object to exist, so that it can generate the input fields for first_name
and last_name
.
Update the Strong Params
Now that we're creating two things at once, it's still only getting sent to the original form location. In our example, it's getting sent to the groups#create
action. We actually don't need to change how a group gets created. It could potentially look like this (and notice that there is nothing about a person
in here):
def create
@group = Group.new(group_params)
@group.user = current_user
if @group.save
redirect_to group_path(@group)
else
redirect_to groups_path
end
end
See? Nothing about people
. But we will have to change one thing, and that's our strong parameters. Basically we told our group
model that we'd also be passing people into it. So let's take a look at the parameters that got sent from the form:
{
"group"=> {
"name"=>"The Berkleys",
"people_attributes"=> {
"0"=> { "first_name"=>"Douglas", "last_name"=>"Berkley" }
}
}
}
The params looks a bit strange, right? It's normal to see our "group"=> { "name"=>"The Berkleys" }"
but the people_attributes
is new from our nested form and accepts_nested_attributes_for
. There's a 0
in the people_attributes
from the form because in theory, we can now create as many people as we want for the one group inside the form.
So now let's update our strong parameters to accept :people_attributes
.
def group_params
params.require(:group).permit(:name, people_attributes: [:first_name, :last_name, :_destroy])
end
Now our form should be good to go!
Going further...
What if I want to create more than one person
at a time? As of now, our nested form just has one option for our person
(just one first_name
and last_name
input). I could hard-code it by just copying and pasting our nested part in our form:
<%= simple_form_for @group do |f| %>
<!-- Anything a group would have -->
<%= f.input :name, placeholder: 'The Berkleys' %>
<!-- FIRST PERSON -->
<%= f.simple_fields_for :people do |person_form| %>
<div class="d-flex align-items-center w-100">
<!-- Anything a person would have -->
<%= person_form.input :first_name, placeholder: 'John' %>
<%= person_form.input :last_name, placeholder: 'John' %>
</div>
<% end %>
<!-- SECOND PERSON -->
<%= f.simple_fields_for :people do |person_form| %>
<div class="d-flex align-items-center w-100">
<!-- Anything a person would have -->
<%= person_form.input :first_name, placeholder: 'John' %>
<%= person_form.input :last_name, placeholder: 'John' %>
</div>
<% end %>
<%= f.submit 'Submit', class: 'btn btn-primary mt-3' %>
<% end %>
This is fine if we know exactly how many people I want to add when I create a group. But it would be much better if it was dynamic. For example, I can add 1 person or 10 people.
To make it dynamic, it can be a bit tricky. So I like to use the Rails Nested Form - Stimulus component which simplifies the process. I'll let you follow the other tutorial to solve that issue, but in the end, we could have a nice dynamic form that looks like this:
Top comments (0)