Nested forms in Rails

#tutorial

What I’m trying to achieve

I have two models:

When a user creates a Training Session, I want a form that contains fields for Training Steps inside the Training Session form. When I submit the form, I will create a Training Session object along with the related Training Steps.

1. Accepting nested attributes

First, I added accepts_nested_attributes_for to my Training Session model.

class TrainingStep < ApplicationRecord
  belongs_to :training_session
end

class TrainingSession < ApplicationRecord
  has_many :training_steps
  accepts_nested_attributes_for :training_steps, reject_if: lambda { |attributes| attributes['description'].blank? }
end

By default, nested attribute updating is turned off. With accepts_nested_attributes_for I’m able to create a Training Session and the Training Steps in one go. With this method, TrainingSession will now accept a key training_steps_attributes as a value. With reject_if, it will also silently ignore any records that don’t pass the lambda.

2. Adding the controller new method

class TrainingSessionsController < ApplicationController
  def new
    @training_session = TrainingSession.new
    2.times { @training_session.training_steps.build }
  end
end

In my new method, I’ll initialise a new Training Session model assigned to @training_session, and build two training_steps models along with it. @training_session will be used in the view.

3. Adding a nested form in the view

<%= form_with model: @training_session do |form| %>
  Session Steps:
  <ul>
    <%= form.fields_for :training_steps do |training_steps_form| %>
      <li>
        <%= training_steps_form.label :description %>
        <%= training_steps_form.text_field :description %>
      </li>
    <% end %>
  </ul>
  <%= form.submit "Create New Session" %>
<% end %>

In the view, new.html.erb, we’re using form_with to build a form that routes to the correct create path. Rails will infer the correct routes based on the resource, in this case @training_session. fields_for is then used to refer to the nested training_steps objects. This method is used to refer to a specific model object, training_steps in this case, but it yields to the form_with element. In other words, you would use fields_for inside a form_with.

TIL - form_with is the new way to build forms. I used form_for in the past but that’s now obselete (here’s the PR with the change to form_with).

4. Creating a nested object

class TrainingSessionsController < ApplicationController
  def new
    @training_session = TrainingSession.new
    2.times { @training_session.training_steps.build }
  end

  def create
    training_session = TrainingSession.new(training_session_params)
    if training_session.save
      training_session.save
      redirect_to training_sessions_path
    else
      render "new"
    end
  end

  private

  def training_session_params
    params.require(:training_session).permit(training_steps_attributes: [:id, :description])
  end
end

In the controller, we can add the create method to handle creating the Training Session object. Notice that we use the training_steps_attributes in training_session_params. We’ll pass these parameters directly into TrainingSession.new and when we save Rails will create both the TrainingSession object and the associated Training Steps objects.

Want to stay connected?

Subscribe to my newsletter

Weekly, bite-sized, practical tips and insights that have meaningfully improved how I code, design, and write.

No spam. Unsubscribe anytime.