Nested Forms With Turbo (without dependencies)

3d rendering of russian dolls

No time do it yourself? Rails Designer comes with a pre-built solution for nested forms with Turbo.


Nested forms is a common concept in most (Rails) SaaS apps. Examples include a survey that accepts multiple questions, recipe with many ingredients or an (eCommerce) product with many variants.

Out-of-the-box Rails has all the elements to support this kind of business logic through the accepts_nested_attributes_for “macro” and the fields_for helper. But on the front-end you had to previously use third-party dependencies. But with Turbo, specifically with the introduction of supporting get requests, that’s all you need today.

The end result will look something like this:

Image description It looks crap, but it works! 😄

Let’s go over the basic steps needed to create a Survey with multiple Questions. For this I assume you have a modern Rails app at hand.

Getting the Basics Done

Let’s generate the two models and make sure all the logic for nested forms is in place.

  • Survey; rails generate model Survey name
  • Question; rails generate model Question survey:belongs_to content:text

Then tweak the Survey model:

class Survey < ApplicationRecord
  has_many :questions
  accepts_nested_attributes_for :questions
end

Next, a simple controller: rails generate controller Surveys show new. Let’s extend this created controller like so:

class SurveysController < ApplicationController
  def show
    @survey = Survey.find(params[:id])
  end

  def new
    @survey = Survey.new
  end

  def create
    @survey = Survey.new(survey_params)

    if @survey.save
      redirect_to @survey
    else
      render :new
    end
  end

  private

  def survey_params
    params.require(:survey).permit(:name, questions_attributes: [:content])
  end
end

The above nested survey_params is often the tricky bit to get right. Getting the passed attributes from the params hash is often an exercise in patience (just copy above and you’re fine). I’ve personally wasted hours over the last 10 years on this. 😅

Let’s update the app/views/surveys/new.html.erb view.

<h1>New Survey</h1>

<%= form_with model: @survey do |form| %>
  <div>
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

  <div id="questions"></div>

  <div>
    <%= button_tag "Add Question" %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

And something simple for the show-action (app/views/surveys/show.html.erb) so we can see our glorious work.

<h1><%= @survey.name %></h1>

<% @survey.questions.each do |question| %>
  <p>
    <%= question.content %>
  </p>
<% end %>

And with adding resources :surveys, only: %w[show new create] to config/routes.rb, and removing the unneeded generated routes, all the basics are in place and you can navigate to localhost:3000/surveys/new and create your first Survey. But no nested question’s yet…

Using Turbo Streams to Create Nested Forms

First the partial that holds the fields for the Question object (app/views/surveys/_question_fields.html.erb):

<div>
  <%= form.label :content, "Question" %>
  <%= form.text_area :content %>
</div>

Simple enough.

Let’s wire up the button with the Add Question value. Replace it like so:

<%= button_tag "Add Question", formaction: new_question_path, formmethod: :get, data: {turbo_stream: true} %>

This uses two attributes that might be new to you: formaction and formmethod. Check out this article about forms inside forms for all the details. Next to that is data: {turbo_stream: true}. This will request a turbo_stream response that will be created in a minute.

The above new_question_path is not created. A simple controller and route will do:

  • rails generate controller Questions new;
  • add resources :questions, only: %w[new] to config/routes.rb.

The last thing needed is a turbo_stream response at app/views/questions/new.turbo_stream.erb.

<%= turbo_stream.append "questions" do %>
  <%= fields_for 'survey[questions_attributes][]', Question.new, index: Time.current.to_i do |form| %>
    <%= render "surveys/question_fields", form: form %>
  <% end %>
<% end %>

The only exciting part here is index: Time.current.to_i. This generates a unique identifier for each new set of nested form fields to prevent conflicts when adding multiple questions (otherwise it would only create one Question).

Now when you navigate to the surveys/new screen, you should be able to click Add Question and a new instance of surveys/question_fields.html.erb should appear. 🤯

And what’s more mind-blowing. You can create as many questions as you want, and then click Create Survey and your Survey and all your questions are saved to the database. 🤯🤯

If you have ever used any third-party plugin (like Cocoon), you know how amazing it is to do this without any gems! ☺️

Of course this is missing a fair bit of functionality, like removing a question. But with the same technique using Questions#destroy you should be able to pull that off yourself fairly easily. ✌️

If you have any questions about this or ideas to improve, feel free to reach out.


There is even an easier way to add nested forms with Turbo to your Rails app.

Get UI & Product Engineering Insights for Rails Apps (and product updates!)

Published at . Last updated at . Have suggestions or improvements on this content? Do reach out.

UI components for Ruby on Rails apps

$ 129 one-time
payment

Get Access
  • One-time Payment

  • Access to the Entire Library

  • Built using ViewComponent

  • Designed with Tailwind CSS

  • Enhanced with Hotwire