Handling Multiple Model Objects in a Single Rails Form

Gaetano OsurGaetano Osur
4 min read

I have been working on a flight booking project that required me to dynamically create passengers upon booking a flight. This led me to a painstakingly interesting journey of learning about nested forms in rails. In this blog, we'll explore how to handle forms in Rails that work with more than one model object, focusing on nested forms.

Forms are an essential part of web applications, allowing users to submit data that gets processed on the backend. In Ruby on Rails, handling forms for a single model is straightforward using form_with or form_for. However, there are cases where you need to submit data to multiple models in one form, such as when creating a Post with multiple associated Comment objects.

What Are Nested Forms?

Nested forms allow you to create or update a parent object and its associated child objects in a single form submission. This technique is extremely useful when you have a one-to-many or many-to-many relationship between models.

Example Scenario

Imagine you're building a blog where a user can create a post and add multiple comments at the same time. You want a single form that allows the user to:

  1. Enter the post's title and body.

  2. Add one or more comments.

Let’s dive into how to implement this.

Step 1: Set Up the Models and Associations

First, we'll create the Post and Comment models with a one-to-many relationship. A Post can have many Comments, and a Comment belongs to a Post.

# app/models/post.rb
class Post < ApplicationRecord
  has_many :comments, inverse_of: :post
  accepts_nested_attributes_for :comments, allow_destroy: true
end

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post
end

In the Post model:

  • has_many :comments defines the one-to-many relationship.

  • accepts_nested_attributes_for :comments allows the form to handle attributes for comments along with post.

The allow_destroy: true option will let us remove comments directly from the form if needed.

Step 2: Create the Controller

Next, we need to modify the controller to handle the nested attributes. In the PostsController, we need to build comments when initializing a new post, and permit nested attributes for comments in the post_params.

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def new
    @post = Post.new
    @post.comments.build  # Builds an empty comment for the form
  end

  def create
    @post = Post.new(post_params)
    if @post.save
      redirect_to @post, notice: 'Post and comments were successfully created.'
    else
      render :new
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :body, comments_attributes: [:id, :content, :_destroy])
  end
end

Here:

  • In the new action, we call @post.comments.build to ensure the form displays fields for a new comment.

  • In the post_params, we permit comments_attributes, which will allow nested data to be passed for the associated comments.

Step 3: Build the Form

Now that the models and controller are set up, we can create the form that will allow the user to enter both the post details and associated comments.

<!-- app/views/posts/new.html.erb -->
<%= form_with(model: @post, local: true) do |form| %>
  <%= form.label :title %>
  <%= form.text_field :title %>

  <%= form.label :body %>
  <%= form.text_area :body %>

  <h3>Comments</h3>

  <%= form.fields_for :comments do |comment_form| %>
    <div class="comment-fields">
      <%= comment_form.label :content, "Comment" %>
      <%= comment_form.text_area :content %>
    </div>
  <% end %>

  <%= form.submit 'Create Post and Comments' %>
<% end %>

Explanation:

  • We use form_with to generate the form for the Post model.

  • Inside the form, fields_for :comments is used to create fields for the associated Comment objects. This tells Rails that we’re accepting nested attributes for comments.

  • The form will display fields for entering the post's title, body, and one comment.

Step 4: Handling Multiple Comments

To handle more than one comment in the form, we need to allow the user to add multiple comments. This can be done by building multiple comments in the new action of the PostsController.

# app/controllers/posts_controller.rb
def new
  @post = Post.new
  3.times { @post.comments.build }  # Builds three empty comments for the form
end

In this case, we build three empty comments, so the form will display fields for three comments. You can adjust the number of comments as needed.

In the view, you don’t need to change anything from the previous form example. fields_for will iterate through each of the built comments and render form fields for them.

<!-- app/views/posts/new.html.erb -->
<%= form_with(model: @post, local: true) do |form| %>
  <%= form.label :title %>
  <%= form.text_field :title %>

  <%= form.label :body %>
  <%= form.text_area :body %>

  <h3>Comments</h3>

  <%= form.fields_for :comments do |comment_form| %>
    <div class="comment-fields">
      <%= comment_form.label :content, "Comment" %>
      <%= comment_form.text_area :content %>
    </div>
  <% end %>

  <%= form.submit 'Create Post and Comments' %>
<% end %>

Now, when the user submits the form, all the entered comments will be saved along with the post.

Conclusion

Handling multiple model objects in a single Rails form can be done effectively using nested forms. By leveraging accepts_nested_attributes_for in the model, Rails allows you to seamlessly handle parent-child relationships through forms.

In this example, we covered:

  1. Setting up associations between models.

  2. Building the necessary logic in the controller to handle nested attributes.

  3. Creating a form that submits data for both the Post and multiple Comments.

This approach is especially useful when dealing with complex forms involving parent-child relationships, making your Rails applications more powerful and user-friendly.

0
Subscribe to my newsletter

Read articles from Gaetano Osur directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Gaetano Osur
Gaetano Osur