DEV Community

Douglas Berkley
Douglas Berkley

Posted on

How to use nested attributes in Ruby on Rails (create associated objects in one form)

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:

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:
nested form example

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:
simple form example
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 %>
Enter fullscreen mode Exit fullscreen mode

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 your routes.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 %>
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

This doesn't just work by default, though. We have three more steps to get this setup working:

  1. Tell our group model that we'll be creating people along with the group
  2. Define an object in the controller so our form helper can build it.
  3. 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
# ...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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:

final form screenshot

Top comments (0)