Building Nested Forms in Rails with Stimulus

railsdesigner

Rails Designer

Posted on September 5, 2024

Building Nested Forms in Rails with Stimulus

This article was originally published on the Rails Designer Blog. Rails Designer is the first professionally-designed UI components library for Rails.


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.

Image description

So, again 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

🧑‍🎨✨ Looking to up your Rails app's UI? Rails Designer can help you. Explore all the options today. ✌️


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 it is exactly the same as before with the article using 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>
Enter fullscreen mode Exit fullscreen mode

Next up is the Stimulus controller that will make this all work: rails generate stimulus nested-fields.

Let's first wire it 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", 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 %>
Enter fullscreen mode Exit fullscreen mode

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());
  }
}
Enter fullscreen mode Exit fullscreen mode

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 the*INDEX* 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. 💪

💖 💪 🙅 🚩
railsdesigner
Rails Designer

Posted on September 5, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related