In a previous article I wrote about nested forms with Turbo and no other dependencies. This is a great solution that works remarkably well, but I want to explore another option using Stimulus.
Why explore another option? While the Turbo solution works great, there might be cases the round-trip to the server might be a bit much for simple, static HTML. After all, there’s no extra data needed from the server to render the nested fields.
So let’s build the same feature, but now with Stimulus instead of Turbo.
Also for this article, I assume you have a modern Rails app ready and the following basics in place:
- Survey;
rails generate model Survey name
- Question;
rails generate model Question survey:belongs_to content:text
Update the Survey model:
class Survey < ApplicationRecord
has_many :questions
accepts_nested_attributes_for :questions
end
Add a simple controller to go with it. rails generate controller Surveys show new
. Then update it as follows:
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
As mentioned in the earlier article, make sure the survey_params
are set up like above.
Now update the two views:
<h1>New Survey</h1>
<%= form_with model: @survey do |form| %>
<div>
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div></div>
<div>
<%= button_tag "Add Question", type: :button %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
And the show action (just so there’s something to see after create):
<h1><%= @survey.name %></h1>
<% @survey.questions.each do |question| %>
<p>
<%= question.content %>
</p>
<% end %>
And as a last step add resources :surveys, only: %w[show new create]
to config/routes.rb (and remove the generated routes).
Now all the basics are in place and it is exactly the same as before with the Turbo version.
Nested Forms using Stimulus
The approach will be to render a _questions_fields partial in a <template />
element that can then be injected into a HTML element.
Let’s create that partial (app/views/surveys/_question_fields.html.erb) first:
<div>
<%= form.label :content, "Question" %>
<%= form.text_area :content %>
</div>
Next up is the Stimulus controller that will make this all work: rails generate stimulus nested-fields
.
Let’s first wire up the HTML and then look at the Stimulus controller. Inside the app/views/surveys/new.html.erb let’s make a few changes:
<%= form_with model: @survey, data: {controller: "nested-fields"} do |form| %>
# …
<div data-nested-fields-target="fields"></div>
<div>
<%= button_tag "Add Question", type: :button, data: {action: "nested-fields#append"} %>
<template>
<%= fields_for 'survey[questions_attributes][]', Question.new, index: "__INDEX__" do |form| %>
<%= render "surveys/question_fields", form: form %>
<% end %>
</template>
</div>
# …
<% end %>
From top to bottom:
- add the nested-fields controller to the form;
- add the target for the questions;
- add the action to the button;
- insert the template with the question_fields partial (notice the
index: "__INDEX__"
).
Alright, let’s look at the Stimulus controller. It will be pretty straightforward!
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["fields", "template"];
append() {
this.fieldsTarget.insertAdjacentHTML("beforeend", this.#templateContent);
}
// private
get #templateContent() {
return this.templateTarget.innerHTML.replace(/__INDEX__/g, Date.now());
}
}
Most of this is simple enough. Within the append()
function, the result of this.#templateContent
is added before the end of this.fieldsTarget
.
Within #templateContent()
there is something more interesting going on. It generates an unique “identifier” for new nested form fields by replacing theINDEX
placeholder with a timestamp. This is needed so Rails can differentiate between multiple new records in a single form submission, allowing it to correctly process and save all added nested attributes. If the value would be static, it would only save the last added nested field.
Check out localhost:3000/surveys/new and you should be able to insert multiple question fields. 🥳
And there you have it: the basics of nested fields using Stimulus and no third-party dependencies. 🤯 Again, just with the Turbo version, there is still room for improvement here: removing a nested field or maybe even re-ordering nested fields. I will leave that up to you. 💪