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:
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.